[
  {
    "path": ".coveragerc",
    "content": "[run]\nbranch = true\nparallel = true\nsource =\n    telethon\n\n[report]\nprecision = 2\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "content": "name: Bug Report\ndescription: Create a report about a bug inside the library.\nbody:\n\n  - type: textarea\n    id: reproducing-example\n    attributes:\n      label: Code that causes the issue\n      description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies.\n      placeholder: |\n        ```python\n        from telethon.sync import TelegramClient\n        ...\n\n        ```\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected behavior\n      description: Explain what you should expect to happen. Include reproduction steps.\n      placeholder: |\n        \"I was doing... I was expecting the following to happen...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual-behavior\n    attributes:\n      label: Actual behavior\n      description: Explain what actually happens.\n      placeholder: |\n        \"This happened instead...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: traceback\n    attributes:\n      label: Traceback\n      description: |\n        The traceback, if the problem is a crash.\n      placeholder: |\n        ```\n        Traceback (most recent call last):\n          File \"code.py\", line 1, in <code>\n\n        ```\n\n  - type: input\n    id: telethon-version\n    attributes:\n      label: Telethon version\n      description: The output of `python -c \"import telethon; print(telethon.__version__)\"`.\n      placeholder: \"1.x\"\n    validations:\n      required: true\n\n  - type: input\n    id: python-version\n    attributes:\n      label: Python version\n      description: The output of `python --version`.\n      placeholder: \"3.x\"\n    validations:\n      required: true\n\n  - type: input\n    id: os\n    attributes:\n      label: Operating system (including distribution name and version)\n      placeholder: Windows 11, macOS 13.4, Ubuntu 23.04...\n    validations:\n      required: true\n\n  - type: textarea\n    id: other-details\n    attributes:\n      label: Other details\n      placeholder: |\n        Additional details and attachments. Is it a server? Network condition?\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: Read this carefully, we will close and ignore your issue if you skimmed through this.\n      options:\n        - label: The error is in the library's code, and not in my own.\n          required: true\n        - label: I have searched for this issue before posting it and there isn't an open duplicate.\n          required: true\n        - label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Ask questions in StackOverflow\n    url: https://stackoverflow.com/questions/ask?tags=telethon\n    about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed\n  - name: Find about updates and our Telegram groups\n    url: https://t.me/s/TelethonUpdates\n    about: Be notified of updates, chat with other people about the library or ask questions in these groups\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation-issue.yml",
    "content": "name: Documentation Issue\ndescription: Report a problem with the documentation.\nlabels: [documentation]\nbody:\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: Describe the problem in detail.\n      placeholder: This part is unclear...\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: Read this carefully, we will close and ignore your issue if you skimmed through this.\n      options:\n        - label: This is a documentation problem, not a question or a bug report.\n          required: true\n        - label: I have searched for this issue before posting it and there isn't a duplicate.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yml",
    "content": "name: Feature Request\ndescription: Suggest ideas, changes or other enhancements for the library.\nlabels: [enhancement]\nbody:\n\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Describe your suggested feature\n      description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works?\n      placeholder: \"It should work like this...\"\n    validations:\n      required: true\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: Read this carefully, we will close and ignore your issue if you skimmed through this.\n      options:\n        - label: I have searched for this issue before posting it and there isn't a duplicate.\n          required: true\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!--\nThanks for the PR! Please keep in mind that v1 is *feature frozen*.\nNew features very likely won't be merged, although fixes can be sent.\nAll new development should happen in v2. Thanks!\n-->\n"
  },
  {
    "path": ".github/workflows.disabled/python.yml",
    "content": "name: Python Library\n\non: [push, pull_request]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [\"3.5\", \"3.6\", \"3.7\", \"3.8\"]\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Set up env\n      run: |\n        python -m pip install --upgrade pip\n        pip install tox\n    - name: Lint with flake8\n      run: |\n        tox -e flake\n    - name: Test with pytest\n      run: |\n        # use \"py\", which is the default python version\n        tox -e py\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated code\n/telethon/tl/functions/\n/telethon/tl/types/\n/telethon/tl/alltlobjects.py\n/telethon/errors/rpcerrorlist.py\n\n# User session\n*.session\n/usermedia/\n\n# Builds and testing\n__pycache__/\n/dist/\n/build/\n/*.egg-info/\n/readthedocs/_build/\n/.tox/\n\n# API reference docs\n/docs/\n\n# File used to manually test new changes, contains sensitive data\n/example.py\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "-   repo: git://github.com/pre-commit/pre-commit-hooks\n    sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469\n    hooks:\n    -   id: check-added-large-files\n    -   id: check-case-conflict\n    -   id: check-merge-conflict\n    -   id: check-symlinks\n    -   id: check-yaml\n    -   id: double-quote-string-fixer\n    -   id: end-of-file-fixer\n    -   id: name-tests-test\n    -   id: trailing-whitespace\n-   repo: git://github.com/pre-commit/mirrors-yapf\n    sha: v0.11.1\n    hooks:\n    -   id: yapf\n-   repo: git://github.com/FalconSocial/pre-commit-python-sorter\n    sha: 1.0.4\n    hooks:\n    -   id: python-import-sorter\n        args:\n        - --silent-overwrite\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# https://docs.readthedocs.io/en/stable/config-file/v2.html\nversion: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\nsphinx:\n  configuration: readthedocs/conf.py\n\nformats:\n  - pdf\n  - epub\n\npython:\n  install:\n    - requirements: readthedocs/requirements.txt\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016-Present LonamiWebs\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.rst",
    "content": "Moved to https://codeberg.org/Lonami/Telethon. The GitHub repository may be deleted in the future.\n\n----\n\nTelethon\n========\n.. epigraph::\n\n  ⭐️ Thanks **everyone** who has starred the project, it means a lot!\n\n|logo| **Telethon** is an asyncio_ **Python 3**\nMTProto_ library to interact with Telegram_'s API\nas a user or through a bot account (bot API alternative).\n\n.. important::\n\n    If you have code using Telethon before its 1.0 version, you must\n    read `Compatibility and Convenience`_ to learn how to migrate.\n    As with any third-party library for Telegram, be careful not to\n    break `Telegram's ToS`_ or `Telegram can ban the account`_.\n\nWhat is this?\n-------------\n\nTelegram is a popular messaging application. This library is meant\nto make it easy for you to write Python programs that can interact\nwith Telegram. Think of it as a wrapper that has already done the\nheavy job for you, so you can focus on developing an application.\n\n\nInstalling\n----------\n\n.. code-block:: sh\n\n  pip3 install telethon\n\n\nCreating a client\n-----------------\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events, sync\n\n    # These example values won't work. You must get your own api_id and\n    # api_hash from https://my.telegram.org, under API Development.\n    api_id = 12345\n    api_hash = '0123456789abcdef0123456789abcdef'\n\n    client = TelegramClient('session_name', api_id, api_hash)\n    client.start()\n\n\nDoing stuff\n-----------\n\n.. code-block:: python\n\n    print(client.get_me().stringify())\n\n    client.send_message('username', 'Hello! Talking to you from Telethon')\n    client.send_file('username', '/home/myself/Pictures/holidays.jpg')\n\n    client.download_profile_photo('me')\n    messages = client.get_messages('username')\n    messages[0].download_media()\n\n    @client.on(events.NewMessage(pattern='(?i)hi|hello'))\n    async def handler(event):\n        await event.respond('Hey!')\n\n\nNext steps\n----------\n\nDo you like how Telethon looks? Check out `Read The Docs`_ for a more\nin-depth explanation, with examples, troubleshooting issues, and more\nuseful information.\n\n.. _asyncio: https://docs.python.org/3/library/asyncio.html\n.. _MTProto: https://core.telegram.org/mtproto\n.. _Telegram: https://telegram.org\n.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html\n.. _Telegram's ToS: https://core.telegram.org/api/terms\n.. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library\n.. _Read The Docs: https://docs.telethon.dev\n\n.. |logo| image:: logo.svg\n    :width: 24pt\n    :height: 24pt\n"
  },
  {
    "path": "dev-requirements.txt",
    "content": "pytest\npytest-cov\npytest-asyncio\n"
  },
  {
    "path": "optional-requirements.txt",
    "content": "cryptg\npython-socks[asyncio]\nhachoir\npillow\nisal\n"
  },
  {
    "path": "pyproject.toml",
    "content": "# https://snarky.ca/what-the-heck-is-pyproject-toml/\n[build-system]\nrequires = [\"setuptools\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n# Need to use legacy format for the time being\n# https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini\n[tool.tox]\nlegacy_tox_ini = \"\"\"\n[tox]\nenvlist = py35,py36,py37,py38\n\n# run with tox -e py\n[testenv]\ndeps =\n    -rrequirements.txt\n    -roptional-requirements.txt\n    -rdev-requirements.txt\ncommands =\n    # NOTE: you can run any command line tool here - not just tests\n    pytest {posargs}\n\n# run with tox -e flake\n[testenv:flake]\ndeps =\n    -rrequirements.txt\n    -roptional-requirements.txt\n    -rdev-requirements.txt\n    flake8\ncommands =\n    # stop the build if there are Python syntax errors or undefined names\n    flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics\n    # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n    flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics\n\n\"\"\"\n"
  },
  {
    "path": "readthedocs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = Telethon\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "readthedocs/basic/installation.rst",
    "content": ".. _installation:\n\n============\nInstallation\n============\n\nTelethon is a Python library, which means you need to download and install\nPython from https://www.python.org/downloads/ if you haven't already. Once\nyou have Python installed, `upgrade pip`__ and run:\n\n.. code-block:: sh\n\n    python3 -m pip install --upgrade pip\n    python3 -m pip install --upgrade telethon\n\n…to install or upgrade the library to the latest version.\n\n.. __: https://pythonspeed.com/articles/upgrade-pip/\n\nInstalling Development Versions\n===============================\n\nIf you want the *latest* unreleased changes,\nyou can run the following command instead:\n\n.. code-block:: sh\n\n    python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip\n\n.. note::\n\n    The development version may have bugs and is not recommended for production\n    use. However, when you are `reporting a library bug`__, you should try if the\n    bug still occurs in this version.\n\n.. __: https://github.com/LonamiWebs/Telethon/issues/\n\n\nVerification\n============\n\nTo verify that the library is installed correctly, run the following command:\n\n.. code-block:: sh\n\n    python3 -c \"import telethon; print(telethon.__version__)\"\n\nThe version number of the library should show in the output.\n\n\nOptional Dependencies\n=====================\n\nIf cryptg_ is installed, **the library will work a lot faster**, since\nencryption and decryption will be made in C instead of Python. If your\ncode deals with a lot of updates or you are downloading/uploading a lot\nof files, you will notice a considerable speed-up (from a hundred kilobytes\nper second to several megabytes per second, if your connection allows it).\nIf it's not installed, pyaes_ will be used (which is pure Python, so it's\nmuch slower).\n\nIf pillow_ is installed, large images will be automatically resized when\nsending photos to prevent Telegram from failing with \"invalid image\".\nOfficial clients also do this.\n\nIf aiohttp_ is installed, the library will be able to download\n:tl:`WebDocument` media files (otherwise you will get an error).\n\nIf hachoir_ is installed, it will be used to extract metadata from files\nwhen sending documents. Telegram uses this information to show the song's\nperformer, artist, title, duration, and for videos too (including size).\nOtherwise, they will default to empty values, and you can set the attributes\nmanually.\n\n.. note::\n\n    Some of the modules may require additional dependencies before being\n    installed through ``pip``. If you have an ``apt``-based system, consider\n    installing the most commonly missing dependencies (with the right ``pip``):\n\n    .. code-block:: sh\n\n        apt update\n        apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev\n        pip install -U --user setuptools\n        pip install -U --user telethon cryptg pillow\n\n    Thanks to `@bb010g`_ for writing down this nice list.\n\n\n.. _cryptg: https://github.com/cher-nov/cryptg\n.. _pyaes: https://github.com/ricmoo/pyaes\n.. _pillow: https://python-pillow.org\n.. _aiohttp: https://docs.aiohttp.org\n.. _hachoir: https://hachoir.readthedocs.io\n.. _@bb010g: https://static.bb010g.com\n"
  },
  {
    "path": "readthedocs/basic/next-steps.rst",
    "content": "==========\nNext Steps\n==========\n\nThese basic first steps should have gotten you started with the library.\n\nBy now, you should know how to call friendly methods and how to work with\nthe returned objects, how things work inside event handlers, etc.\n\nNext, we will see a quick reference summary of *all* the methods and\nproperties that you will need when using the library. If you follow\nthe links there, you will expand the documentation for the method\nand property, with more examples on how to use them.\n\nTherefore, **you can find an example on every method** of the client\nto learn how to use it, as well as a description of all the arguments.\n\nAfter that, we will go in-depth with some other important concepts\nthat are worth learning and understanding.\n\nFrom now on, you can keep pressing the \"Next\" button if you want,\nor use the menu on the left, since some pages are quite lengthy.\n\nA note on developing applications\n=================================\n\nIf you're using the library to make an actual application (and not just\nautomate things), you should make sure to `comply with the ToS`__:\n\n    […] when logging in as an existing user, apps are supposed to call\n    [:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of\n    Service; this call should be repeated after ``expires`` seconds have\n    elapsed. If an update to the Terms Of Service is available, clients are\n    supposed to show a consent popup; if accepted, clients should call\n    [:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON\n    object; in case of denial, clients are to delete the account using\n    [:tl:`DeleteAccount`], providing Decline ToS update as deletion reason.\n\n.. __: https://core.telegram.org/api/config#terms-of-service\n\nHowever, if you use the library to automate or enhance your Telegram\nexperience, it's very likely that you are using other applications doing this\ncheck for you (so you wouldn't run the risk of violating the ToS).\n\nThe library itself will not automatically perform this check or accept the ToS\nbecause it should require user action (the only exception is during sign-up).\n"
  },
  {
    "path": "readthedocs/basic/quick-start.rst",
    "content": "===========\nQuick-Start\n===========\n\nLet's see a longer example to learn some of the methods that the library\nhas to offer. These are known as \"friendly methods\", and you should always\nuse these if possible.\n\n.. code-block:: python\n\n    from telethon import TelegramClient\n\n    # Remember to use your own values from my.telegram.org!\n    api_id = 12345\n    api_hash = '0123456789abcdef0123456789abcdef'\n    client = TelegramClient('anon', api_id, api_hash)\n\n    async def main():\n        # Getting information about yourself\n        me = await client.get_me()\n\n        # \"me\" is a user object. You can pretty-print\n        # any Telegram object with the \"stringify\" method:\n        print(me.stringify())\n\n        # When you print something, you see a representation of it.\n        # You can access all attributes of Telegram objects with\n        # the dot operator. For example, to get the username:\n        username = me.username\n        print(username)\n        print(me.phone)\n\n        # You can print all the dialogs/conversations that you are part of:\n        async for dialog in client.iter_dialogs():\n            print(dialog.name, 'has ID', dialog.id)\n\n        # You can send messages to yourself...\n        await client.send_message('me', 'Hello, myself!')\n        # ...to some chat ID\n        await client.send_message(-100123456, 'Hello, group!')\n        # ...to your contacts\n        await client.send_message('+34600123123', 'Hello, friend!')\n        # ...or even to any username\n        await client.send_message('username', 'Testing Telethon!')\n\n        # You can, of course, use markdown in your messages:\n        message = await client.send_message(\n            'me',\n            'This message has **bold**, `code`, __italics__ and '\n            'a [nice website](https://example.com)!',\n            link_preview=False\n        )\n\n        # Sending a message returns the sent message object, which you can use\n        print(message.raw_text)\n\n        # You can reply to messages directly if you have a message object\n        await message.reply('Cool!')\n\n        # Or send files, songs, documents, albums...\n        await client.send_file('me', '/home/me/Pictures/holidays.jpg')\n\n        # You can print the message history of any chat:\n        async for message in client.iter_messages('me'):\n            print(message.id, message.text)\n\n            # You can download media from messages, too!\n            # The method will return the path where the file was saved.\n            if message.photo:\n                path = await message.download_media()\n                print('File saved to', path)  # printed after download is done\n\n    with client:\n        client.loop.run_until_complete(main())\n\n\nHere, we show how to sign in, get information about yourself, send\nmessages, files, getting chats, printing messages, and downloading\nfiles.\n\nYou should make sure that you understand what the code shown here\ndoes, take note on how methods are called and used and so on before\nproceeding. We will see all the available methods later on.\n\n.. important::\n\n    Note that Telethon is an asynchronous library, and as such, you should\n    get used to it and learn a bit of basic `asyncio`. This will help a lot.\n    As a quick start, this means you generally want to write all your code\n    inside some ``async def`` like so:\n\n    .. code-block:: python\n\n        client = ...\n\n        async def do_something(me):\n            ...\n\n        async def main():\n            # Most of your code should go here.\n            # You can of course make and use your own async def (do_something).\n            # They only need to be async if they need to await things.\n            me = await client.get_me()\n            await do_something(me)\n\n        with client:\n            client.loop.run_until_complete(main())\n\n    After you understand this, you may use the ``telethon.sync`` hack if you\n    want do so (see :ref:`compatibility-and-convenience`), but note you may\n    run into other issues (iPython, Anaconda, etc. have some issues with it).\n"
  },
  {
    "path": "readthedocs/basic/signing-in.rst",
    "content": ".. _signing-in:\n\n==========\nSigning In\n==========\n\nBefore working with Telegram's API, you need to get your own API ID and hash:\n\n1. `Login to your Telegram account <https://my.telegram.org/>`_ with the\n   phone number of the developer account to use.\n\n2. Click under API Development tools.\n\n3. A *Create new application* window will appear. Fill in your application\n   details. There is no need to enter any *URL*, and only the first two\n   fields (*App title* and *Short name*) can currently be changed later.\n\n4. Click on *Create application* at the end. Remember that your\n   **API hash is secret** and Telegram won't let you revoke it.\n   Don't post it anywhere!\n\n.. note::\n\n    This API ID and hash is the one used by *your application*, not your\n    phone number. You can use this API ID and hash with *any* phone number\n    or even for bot accounts.\n\n\nEditing the Code\n================\n\nThis is a little introduction for those new to Python programming in general.\n\nWe will write our code inside ``hello.py``, so you can use any text\neditor that you like. To run the code, use ``python3 hello.py`` from\nthe terminal.\n\n.. important::\n\n    Don't call your script ``telethon.py``! Python will try to import\n    the client from there and it will fail with an error such as\n    \"ImportError: cannot import name 'TelegramClient' ...\".\n\n\nSigning In\n==========\n\nWe can finally write some code to log into our account!\n\n.. code-block:: python\n\n    from telethon import TelegramClient\n\n    # Use your own values from my.telegram.org\n    api_id = 12345\n    api_hash = '0123456789abcdef0123456789abcdef'\n\n    # The first parameter is the .session file name (absolute paths allowed)\n    with TelegramClient('anon', api_id, api_hash) as client:\n        client.loop.run_until_complete(client.send_message('me', 'Hello, myself!'))\n\n\nIn the first line, we import the class name so we can create an instance\nof the client. Then, we define variables to store our API ID and hash\nconveniently.\n\nAt last, we create a new `TelegramClient <telethon.client.telegramclient.TelegramClient>`\ninstance and call it ``client``. We can now use the client variable\nfor anything that we want, such as sending a message to ourselves.\n\n.. note::\n\n    Since Telethon is an asynchronous library, you need to ``await``\n    coroutine functions to have them run (or otherwise, run the loop\n    until they are complete). In this tiny example, we don't bother\n    making an ``async def main()``.\n\n    See :ref:`mastering-asyncio` to find out more.\n\n\nUsing a ``with`` block is the preferred way to use the library. It will\nautomatically `start() <telethon.client.auth.AuthMethods.start>` the client,\nlogging or signing up if necessary.\n\nIf the ``.session`` file already existed, it will not login\nagain, so be aware of this if you move or rename the file!\n\n\nSigning In as a Bot Account\n===========================\n\nYou can also use Telethon for your bots (normal bot accounts, not users).\nYou will still need an API ID and hash, but the process is very similar:\n\n\n.. code-block:: python\n\n    from telethon.sync import TelegramClient\n\n    api_id = 12345\n    api_hash = '0123456789abcdef0123456789abcdef'\n    bot_token = '12345:0123456789abcdef0123456789abcdef'\n\n    # We have to manually call \"start\" if we want an explicit bot token\n    bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token)\n\n    # But then we can use the client instance as usual\n    with bot:\n        ...\n\n\nTo get a bot account, you need to talk\nwith `@BotFather <https://t.me/BotFather>`_.\n\n\nSigning In behind a Proxy\n=========================\n\nIf you need to use a proxy to access Telegram,\nyou will need to `install python-socks[asyncio]`__\nand then pass a proxy argument to the client:\n\n.. code-block:: python\n\n    TelegramClient('anon', api_id, api_hash, proxy=...)\n\nThe ``proxy=`` argument should be a dict with the following properties:\n\n.. code-block:: python\n\n    proxy = {\n        'proxy_type': 'socks5', # (mandatory) protocol to use (see above)\n        'addr': '1.1.1.1',      # (mandatory) proxy IP address\n        'port': 5555,           # (mandatory) proxy port number\n        'username': 'foo',      # (optional) username if the proxy requires auth\n        'password': 'bar',      # (optional) password if the proxy requires auth\n        'rdns': True            # (optional) whether to use remote or local resolve, default remote\n    }\n\nA warning will be emitted if the proxy is specified but ``python-socks`` is not installed.\n\n.. __: https://github.com/romis2012/python-socks#installation\n\n\nUsing MTProto Proxies\n=====================\n\nMTProto Proxies are Telegram's alternative to normal proxies,\nand work a bit differently. The following protocols are available:\n\n* ``ConnectionTcpMTProxyAbridged``\n* ``ConnectionTcpMTProxyIntermediate``\n* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred)\n\nFor now, you need to manually specify these special connection modes\nif you want to use a MTProto Proxy. Your code would look like this:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, connection\n    #   we need to change the connection ^^^^^^^^^^\n\n    client = TelegramClient(\n        'anon',\n        api_id,\n        api_hash,\n\n        # Use one of the available connection modes.\n        # Normally, this one works with most proxies.\n        connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,\n\n        # Then, pass the proxy details as a tuple:\n        #     (host name, port, proxy secret)\n        #\n        # If the proxy has no secret, the secret must be:\n        #     '00000000000000000000000000000000'\n        proxy=('mtproxy.example.com', 2002, 'secret')\n    )\n\nIn future updates, we may make it easier to use MTProto Proxies\n(such as avoiding the need to manually pass ``connection=``).\n\nIn short, the same code above but without comments to make it clearer:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, connection\n\n    client = TelegramClient(\n        'anon', api_id, api_hash,\n        connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,\n        proxy=('mtproxy.example.com', 2002, 'secret')\n    )\n"
  },
  {
    "path": "readthedocs/basic/updates.rst",
    "content": "=======\nUpdates\n=======\n\nUpdates are an important topic in a messaging platform like Telegram.\nAfter all, you want to be notified when a new message arrives, when\na member joins, when someone starts typing, etc.\nFor that, you can use **events**.\n\n.. important::\n\n    It is strongly advised to enable logging when working with events,\n    since exceptions in event handlers are hidden by default. Please\n    add the following snippet to the very top of your file:\n\n    .. code-block:: python\n\n        import logging\n        logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',\n                            level=logging.WARNING)\n\n\nGetting Started\n===============\n\nLet's start things with an example to automate replies:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    client = TelegramClient('anon', api_id, api_hash)\n\n    @client.on(events.NewMessage)\n    async def my_event_handler(event):\n        if 'hello' in event.raw_text:\n            await event.reply('hi!')\n\n    client.start()\n    client.run_until_disconnected()\n\n\nThis code isn't much, but there might be some things unclear.\nLet's break it down:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    client = TelegramClient('anon', api_id, api_hash)\n\n\nThis is normal creation (of course, pass session name, API ID and hash).\nNothing we don't know already.\n\n.. code-block:: python\n\n    @client.on(events.NewMessage)\n\n\nThis Python decorator will attach itself to the ``my_event_handler``\ndefinition, and basically means that *on* a `NewMessage\n<telethon.events.newmessage.NewMessage>` *event*,\nthe callback function you're about to define will be called:\n\n.. code-block:: python\n\n    async def my_event_handler(event):\n        if 'hello' in event.raw_text:\n            await event.reply('hi!')\n\n\nIf a `NewMessage\n<telethon.events.newmessage.NewMessage>` event occurs,\nand ``'hello'`` is in the text of the message, we `reply()\n<telethon.tl.custom.message.Message.reply>` to the event\nwith a ``'hi!'`` message.\n\n.. note::\n\n    Event handlers **must** be ``async def``. After all,\n    Telethon is an asynchronous library based on `asyncio`,\n    which is a safer and often faster approach to threads.\n\n    You **must** ``await`` all method calls that use\n    network requests, which is most of them.\n\n\nMore Examples\n=============\n\nReplying to messages with hello is fun, but, can we do more?\n\n.. code-block:: python\n\n    @client.on(events.NewMessage(outgoing=True, pattern=r'\\.save'))\n    async def handler(event):\n        if event.is_reply:\n            replied = await event.get_reply_message()\n            sender = replied.sender\n            await client.download_profile_photo(sender)\n            await event.respond('Saved your photo {}'.format(sender.username))\n\nWe could also get replies. This event filters outgoing messages\n(only those that we send will trigger the method), then we filter\nby the regex ``r'\\.save'``, which will match messages starting\nwith ``\".save\"``.\n\nInside the method, we check whether the event is replying to another message\nor not. If it is, we get the reply message and the sender of that message,\nand download their profile photo.\n\nLet's delete messages which contain \"heck\". We don't allow swearing here.\n\n.. code-block:: python\n\n    @client.on(events.NewMessage(pattern=r'(?i).*heck'))\n    async def handler(event):\n        await event.delete()\n\n\nWith the ``r'(?i).*heck'`` regex, we match case-insensitive\n\"heck\" anywhere in the message. Regex is very powerful and you\ncan learn more at https://regexone.com/.\n\nSo far, we have only seen the `NewMessage\n<telethon.events.newmessage.NewMessage>`, but there are many more\nwhich will be covered later. This is only a small introduction to updates.\n\nEntities\n========\n\nWhen you need the user or chat where an event occurred, you **must** use\nthe following methods:\n\n.. code-block:: python\n\n    async def handler(event):\n        # Good\n        chat = await event.get_chat()\n        sender = await event.get_sender()\n        chat_id = event.chat_id\n        sender_id = event.sender_id\n\n        # BAD. Don't do this\n        chat = event.chat\n        sender = event.sender\n        chat_id = event.chat.id\n        sender_id = event.sender.id\n\nEvents are like messages, but don't have all the information a message has!\nWhen you manually get a message, it will have all the information it needs.\nWhen you receive an update about a message, it **won't** have all the\ninformation, so you have to **use the methods**, not the properties.\n\nMake sure you understand the code seen here before continuing!\nAs a rule of thumb, remember that new message events behave just\nlike message objects, so you can do with them everything you can\ndo with a message object.\n"
  },
  {
    "path": "readthedocs/concepts/asyncio.rst",
    "content": ".. _mastering-asyncio:\n\n=================\nMastering asyncio\n=================\n\n.. contents::\n\n\nWhat's asyncio?\n===============\n\n`asyncio` is a Python 3's built-in library. This means it's already installed if\nyou have Python 3. Since Python 3.5, it is convenient to work with asynchronous\ncode. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do.\n\n`asyncio` stands for *Asynchronous Input Output*. This is a very powerful\nconcept to use whenever you work IO. Interacting with the web or external\nAPIs such as Telegram's makes a lot of sense this way.\n\n\nWhy asyncio?\n============\n\nAsynchronous IO makes a lot of sense in a library like Telethon.\nYou send a request to the server (such as \"get some message\"), and\nthanks to `asyncio`, your code won't block while a response arrives.\n\nThe alternative would be to spawn a thread for each update so that\nother code can run while the response arrives. That is *a lot* more\nexpensive.\n\nThe code will also run faster, because instead of switching back and\nforth between the OS and your script, your script can handle it all.\nAvoiding switching saves quite a bit of time, in Python or any other\nlanguage that supports asynchronous IO. It will also be cheaper,\nbecause tasks are smaller than threads, which are smaller than processes.\n\n\nWhat are asyncio basics?\n========================\n\nThe code samples below assume that you have Python 3.7 or greater installed.\n\n.. code-block:: python\n\n    # First we need the asyncio library\n    import asyncio\n\n    # We also need something to run\n    async def main():\n        for char in 'Hello, world!\\n':\n            print(char, end='', flush=True)\n            await asyncio.sleep(0.2)\n\n    # Then, we can create a new asyncio loop and use it to run our coroutine.\n    # The creation and tear-down of the loop is hidden away from us.\n    asyncio.run(main())\n\n\nWhat does telethon.sync do?\n===========================\n\nThe moment you import any of these:\n\n.. code-block:: python\n\n    from telethon import sync, ...\n    # or\n    from telethon.sync import ...\n    # or\n    import telethon.sync\n\nThe ``sync`` module rewrites most ``async def``\nmethods in Telethon to something similar to this:\n\n.. code-block:: python\n\n    def new_method():\n        result = original_method()\n        if loop.is_running():\n            # the loop is already running, return the await-able to the user\n            return result\n        else:\n            # the loop is not running yet, so we can run it for the user\n            return loop.run_until_complete(result)\n\n\nThat means you can do this:\n\n.. code-block:: python\n\n    print(client.get_me().username)\n\nInstead of this:\n\n.. code-block:: python\n\n    me = client.loop.run_until_complete(client.get_me())\n    print(me.username)\n\n    # or, using asyncio's default loop (it's the same)\n    import asyncio\n    loop = asyncio.get_running_loop()  # == client.loop\n    me = loop.run_until_complete(client.get_me())\n    print(me.username)\n\n\nAs you can see, it's a lot of boilerplate and noise having to type\n``run_until_complete`` all the time, so you can let the magic module\nto rewrite it for you. But notice the comment above: it won't run\nthe loop if it's already running, because it can't. That means this:\n\n.. code-block:: python\n\n    async def main():\n        # 3. the loop is running here\n        print(\n            client.get_me()  # 4. this will return a coroutine!\n            .username  # 5. this fails, coroutines don't have usernames\n        )\n\n    loop.run_until_complete(  # 2. run the loop and the ``main()`` coroutine\n        main()  # 1. calling ``async def`` \"returns\" a coroutine\n    )\n\n\nWill fail. So if you're inside an ``async def``, then the loop is\nrunning, and if the loop is running, you must ``await`` things yourself:\n\n.. code-block:: python\n\n    async def main():\n        print((await client.get_me()).username)\n\n    loop.run_until_complete(main())\n\n\nWhat are async, await and coroutines?\n=====================================\n\nThe ``async`` keyword lets you define asynchronous functions,\nalso known as coroutines, and also iterate over asynchronous\nloops or use ``async with``:\n\n.. code-block:: python\n\n    import asyncio\n\n    async def main():\n        # ^ this declares the main() coroutine function\n\n        async with client:\n            # ^ this is an asynchronous with block\n\n            async for message in client.iter_messages(chat):\n                # ^ this is a for loop over an asynchronous generator\n\n                print(message.sender.username)\n\n    asyncio.run(main())\n    # ^ this will create a new asyncio loop behind the scenes and tear it down\n    #   once the function returns. It will run the loop untiil main finishes.\n    #   You should only use this function if there is no other loop running.\n\n\nThe ``await`` keyword blocks the *current* task, and the loop can run\nother tasks. Tasks can be thought of as \"threads\", since many can run\nconcurrently:\n\n.. code-block:: python\n\n    import asyncio\n\n    async def hello(delay):\n        await asyncio.sleep(delay)  # await tells the loop this task is \"busy\"\n        print('hello')  # eventually the loop resumes the code here\n\n    async def world(delay):\n        # the loop decides this method should run first\n        await asyncio.sleep(delay)  # await tells the loop this task is \"busy\"\n        print('world')  # eventually the loop finishes all tasks\n\n    async def main():\n        asyncio.create_task(world(2))  # create the world task, passing 2 as delay\n        asyncio.create_task(hello(delay=1))  # another task, but with delay 1\n        await asyncio.sleep(3)  # wait for three seconds before exiting\n\n    try:\n        # create a new temporary asyncio loop and use it to run main\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        pass\n\nThe same example, but without the comment noise:\n\n.. code-block:: python\n\n    import asyncio\n\n    async def hello(delay):\n        await asyncio.sleep(delay)\n        print('hello')\n\n    async def world(delay):\n        await asyncio.sleep(delay)\n        print('world')\n\n    async def main():\n        asyncio.create_task(world(2))\n        asyncio.create_task(hello(delay=1))\n        await asyncio.sleep(3)\n\n    try:\n        asyncio.run(main())\n    except KeyboardInterrupt:\n        pass\n\n\nCan I use threads?\n==================\n\nYes, you can, but you must understand that the loops themselves are\nnot thread safe. and you must be sure to know what is happening. The\neasiest and cleanest option is to use `asyncio.run` to create and manage\nthe new event loop for you:\n\n.. code-block:: python\n\n    import asyncio\n    import threading\n\n    async def actual_work():\n        client = TelegramClient(..., loop=loop)\n        ...  # can use `await` here\n\n    def go():\n        asyncio.run(actual_work())\n\n    threading.Thread(target=go).start()\n\n\nGenerally, **you don't need threads** unless you know what you're doing.\nJust create another task, as shown above. If you're using the Telethon\nwith a library that uses threads, you must be careful to use `threading.Lock`\nwhenever you use the client, or enable the compatible mode. For that, see\n:ref:`compatibility-and-convenience`.\n\nYou may have seen this error:\n\n.. code-block:: text\n\n    RuntimeError: There is no current event loop in thread 'Thread-1'.\n\nIt just means you didn't create a loop for that thread. Please refer to\nthe ``asyncio`` documentation to correctly learn how to set the event loop\nfor non-main threads.\n\n\nclient.run_until_disconnected() blocks!\n=======================================\n\nAll of what `client.run_until_disconnected()\n<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is\nrun the `asyncio`'s event loop until the client is disconnected. That means\n*the loop is running*. And if the loop is running, it will run all the tasks\nin it. So if you want to run *other* code, create tasks for it:\n\n.. code-block:: python\n\n    from datetime import datetime\n\n    async def clock():\n        while True:\n            print('The time:', datetime.now())\n            await asyncio.sleep(1)\n\n    loop.create_task(clock())\n    ...\n    client.run_until_disconnected()\n\nThis creates a task for a clock that prints the time every second.\nYou don't need to use `client.run_until_disconnected()\n<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!\nYou just need to make the loop is running, somehow. `loop.run_forever()\n<asyncio.loop.run_forever()>` and `loop.run_until_complete()\n<asyncio.loop.run_until_complete>` can also be used to run\nthe loop, and Telethon will be happy with any approach.\n\nOf course, there are better tools to run code hourly or daily, see below.\n\n\nWhat else can asyncio do?\n=========================\n\nAsynchronous IO is a really powerful tool, as we've seen. There are plenty\nof other useful libraries that also use `asyncio` and that you can integrate\nwith Telethon.\n\n* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous\n  `requests <https://github.com/requests/requests/>`_ but asynchronous.\n* `quart <https://gitlab.com/pgjones/quart>`_ is an asynchronous alternative\n  to `Flask <http://flask.pocoo.org/>`_.\n* `aiocron <https://github.com/gawel/aiocron>`_ lets you schedule things\n  to run things at a desired time, or run some tasks hourly, daily, etc.\n\nAnd of course, `asyncio <https://docs.python.org/3/library/asyncio.html>`_\nitself! It has a lot of methods that let you do nice things. For example,\nyou can run requests in parallel:\n\n.. code-block:: python\n\n    async def main():\n        last, sent, download_path = await asyncio.gather(\n            client.get_messages('telegram', 10),\n            client.send_message('me', 'Using asyncio!'),\n            client.download_profile_photo('telegram')\n        )\n\n    loop.run_until_complete(main())\n\n\nThis code will get the 10 last messages from `@telegram\n<https://t.me/telegram>`_, send one to the chat with yourself, and also\ndownload the profile photo of the channel. `asyncio` will run all these\nthree tasks at the same time. You can run all the tasks you want this way.\n\nA different way would be:\n\n.. code-block:: python\n\n    loop.create_task(client.get_messages('telegram', 10))\n    loop.create_task(client.send_message('me', 'Using asyncio!'))\n    loop.create_task(client.download_profile_photo('telegram'))\n\nThey will run in the background as long as the loop is running too.\n\nYou can also `start an asyncio server\n<https://docs.python.org/3/library/asyncio-stream.html#asyncio.start_server>`_\nin the main script, and from another script, `connect to it\n<https://docs.python.org/3/library/asyncio-stream.html#asyncio.open_connection>`_\nto achieve `Inter-Process Communication\n<https://en.wikipedia.org/wiki/Inter-process_communication>`_.\nYou can get as creative as you want. You can program anything you want.\nWhen you use a library, you're not limited to use only its methods. You can\ncombine all the libraries you want. People seem to forget this simple fact!\n\n\nWhy does client.start() work outside async?\n===========================================\n\nBecause it's so common that it's really convenient to offer said\nfunctionality by default. This means you can set up all your event\nhandlers and start the client without worrying about loops at all.\n\nUsing the client in a ``with`` block, `start\n<telethon.client.auth.AuthMethods.start>`, `run_until_disconnected\n<telethon.client.updates.UpdateMethods.run_until_disconnected>`, and\n`disconnect <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`\nall support this.\n\nWhere can I read more?\n======================\n\n`Check out my blog post\n<https://lonami.dev/blog/asyncio/>`_ about `asyncio`, which\nhas some more examples and pictures to help you understand what happens\nwhen the loop runs.\n"
  },
  {
    "path": "readthedocs/concepts/botapi-vs-mtproto.rst",
    "content": ".. _botapi:\n\n=======================\nHTTP Bot API vs MTProto\n=======================\n\n\nTelethon is more than just another viable alternative when developing bots\nfor Telegram. If you haven't decided which wrapper library for bots to use\nyet, using Telethon from the beginning may save you some headaches later.\n\n.. contents::\n\n\nWhat is Bot API?\n================\n\nThe `Telegram Bot API`_, also known as HTTP Bot API and from now on referred\nto as simply \"Bot API\" is Telegram's official way for developers to control\ntheir own Telegram bots. Quoting their main page:\n\n    The Bot API is an HTTP-based interface created for developers keen on\n    building bots for Telegram.\n\n    To learn how to create and set up a bot, please consult our\n    `Introduction to Bots`_ and `Bot FAQ`_.\n\nBot API is simply an HTTP endpoint which translates your requests to it into\nMTProto calls through tdlib_, their bot backend.\n\nConfiguration of your bot, such as its available commands and auto-completion,\nis configured through `@BotFather <https://t.me/BotFather>`_.\n\n\nWhat is MTProto?\n================\n\nMTProto_ is Telegram's own protocol to communicate with their API when you\nconnect to their servers.\n\nTelethon is an alternative MTProto-based backend written entirely in Python\nand much easier to setup and use.\n\nBoth official applications and third-party clients (like your own\napplications) logged in as either user or bots **can use MTProto** to\ncommunicate directly with Telegram's API (which is not the HTTP bot API).\n\nWhen we talk about MTProto, we often mean \"MTProto-based clients\".\n\n\nAdvantages of MTProto over Bot API\n==================================\n\nMTProto clients (like Telethon) connect directly to Telegram's servers,\nwhich means there is no HTTP connection, no \"polling\" or \"web hooks\". This\nmeans **less overhead**, since the protocol used between you and the server\nis much more compact than HTTP requests with responses in wasteful JSON.\n\nSince there is a direct connection to Telegram's servers, even if their\nBot API endpoint is down, you can still have connection to Telegram directly.\n\nUsing a MTProto client, you are also not limited to the public API that\nthey expose, and instead, **you have full control** of what your bot can do.\nTelethon offers you all the power with often **much easier usage** than any\nof the available Python Bot API wrappers.\n\nIf your application ever needs user features because bots cannot do certain\nthings, you will be able to easily login as a user and even keep your bot\nwithout having to learn a new library.\n\nIf less overhead and full control didn't convince you to use Telethon yet,\ncheck out the wiki page `MTProto vs HTTP Bot API`_ with a more exhaustive\nand up-to-date list of differences.\n\n\nMigrating from Bot API to Telethon\n==================================\n\nIt doesn't matter if you wrote your bot with requests_ and you were\nmaking API requests manually, or if you used a wrapper library like\npython-telegram-bot_ or pyTelegramBotAPI_. It's never too late to\nmigrate to Telethon!\n\nIf you were using an asynchronous library like aiohttp_ or a wrapper like\naiogram_ or dumbot_, it will be even easier, because Telethon is also an\nasynchronous library.\n\nNext, we will see some examples from the most popular libraries.\n\n\nMigrating from python-telegram-bot\n----------------------------------\n\nLet's take their `echobot.py`_ example and shorten it a bit:\n\n.. code-block:: python\n\n    from telegram.ext import Updater, CommandHandler, MessageHandler, Filters\n\n    def start(update, context):\n        \"\"\"Send a message when the command /start is issued.\"\"\"\n        update.message.reply_text('Hi!')\n\n    def echo(update, context):\n        \"\"\"Echo the user message.\"\"\"\n        update.message.reply_text(update.message.text)\n\n    def main():\n        \"\"\"Start the bot.\"\"\"\n        updater = Updater(\"TOKEN\")\n        dp = updater.dispatcher\n        dp.add_handler(CommandHandler(\"start\", start))\n        dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))\n\n        updater.start_polling()\n\n        updater.idle()\n\n    if __name__ == '__main__':\n        main()\n\n\nAfter using Telethon:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')\n\n    @bot.on(events.NewMessage(pattern='/start'))\n    async def start(event):\n        \"\"\"Send a message when the command /start is issued.\"\"\"\n        await event.respond('Hi!')\n        raise events.StopPropagation\n\n    @bot.on(events.NewMessage)\n    async def echo(event):\n        \"\"\"Echo the user message.\"\"\"\n        await event.respond(event.text)\n\n    def main():\n        \"\"\"Start the bot.\"\"\"\n        bot.run_until_disconnected()\n\n    if __name__ == '__main__':\n        main()\n\nKey differences:\n\n* The recommended way to do it imports fewer things.\n* All handlers trigger by default, so we need ``events.StopPropagation``.\n* Adding handlers, responding and running is a lot less verbose.\n* Telethon needs ``async def`` and ``await``.\n* The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``.\n\n\nMigrating from pyTelegramBotAPI\n-------------------------------\n\nLet's show another echobot from their README:\n\n.. code-block:: python\n\n    import telebot\n\n    bot = telebot.TeleBot(\"TOKEN\")\n\n    @bot.message_handler(commands=['start'])\n    def send_welcome(message):\n        bot.reply_to(message, \"Howdy, how are you doing?\")\n\n    @bot.message_handler(func=lambda m: True)\n    def echo_all(message):\n        bot.reply_to(message, message.text)\n\n    bot.polling()\n\nNow we rewrite it to use Telethon:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')\n\n    @bot.on(events.NewMessage(pattern='/start'))\n    async def send_welcome(event):\n        await event.reply('Howdy, how are you doing?')\n\n    @bot.on(events.NewMessage)\n    async def echo_all(event):\n        await event.reply(event.text)\n\n    bot.run_until_disconnected()\n\nKey differences:\n\n* Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``.\n  Note that the ``event`` behaves just like their ``message``.\n* Telethon also supports ``func=lambda m: True``, but it's not necessary.\n\n\nMigrating from aiogram\n----------------------\n\nFrom their GitHub:\n\n.. code-block:: python\n\n    from aiogram import Bot, Dispatcher, executor, types\n\n    API_TOKEN = 'BOT TOKEN HERE'\n\n    # Initialize bot and dispatcher\n    bot = Bot(token=API_TOKEN)\n    dp = Dispatcher(bot)\n\n    @dp.message_handler(commands=['start'])\n    async def send_welcome(message: types.Message):\n        \"\"\"\n        This handler will be called when client send `/start` command.\n        \"\"\"\n        await message.reply(\"Hi!\\nI'm EchoBot!\\nPowered by aiogram.\")\n\n    @dp.message_handler(regexp='(^cat[s]?$|puss)')\n    async def cats(message: types.Message):\n        with open('data/cats.jpg', 'rb') as photo:\n            await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺',\n                                 reply_to_message_id=message.message_id)\n\n    @dp.message_handler()\n    async def echo(message: types.Message):\n        await bot.send_message(message.chat.id, message.text)\n\n    if __name__ == '__main__':\n        executor.start_polling(dp, skip_updates=True)\n\n\nAfter rewrite:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    # Initialize bot and... just the bot!\n    bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')\n\n    @bot.on(events.NewMessage(pattern='/start'))\n    async def send_welcome(event):\n        await event.reply('Howdy, how are you doing?')\n\n    @bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)'))\n    async def cats(event):\n        await event.reply('Cats is here 😺', file='data/cats.jpg')\n\n    @bot.on(events.NewMessage)\n    async def echo_all(event):\n        await event.reply(event.text)\n\n    if __name__ == '__main__':\n        bot.run_until_disconnected()\n\n\nKey differences:\n\n* Telethon offers convenience methods to avoid retyping\n  ``bot.send_photo(message.chat.id, ...)`` all the time,\n  and instead let you type ``event.reply``.\n* Sending files is **a lot** easier. The methods for sending\n  photos, documents, audios, etc. are all the same!\n\nMigrating from dumbot\n---------------------\n\nShowcasing their subclassing example:\n\n.. code-block:: python\n\n    from dumbot import Bot\n\n    class Subbot(Bot):\n        async def init(self):\n            self.me = await self.getMe()\n\n        async def on_update(self, update):\n            await self.sendMessage(\n                chat_id=update.message.chat.id,\n                text='i am {}'.format(self.me.username)\n            )\n\n    Subbot(token).run()\n\nAfter rewriting:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    class Subbot(TelegramClient):\n        def __init__(self, *a, **kw):\n            super().__init__(*a, **kw)\n            self.add_event_handler(self.on_update, events.NewMessage)\n\n        async def connect():\n            await super().connect()\n            self.me = await self.get_me()\n\n        async def on_update(event):\n            await event.reply('i am {}'.format(self.me.username))\n\n    bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')\n    bot.run_until_disconnected()\n\n\nKey differences:\n\n* Telethon method names are ``snake_case``.\n* dumbot does not offer friendly methods like ``update.reply``.\n* Telethon does not have an implicit ``on_update`` handler, so\n  we need to manually register one.\n\n\n.. _Telegram Bot API: https://core.telegram.org/bots/api\n.. _Introduction to Bots: https://core.telegram.org/bots\n.. _Bot FAQ: https://core.telegram.org/bots/faq\n.. _tdlib: https://core.telegram.org/tdlib\n.. _MTProto: https://core.telegram.org/mtproto\n.. _MTProto vs HTTP Bot API: https://github.com/LonamiWebs/Telethon/wiki/MTProto-vs-HTTP-Bot-API\n.. _requests: https://pypi.org/project/requests/\n.. _python-telegram-bot: https://python-telegram-bot.readthedocs.io\n.. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI\n.. _aiohttp: https://docs.aiohttp.org/en/stable\n.. _aiogram: https://aiogram.readthedocs.io\n.. _dumbot: https://github.com/Lonami/dumbot\n.. _echobot.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py\n"
  },
  {
    "path": "readthedocs/concepts/chats-vs-channels.rst",
    "content": ".. _chats-channels:\n\n=================\nChats vs Channels\n=================\n\nTelegram's raw API can get very confusing sometimes, in particular when it\ncomes to talking about \"chats\", \"channels\", \"groups\", \"megagroups\", and all\nthose concepts.\n\nThis section will try to explain what each of these concepts are.\n\n\nChats\n=====\n\nA ``Chat`` can be used to talk about either the common \"subclass\" that both\nchats and channels share, or the concrete :tl:`Chat` type.\n\nTechnically, both :tl:`Chat` and :tl:`Channel` are a form of the `Chat type`_.\n\n**Most of the time**, the term :tl:`Chat` is used to talk about *small group\nchats*. When you create a group through an official application, this is the\ntype that you get. Official applications refer to these as \"Group\".\n\nBoth the bot API and Telethon will add a minus sign (negate) the real chat ID\nso that you can tell at a glance, with just a number, the entity type.\n\nFor example, if you create a chat with :tl:`CreateChatRequest`, the real chat\nID might be something like `123`. If you try printing it from a\n`message.chat_id` you will see `-123`. This ID helps Telethon know you're\ntalking about a :tl:`Chat`.\n\n\nChannels\n========\n\nOfficial applications create a *broadcast* channel when you create a new\nchannel (used to broadcast messages, only administrators can post messages).\n\nOfficial applications implicitly *migrate* an *existing* :tl:`Chat` to a\n*megagroup* :tl:`Channel` when you perform certain actions (exceed user limit,\nadd a public username, set certain permissions, etc.).\n\nA ``Channel`` can be created directly with :tl:`CreateChannelRequest`, as\neither a ``megagroup`` or ``broadcast``.\n\nOfficial applications use the term \"channel\" **only** for broadcast channels.\n\nThe API refers to the different types of :tl:`Channel` with certain attributes:\n\n* A **broadcast channel** is a :tl:`Channel` with the ``channel.broadcast``\n  attribute set to `True`.\n\n* A **megagroup channel** is a :tl:`Channel` with the ``channel.megagroup``\n  attribute set to `True`. Official applications refer to this as \"supergroup\".\n\n* A **gigagroup channel** is a :tl:`Channel` with the ``channel.gigagroup``\n  attribute set to `True`. Official applications refer to this as \"broadcast\n  groups\", and is used when a megagroup becomes very large and administrators\n  want to transform it into something where only they can post messages.\n\n\nBoth the bot API and Telethon will \"concatenate\" ``-100`` to the real chat ID\nso that you can tell at a glance, with just a number, the entity type.\n\nFor example, if you create a new broadcast channel, the real channel ID might\nbe something like `456`. If you try printing it from a `message.chat_id` you\nwill see `-1000000000456`. This ID helps Telethon know you're talking about a\n:tl:`Channel`.\n\n\nConverting IDs\n==============\n\nYou can convert between the \"marked\" identifiers (prefixed with a minus sign)\nand the real ones with ``utils.resolve_id``. It will return a tuple with the\nreal ID, and the peer type (the class):\n\n.. code-block:: python\n\n    from telethon import utils\n    real_id, peer_type = utils.resolve_id(-1000000000456)\n\n    print(real_id)  # 456\n    print(peer_type)  # <class 'telethon.tl.types.PeerChannel'>\n\n    peer = peer_type(real_id)\n    print(peer)  # PeerChannel(channel_id=456)\n\n\nThe reverse operation can be done with ``utils.get_peer_id``:\n\n.. code-block:: python\n\n    print(utils.get_peer_id(types.PeerChannel(456)))  # -1000000000456\n\n\nNote that this function can also work with other types, like :tl:`Chat` or\n:tl:`Channel` instances.\n\nIf you need to convert other types like usernames which might need to perform\nAPI calls to find out the identifier, you can use ``client.get_peer_id``:\n\n\n.. code-block:: python\n\n    print(await client.get_peer_id('me'))  # your id\n\n\nIf there is no \"mark\" (no minus sign), Telethon will assume your identifier\nrefers to a :tl:`User`. If this is **not** the case, you can manually fix it:\n\n\n.. code-block:: python\n\n    from telethon import types\n    await client.send_message(types.PeerChannel(456), 'hello')\n    #                         ^^^^^^^^^^^^^^^^^ explicit peer type\n\n\nA note on raw API\n=================\n\nCertain methods only work on a :tl:`Chat`, and some others only work on a\n:tl:`Channel` (and these may only work in broadcast, or megagroup). Your code\nlikely knows what it's working with, so it shouldn't be too much of an issue.\n\nIf you need to find the :tl:`Channel` from a :tl:`Chat` that migrated to it,\naccess the `migrated_to` property:\n\n.. code-block:: python\n\n    # chat is a Chat\n    channel = await client.get_entity(chat.migrated_to)\n    # channel is now a Channel\n\nChannels do not have a \"migrated_from\", but a :tl:`ChannelFull` does. You can\nuse :tl:`GetFullChannelRequest` to obtain this:\n\n.. code-block:: python\n\n    from telethon import functions\n    full = await client(functions.channels.GetFullChannelRequest(your_channel))\n    full_channel = full.full_chat\n    # full_channel is a ChannelFull\n    print(full_channel.migrated_from_chat_id)\n\nThis way, you can also access the linked discussion megagroup of a broadcast channel:\n\n.. code-block:: python\n\n    print(full_channel.linked_chat_id)  # prints ID of linked discussion group or None\n\nYou do not need to use ``client.get_entity`` to access the\n``migrated_from_chat_id`` :tl:`Chat` or the ``linked_chat_id`` :tl:`Channel`.\nThey are in the ``full.chats`` attribute:\n\n.. code-block:: python\n\n    if full_channel.migrated_from_chat_id:\n        migrated_from_chat = next(c for c in full.chats if c.id == full_channel.migrated_from_chat_id)\n        print(migrated_from_chat.title)\n\n    if full_channel.linked_chat_id:\n        linked_group = next(c for c in full.chats if c.id == full_channel.linked_chat_id)\n        print(linked_group.username)\n\n.. _Chat type: https://tl.telethon.dev/types/chat.html\n"
  },
  {
    "path": "readthedocs/concepts/entities.rst",
    "content": ".. _entities:\n\n========\nEntities\n========\n\nThe library widely uses the concept of \"entities\". An entity will refer\nto any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return\nin response to certain methods, such as :tl:`GetUsersRequest`.\n\n.. note::\n\n    When something \"entity-like\" is required, it means that you need to\n    provide something that can be turned into an entity. These things include,\n    but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,\n    or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even\n    phone numbers **from people you have in your contact list**.\n\n    To \"encounter\" an ID, you would have to \"find it\" like you would in the\n    normal app. If the peer is in your dialogs, you would need to\n    `client.get_dialogs() <telethon.client.dialogs.DialogMethods.get_dialogs>`.\n    If the peer is someone in a group, you would similarly\n    `client.get_participants(group) <telethon.client.chats.ChatMethods.get_participants>`.\n\n    Once you have encountered an ID, the library will (by default) have saved\n    their ``access_hash`` for you, which is needed to invoke most methods.\n    This is why sometimes you might encounter this error when working with\n    the library. You should ``except ValueError`` and run code that you know\n    should work to find the entity.\n\n\n.. contents::\n\n\nWhat is an Entity?\n==================\n\nA lot of methods and requests require *entities* to work. For example,\nyou send a message to an *entity*, get the username of an *entity*, and\nso on.\n\nThere are a lot of things that work as entities: usernames, phone numbers,\nchat links, invite links, IDs, and the types themselves. That is, you can\nuse any of those when you see an \"entity\" is needed.\n\n.. note::\n\n    Remember that the phone number must be in your contact list before you\n    can use it.\n\nYou should use, **from better to worse**:\n\n1. Input entities. For example, `event.input_chat\n   <telethon.tl.custom.chatgetter.ChatGetter.input_chat>`,\n   `message.input_sender\n   <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`,\n   or caching an entity you will use a lot with\n   ``entity = await client.get_input_entity(...)``.\n\n2. Entities. For example, if you had to get someone's\n   username, you can just use ``user`` or ``channel``.\n   It will work. Only use this option if you already have the entity!\n\n3. IDs. This will always look the entity up from the\n   cache (the ``*.session`` file caches seen entities).\n\n4. Usernames, phone numbers and links. The cache will be\n   used too (unless you force a `client.get_entity()\n   <telethon.client.users.UserMethods.get_entity>`),\n   but may make a request if the username, phone or link\n   has not been found yet.\n\nIn recent versions of the library, the following two are equivalent:\n\n.. code-block:: python\n\n    async def handler(event):\n        await client.send_message(event.sender_id, 'Hi')\n        await client.send_message(event.input_sender, 'Hi')\n\n\nIf you need to be 99% sure that the code will work (sometimes it's\nsimply impossible for the library to find the input entity), or if\nyou will reuse the chat a lot, consider using the following instead:\n\n.. code-block:: python\n\n    async def handler(event):\n        # This method may make a network request to find the input sender.\n        # Properties can't make network requests, so we need a method.\n        sender = await event.get_input_sender()\n        await client.send_message(sender, 'Hi')\n        await client.send_message(sender, 'Hi')\n\n\nGetting Entities\n================\n\nThrough the use of the :ref:`sessions`, the library will automatically\nremember the ID and hash pair, along with some extra information, so\nyou're able to just do this:\n\n.. code-block:: python\n\n    # (These examples assume you are inside an \"async def\")\n    #\n    # Dialogs are the \"conversations you have open\".\n    # This method returns a list of Dialog, which\n    # has the .entity attribute and other information.\n    #\n    # This part is IMPORTANT, because it fills the entity cache.\n    dialogs = await client.get_dialogs()\n\n    # All of these work and do the same.\n    username = await client.get_entity('username')\n    username = await client.get_entity('t.me/username')\n    username = await client.get_entity('https://telegram.dog/username')\n\n    # Other kind of entities.\n    channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')\n    contact = await client.get_entity('+34xxxxxxxxx')\n    friend  = await client.get_entity(friend_id)\n\n    # Getting entities through their ID (User, Chat or Channel)\n    entity = await client.get_entity(some_id)\n\n    # You can be more explicit about the type for said ID by wrapping\n    # it inside a Peer instance. This is recommended but not necessary.\n    from telethon.tl.types import PeerUser, PeerChat, PeerChannel\n\n    my_user    = await client.get_entity(PeerUser(some_id))\n    my_chat    = await client.get_entity(PeerChat(some_id))\n    my_channel = await client.get_entity(PeerChannel(some_id))\n\n\n.. note::\n\n    You **don't** need to get the entity before using it! Just let the\n    library do its job. Use a phone from your contacts, username, ID or\n    input entity (preferred but not necessary), whatever you already have.\n\nAll methods in the :ref:`telethon-client` call `.get_input_entity()\n<telethon.client.users.UserMethods.get_input_entity>` prior\nto sending the request to save you from the hassle of doing so manually.\nThat way, convenience calls such as `client.send_message('username', 'hi!')\n<telethon.client.messages.MessageMethods.send_message>`\nbecome possible.\n\nEvery entity the library encounters (in any response to any call) will by\ndefault be cached in the ``.session`` file (an SQLite database), to avoid\nperforming unnecessary API calls. If the entity cannot be found, additonal\ncalls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be\nmade to obtain the required information.\n\n\nEntities vs. Input Entities\n===========================\n\n.. note::\n\n    This section is informative, but worth reading. The library\n    will transparently handle all of these details for you.\n\nOn top of the normal types, the API also make use of what they call their\n``Input*`` versions of objects. The input version of an entity (e.g.\n:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum\ninformation that's required from Telegram to be able to identify\nwho you're referring to: a :tl:`Peer`'s **ID** and **hash**. They\nare named like this because they are input parameters in the requests.\n\nEntities' ID are the same for all user and bot accounts, however, the access\nhash is **different for each account**, so trying to reuse the access hash\nfrom one account in another will **not** work.\n\nSometimes, Telegram only needs to indicate the type of the entity along\nwith their ID. For this purpose, :tl:`Peer` versions of the entities also\nexist, which just have the ID. You cannot get the hash out of them since\nyou should not be needing it. The library probably has cached it before.\n\nPeers are enough to identify an entity, but they are not enough to make\na request with them. You need to know their hash before you can\n\"use them\", and to know the hash you need to \"encounter\" them, let it\nbe in your dialogs, participants, message forwards, etc.\n\n.. note::\n\n    You *can* use peers with the library. Behind the scenes, they are\n    replaced with the input variant. Peers \"aren't enough\" on their own\n    but the library will do some more work to use the right type.\n\nAs we just mentioned, API calls don't need to know the whole information\nabout the entities, only their ID and hash. For this reason, another method,\n`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`\nis available. This will always use the cache while possible, making zero API\ncalls most of the time. When a request is made, if you provided the full\nentity, e.g. an :tl:`User`, the library will convert it to the required\n:tl:`InputPeer` automatically for you.\n\n**You should always favour**\n`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`\n**over**\n`client.get_entity() <telethon.client.users.UserMethods.get_entity>`\nfor this reason! Calling the latter will always make an API call to get\nthe most recent information about said entity, but invoking requests don't\nneed this information, just the :tl:`InputPeer`. Only use\n`client.get_entity() <telethon.client.users.UserMethods.get_entity>`\nif you need to get actual information, like the username, name, title, etc.\nof the entity.\n\nTo further simplify the workflow, since the version ``0.16.2`` of the\nlibrary, the raw requests you make to the API are also able to call\n`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`\nwherever needed, so you can even do things like:\n\n.. code-block:: python\n\n    await client(SendMessageRequest('username', 'hello'))\n\nThe library will call the ``.resolve()`` method of the request, which will\nresolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if\nyou don't get this yet, but remember some of the details here are important.\n\n\nFull Entities\n=============\n\nIn addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its\nvariants for chats and channels), there is also the concept of :tl:`UserFull`.\n\nThis full variant has additional information such as whether the user is\nblocked, its notification settings, the bio or about of the user, etc.\n\nThere is also :tl:`messages.ChatFull` which is the equivalent of full entities\nfor chats and channels, with also the about section of the channel. Note that\nthe ``users`` field only contains bots for the channel (so that clients can\nsuggest commands to use).\n\nYou can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat`\nand :tl:`GetFullChannel` respectively.\n\n\nAccessing Entities\n==================\n\nAlthough it's explicitly noted in the documentation that messages\n*subclass* `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`\nand `SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`,\nsome people still don't get inheritance.\n\nWhen the documentation says \"Bases: `telethon.tl.custom.chatgetter.ChatGetter`\"\nit means that the class you're looking at, *also* can act as the class it\nbases. In this case, `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`\nknows how to get the *chat* where a thing belongs to.\n\nSo, a `Message <telethon.tl.custom.message.Message>` is a\n`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.\nThat means you can do this:\n\n.. code-block:: python\n\n    message.is_private\n    message.chat_id\n    await message.get_chat()\n    # ...etc\n\n`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is similar:\n\n.. code-block:: python\n\n    message.user_id\n    await message.get_input_sender()\n    message.user\n    # ...etc\n\nQuite a few things implement them, so it makes sense to reuse the code.\nFor example, all events (except raw updates) implement `ChatGetter\n<telethon.tl.custom.chatgetter.ChatGetter>` since all events occur\nin some chat.\n\n\nSummary\n=======\n\nTL;DR; If you're here because of *\"Could not find the input entity for\"*,\nyou must ask yourself \"how did I find this entity through official\napplications\"? Now do the same with the library. Use what applies:\n\n.. code-block:: python\n\n    # (These examples assume you are inside an \"async def\")\n    async with client:\n        # Does it have a username? Use it!\n        entity = await client.get_entity(username)\n\n        # Do you have a conversation open with them? Get dialogs.\n        await client.get_dialogs()\n\n        # Are they participant of some group? Get them.\n        await client.get_participants('username')\n\n        # Is the entity the original sender of a forwarded message? Get it.\n        await client.get_messages('username', 100)\n\n        # NOW you can use the ID, anywhere!\n        await client.send_message(123456, 'Hi!')\n\n        entity = await client.get_entity(123456)\n        print(entity)\n\nOnce the library has \"seen\" the entity, you can use their **integer** ID.\nYou can't use entities from IDs the library hasn't seen. You must make the\nlibrary see them *at least once* and disconnect properly. You know where\nthe entities are and you must tell the library. It won't guess for you.\n"
  },
  {
    "path": "readthedocs/concepts/errors.rst",
    "content": ".. _rpc-errors:\n\n==========\nRPC Errors\n==========\n\nRPC stands for Remote Procedure Call, and when the library raises\na ``RPCError``, it's because you have invoked some of the API\nmethods incorrectly (wrong parameters, wrong permissions, or even\nsomething went wrong on Telegram's server).\n\nYou should import the errors from ``telethon.errors`` like so:\n\n.. code-block:: python\n\n    from telethon import errors\n\n    try:\n        async with client.takeout() as takeout:\n            ...\n\n    except errors.TakeoutInitDelayError as e:\n        #  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ here we except TAKEOUT_INIT_DELAY\n        print('Must wait', e.seconds, 'before takeout')\n\n\nThere isn't any official list of all possible RPC errors, so the\n`list of known errors`_ is provided on a best-effort basis. When new methods\nare available, the list may be lacking since we simply don't know what errors\ncan raise from them.\n\nOnce we do find out about a new error and what causes it, the list is\nupdated, so if you see an error without a specific class, do report it\n(and what method caused it)!.\n\nThis list is used to generate documentation for the `raw API page`_.\nFor example, if we want to know what errors can occur from\n`messages.sendMessage`_ we can simply navigate to its raw API page\nand find it has 24 known RPC errors at the time of writing.\n\n\nBase Errors\n===========\n\nAll the \"base\" errors are listed in :ref:`telethon-errors`.\nAny other more specific error will be a subclass of these.\n\nIf the library isn't aware of a specific error just yet, it will instead\nraise one of these superclasses. This means you may find stuff like this:\n\n.. code-block:: text\n\n    telethon.errors.rpcbaseerrors.BadRequestError: RPCError 400: MESSAGE_POLL_CLOSED (caused by SendVoteRequest)\n\nIf you do, make sure to open an issue or send a pull request to update the\n`list of known errors`_.\n\n\nCommon Errors\n=============\n\nThese are some of the errors you may normally need to deal with:\n\n-  ``FloodWaitError`` (420), the same request was repeated many times.\n   Must wait ``.seconds`` (you can access this attribute). For example:\n\n   .. code-block:: python\n\n       ...\n       from telethon import errors\n\n       try:\n           messages = await client.get_messages(chat)\n           print(messages[0].text)\n       except errors.FloodWaitError as e:\n           print('Have to sleep', e.seconds, 'seconds')\n           time.sleep(e.seconds)\n\n-  ``SessionPasswordNeededError``, if you have setup two-steps\n   verification on Telegram and are trying to sign in.\n-  ``FilePartMissingError``, if you have tried to upload an empty file.\n-  ``ChatAdminRequiredError``, you don't have permissions to perform\n   said operation on a chat or channel. Try avoiding filters, i.e. when\n   searching messages.\n\nThe generic classes for different error codes are:\n\n- ``InvalidDCError`` (303), the request must be repeated on another DC.\n- ``BadRequestError`` (400), the request contained errors.\n- ``UnauthorizedError`` (401), the user is not authorized yet.\n- ``ForbiddenError`` (403), privacy violation error.\n- ``NotFoundError`` (404), make sure you're invoking ``Request``\\ 's!\n\nIf the error is not recognised, it will only be an ``RPCError``.\n\nYou can refer to all errors from Python through the ``telethon.errors``\nmodule. If you don't know what attributes they have, try printing their\ndir (like ``print(dir(e))``).\n\n\nAttributes\n==========\n\nSome of the errors carry additional data in them. When they look like\n``EMAIL_UNCONFIRMED_X``, the ``_X`` value will be accessible from the\nerror instance. The current list of errors that do this is the following:\n\n- ``EmailUnconfirmedError`` has ``.code_length``.\n- ``FileMigrateError`` has ``.new_dc``.\n- ``FilePartMissingError`` has ``.which``.\n- ``FloodTestPhoneWaitError`` has ``.seconds``.\n- ``FloodWaitError`` has ``.seconds``.\n- ``InterdcCallErrorError`` has ``.dc``.\n- ``InterdcCallRichErrorError`` has ``.dc``.\n- ``NetworkMigrateError`` has ``.new_dc``.\n- ``PhoneMigrateError`` has ``.new_dc``.\n- ``SlowModeWaitError`` has ``.seconds``.\n- ``TakeoutInitDelayError`` has ``.seconds``.\n- ``UserMigrateError`` has ``.new_dc``.\n\n\nAvoiding Limits\n===============\n\nDon't spam. You won't get ``FloodWaitError`` or your account banned or\ndeleted if you use the library *for legit use cases*. Make cool tools.\nDon't spam! Nobody knows the exact limits for all requests since they\ndepend on a lot of factors, so don't bother asking.\n\nStill, if you do have a legit use case and still get those errors, the\nlibrary will automatically sleep when they are smaller than 60 seconds\nby default. You can set different \"auto-sleep\" thresholds:\n\n.. code-block:: python\n\n    client.flood_sleep_threshold = 0  # Don't auto-sleep\n    client.flood_sleep_threshold = 24 * 60 * 60  # Sleep always\n\nYou can also except it and act as you prefer:\n\n.. code-block:: python\n\n    from telethon.errors import FloodWaitError\n    try:\n        ...\n    except FloodWaitError as e:\n        print('Flood waited for', e.seconds)\n        quit(1)\n\nVoIP numbers are very limited, and some countries are more limited too.\n\n\n.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv\n.. _raw API page: https://tl.telethon.dev/\n.. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html\n"
  },
  {
    "path": "readthedocs/concepts/full-api.rst",
    "content": ".. _full-api:\n\n============\nThe Full API\n============\n\n.. important::\n\n    While you have access to this, you should always use the friendly\n    methods listed on :ref:`client-ref` unless you have a better reason\n    not to, like a method not existing or you wanting more control.\n\n.. contents::\n\n\nIntroduction\n============\n\nThe :ref:`telethon-client` doesn't offer a method for every single request\nthe Telegram API supports. However, it's very simple to *call* or *invoke*\nany request defined in Telegram's API.\n\nThis section will teach you how to use what Telethon calls the `TL reference`_.\nThe linked page contains a list and a way to search through *all* types\ngenerated from the definition of Telegram's API (in ``.tl`` file format,\nhence the name). These types include requests and constructors.\n\n.. note::\n\n    The reason to keep both https://tl.telethon.dev and this\n    documentation alive is that the former allows instant search results\n    as you type, and a \"Copy import\" button. If you like namespaces, you\n    can also do ``from telethon.tl import types, functions``. Both work.\n\nTelegram makes these ``.tl`` files public, which other implementations, such\nas Telethon, can also use to generate code. These files are versioned under\nwhat's called \"layers\". ``.tl`` files consist of thousands of definitions,\nand newer layers often add, change, or remove them. Each definition refers\nto either a Remote Procedure Call (RPC) function, or a type (which the\n`TL reference`_ calls \"constructors\", as they construct particular type\ninstances).\n\nAs such, the `TL reference`_ is a good place to go to learn about all possible\nrequests, types, and what they look like. If you're curious about what's been\nchanged between layers, you can refer to the `TL diff`_ site.\n\n\nNavigating the TL reference\n===========================\n\nFunctions\n---------\n\n\"Functions\" is the term used for the Remote Procedure Calls (RPC) that can be\nsent to Telegram to ask it to perform something (e.g. \"send message\"). These\nrequests have an associated return type. These can be invoked (\"called\"):\n\n.. code-block:: python\n\n    client = TelegramClient(...)\n    function_instance = SomeRequest(...)\n\n    # Invoke the request\n    returned_type = await client(function_instance)\n\nWhenever you find the type for a function in the `TL reference`_, the page\nwill contain the following information:\n\n* What type of account can use the method. This information is regenerated\n  from time to time (by attempting to invoke the function under both account\n  types and finding out where it fails). Some requests can only be used by\n  bot accounts, others by user accounts, and others by both.\n* The TL definition. This helps you get a feel for the what the function\n  looks like. This is not Python code. It just contains the definition in\n  a concise manner.\n* \"Copy import\" button. Does what it says: it will copy the necessary Python\n  code to import the function to your system's clipboard for easy access.\n* Returns. The returned type. When you invoke the function, this is what the\n  result will be. It also includes which of the constructors can be returned\n  inline, to save you a click.\n* Parameters. The parameters accepted by the function, including their type,\n  whether they expect a list, and whether they're optional.\n* Known RPC errors. A best-effort list of known errors the request may cause.\n  This list is not complete and may be out of date, but should provide an\n  overview of what could go wrong.\n* Example. Autogenerated example, showcasing how you may want to call it.\n  Bear in mind that this is *autogenerated*. It may be spitting out non-sense.\n  The goal of this example is not to show you everything you can do with the\n  request, only to give you a feel for what it looks like to use it.\n\nIt is very important to click through the links and navigate to get the full\npicture. A specific page will show you what the specific function returns and\nneeds as input parameters. But it may reference other types, so you need to\nnavigate to those to learn what those contain or need.\n\nTypes\n-----\n\n\"Types\" as understood by TL are not actually generated in Telethon.\nThey would be the \"abstract base class\" of the constructors, but since Python\nis duck-typed, there is hardly any need to generate mostly unnecessary code.\nThe page for a type contains:\n\n* Constructors. Every type will have one or more constructors. These\n  constructors *are* generated and can be immported and used.\n* Requests returning this type. A helpful way to find out \"what requests can\n  return this?\". This is how you may learn what request you need to use to\n  obtain a particular instance of a type.\n* Requests accepting this type as input. A helpful way to find out \"what\n  requests can use this type as one of their input parameters?\". This is how\n  you may learn where a type is used.\n* Other types containing this type. A helpful way to find out \"where else\n  does this type appear?\". This is how you can walk back through nested\n  objects.\n\nConstructors\n------------\n\nConstructors are used to create instances of a particular type, and are also\nreturned when invoking requests. You will have to create instances yourself\nwhen invoking requests that need a particular type as input.\nThe page for a constructor contains:\n\n* Belongs to. The parent type. This is a link back to the types page for the\n  specific constructor. It also contains the sibling constructors inline, to\n  save you a click.\n* Members. Both the input parameters *and* fields the constructor contains.\n\n\nUsing the TL reference\n======================\n\nAfter you've found a request you want to send, a good start would be to simply\ncopy and paste the autogenerated example into your script. Then you can simply\ntweak it to your needs.\n\nIf you want to do it from scratch, first, make sure to import the request into\nyour code (either using the \"Copy import\" button near the top, or by manually\nspelling out the package under ``telethon.tl.functions.*``).\n\nThen, start reading the parameters one by one. If the parameter cannot be\nomitted, you **will** need to specify it, so make sure to spell it out as\nan input parameter when constructing the request instance. Let's look at\n`PingRequest`_ for example. First, we copy the import:\n\n.. code-block:: python\n\n    from telethon.tl.functions import PingRequest\n\nThen, we look at the parameters:\n\n    ping_id - long\n\nA single parameter, and it's a long (a integer number with a large range of\nvalues). It doesn't say it can be omitted, so we must provide it, like so:\n\n.. code-block:: python\n\n    PingRequest(\n        ping_id=48641868471\n    )\n\n(In this case, the ping ID is a random number. You often have to guess what\nthe parameter needs just by looking at the name.)\n\nNow that we have our request, we can invoke it:\n\n.. code-block:: python\n\n    response = await client(PingRequest(\n        ping_id=48641868471\n    ))\n\nTo find out what ``response`` looks like, we can do as the autogenerated\nexample suggests and \"stringify\" the result as a pretty-printed string:\n\n.. code-block:: python\n\n    print(result.stringify())\n\nThis will print out the following:\n\n.. code-block:: python\n\n    Pong(\n        msg_id=781875678118,\n        ping_id=48641868471\n    )\n\nWhich is a very easy way to get a feel for a response. You should nearly\nalways print the stringified result, at least once, when trying out requests,\nto get a feel for what the response may look like.\n\nBut of course, you don't need to do that. Without writing any code, you could\nhave navigated through the \"Returns\" link to learn ``PingRequest`` returns a\n``Pong``, which only has one constructor, and the constructor has two members,\n``msg_id`` and ``ping_id``.\n\nIf you wanted to create your own ``Pong``, you would use both members as input\nparameters:\n\n.. code-block:: python\n\n    my_pong = Pong(\n        msg_id=781875678118,\n        ping_id=48641868471\n    )\n\n(Yes, constructing object instances can use the same code that ``.stringify``\nwould return!)\n\nAnd if you wanted to access the ``msg_id`` member, you would simply access it\nlike any other attribute access in Python:\n\n.. code-block:: python\n\n    print(response.msg_id)\n\n\nExample walkthrough\n===================\n\nSay `client.send_message()\n<telethon.client.messages.MessageMethods.send_message>` didn't exist,\nwe could `use the search`_ to look for \"message\". There we would find\n:tl:`SendMessageRequest`, which we can work with.\n\nEvery request is a Python class, and has the parameters needed for you\nto invoke it. You can also call ``help(request)`` for information on\nwhat input parameters it takes. Remember to \"Copy import to the\nclipboard\", or your script won't be aware of this class! Now we have:\n\n.. code-block:: python\n\n    from telethon.tl.functions.messages import SendMessageRequest\n\nIf you're going to use a lot of these, you may do:\n\n.. code-block:: python\n\n    from telethon.tl import types, functions\n    # We now have access to 'functions.messages.SendMessageRequest'\n\nWe see that this request must take at least two parameters, a ``peer``\nof type :tl:`InputPeer`, and a ``message`` which is just a Python\n`str`\\ ing.\n\nHow can we retrieve this :tl:`InputPeer`? We have two options. We manually\nconstruct one, for instance:\n\n.. code-block:: python\n\n    from telethon.tl.types import InputPeerUser\n\n    peer = InputPeerUser(user_id, user_hash)\n\nOr we call `client.get_input_entity()\n<telethon.client.users.UserMethods.get_input_entity>`:\n\n.. code-block:: python\n\n    import telethon\n\n    async def main():\n        peer = await client.get_input_entity('someone')\n\n    client.loop.run_until_complete(main())\n\n.. note::\n\n    Remember that ``await`` must occur inside an ``async def``.\n    Every full API example assumes you already know and do this.\n\n\nWhen you're going to invoke an API method, most require you to pass an\n:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using\n`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`\nis more straightforward (and often immediate, if you've seen the user before,\nknow their ID, etc.). If you also **need** to have information about the whole\nuser, use `client.get_entity() <telethon.client.users.UserMethods.get_entity>`\ninstead:\n\n.. code-block:: python\n\n    entity = await client.get_entity('someone')\n\nIn the later case, when you use the entity, the library will cast it to\nits \"input\" version for you. If you already have the complete user and\nwant to cache its input version so the library doesn't have to do this\nevery time its used, simply call `telethon.utils.get_input_peer`:\n\n.. code-block:: python\n\n    from telethon import utils\n    peer = utils.get_input_peer(entity)\n\n\n.. note::\n\n    Since ``v0.16.2`` this is further simplified. The ``Request`` itself\n    will call `client.get_input_entity\n    <telethon.client.users.UserMethods.get_input_entity>` for you when\n    required, but it's good to remember what's happening.\n\nAfter this small parenthesis about `client.get_entity\n<telethon.client.users.UserMethods.get_entity>` versus\n`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`,\nwe have everything we need. To invoke our\nrequest we do:\n\n.. code-block:: python\n\n    result = await client(SendMessageRequest(peer, 'Hello there!'))\n\nMessage sent! Of course, this is only an example. There are over 250\nmethods available as of layer 80, and you can use every single of them\nas you wish. Remember to use the right types! To sum up:\n\n.. code-block:: python\n\n    result = await client(SendMessageRequest(\n        await client.get_input_entity('username'), 'Hello there!'\n    ))\n\n\nThis can further be simplified to:\n\n.. code-block:: python\n\n    result = await client(SendMessageRequest('username', 'Hello there!'))\n    # Or even\n    result = await client(SendMessageRequest(PeerChannel(id), 'Hello there!'))\n\n.. note::\n\n    Note that some requests have a \"hash\" parameter. This is **not**\n    your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.\n\n    It's a special hash used by Telegram to only send a difference of new data\n    that you don't already have with that request, so you can leave it to 0,\n    and it should work (which means no hash is known yet).\n\n    For those requests having a \"limit\" parameter, you can often set it to\n    zero to signify \"return default amount\". This won't work for all of them\n    though, for instance, in \"messages.search\" it will actually return 0 items.\n\n\nRequests in Parallel\n====================\n\nThe library will automatically merge outgoing requests into a single\n*container*. Telegram's API supports sending multiple requests in a\nsingle container, which is faster because it has less overhead and\nthe server can run them without waiting for others. You can also\nforce using a container manually:\n\n.. code-block:: python\n\n    async def main():\n\n        # Letting the library do it behind the scenes\n        await asyncio.wait([\n            client.send_message('me', 'Hello'),\n            client.send_message('me', ','),\n            client.send_message('me', 'World'),\n            client.send_message('me', '.')\n        ])\n\n        # Manually invoking many requests at once\n        await client([\n            SendMessageRequest('me', 'Hello'),\n            SendMessageRequest('me', ', '),\n            SendMessageRequest('me', 'World'),\n            SendMessageRequest('me', '.')\n        ])\n\nNote that you cannot guarantee the order in which they are run.\nTry running the above code more than one time. You will see the\norder in which the messages arrive is different.\n\nIf you use the raw API (the first option), you can use ``ordered``\nto tell the server that it should run the requests sequentially.\nThis will still be faster than going one by one, since the server\nknows all requests directly:\n\n.. code-block:: python\n\n    await client([\n        SendMessageRequest('me', 'Hello'),\n        SendMessageRequest('me', ', '),\n        SendMessageRequest('me', 'World'),\n        SendMessageRequest('me', '.')\n    ], ordered=True)\n\nIf any of the requests fails with a Telegram error (not connection\nerrors or any other unexpected events), the library will raise\n`telethon.errors.common.MultiError`. You can ``except`` this\nand still access the successful results:\n\n.. code-block:: python\n\n    from telethon.errors import MultiError\n\n    try:\n        await client([\n            SendMessageRequest('me', 'Hello'),\n            SendMessageRequest('me', ''),\n            SendMessageRequest('me', 'World')\n        ], ordered=True)\n    except MultiError as e:\n        # The first and third requests worked.\n        first = e.results[0]\n        third = e.results[2]\n        # The second request failed.\n        second = e.exceptions[1]\n\n.. _TL reference: https://tl.telethon.dev\n.. _TL diff: https://diff.telethon.dev\n.. _PingRequest: https://tl.telethon.dev/methods/ping.html\n.. _use the search: https://tl.telethon.dev/?q=message&redirect=no\n"
  },
  {
    "path": "readthedocs/concepts/sessions.rst",
    "content": ".. _sessions:\n\n==============\nSession Files\n==============\n\n.. contents::\n\nThey are an important part for the library to be efficient, such as caching\nand handling your authorization key (or you would have to login every time!).\n\nWhat are Sessions?\n==================\n\nThe first parameter you pass to the constructor of the\n:ref:`TelegramClient <telethon-client>` is\nthe ``session``, and defaults to be the session name (or full path). That is,\nif you create a ``TelegramClient('anon')`` instance and connect, an\n``anon.session`` file will be created in the working directory.\n\nNote that if you pass a string it will be a file in the current working\ndirectory, although you can also pass absolute paths.\n\nThe session file contains enough information for you to login without\nre-sending the code, so if you have to enter the code more than once,\nmaybe you're changing the working directory, renaming or removing the\nfile, or using random names.\n\nThese database files using ``sqlite3`` contain the required information to\ntalk to the Telegram servers, such as to which IP the client should connect,\nport, authorization key so that messages can be encrypted, and so on.\n\nThese files will by default also save all the input entities that you've seen,\nso that you can get information about a user or channel by just their ID.\nTelegram will **not** send their ``access_hash`` required to retrieve more\ninformation about them, if it thinks you have already seem them. For this\nreason, the library needs to store this information offline.\n\nThe library will by default too save all the entities (chats and channels\nwith their name and username, and users with the phone too) in the session\nfile, so that you can quickly access them by username or phone number.\n\nIf you're not going to work with updates, or don't need to cache the\n``access_hash`` associated with the entities' ID, you can disable this\nby setting ``client.session.save_entities = False``.\n\n\nDifferent Session Storage\n=========================\n\nIf you don't want to use the default SQLite session storage, you can also\nuse one of the other implementations or implement your own storage.\n\nWhile it's often not the case, it's possible that SQLite is slow enough to\nbe noticeable, in which case you can also use a different storage. Note that\nthis is rare and most people won't have this issue, but it's worth a mention.\n\nTo use a custom session storage, simply pass the custom session instance to\n:ref:`TelegramClient <telethon-client>` instead of\nthe session name.\n\nTelethon contains three implementations of the abstract ``Session`` class:\n\n.. currentmodule:: telethon.sessions\n\n* `MemorySession <memory.MemorySession>`: stores session data within memory.\n* `SQLiteSession <sqlite.SQLiteSession>`: stores sessions within on-disk SQLite databases. Default.\n* `StringSession <string.StringSession>`: stores session data within memory,\n  but can be saved as a string.\n\nYou can import these ``from telethon.sessions``. For example, using the\n`StringSession <string.StringSession>` is done as follows:\n\n.. code-block:: python\n\n    from telethon.sync import TelegramClient\n    from telethon.sessions import StringSession\n\n    with TelegramClient(StringSession(string), api_id, api_hash) as client:\n        ...  # use the client\n\n        # Save the string session as a string; you should decide how\n        # you want to save this information (over a socket, remote\n        # database, print it and then paste the string in the code,\n        # etc.); the advantage is that you don't need to save it\n        # on the current disk as a separate file, and can be reused\n        # anywhere else once you log in.\n        string = client.session.save()\n\n    # Note that it's also possible to save any other session type\n    # as a string by using ``StringSession.save(session_instance)``:\n    client = TelegramClient('sqlite-session', api_id, api_hash)\n    string = StringSession.save(client.session)\n\nThere are other community-maintained implementations available:\n\n* `SQLAlchemy <https://github.com/tulir/telethon-session-sqlalchemy>`_:\n  stores all sessions in a single database via SQLAlchemy.\n\n* `Redis <https://github.com/ezdev128/telethon-session-redis>`_:\n  stores all sessions in a single Redis data store.\n\n* `MongoDB <https://github.com/watzon/telethon-session-mongo>`_:\n  stores the current session in a MongoDB database.\n\n\nCreating your Own Storage\n=========================\n\nThe easiest way to create your own storage implementation is to use\n`MemorySession <memory.MemorySession>` as the base and check out how\n`SQLiteSession <sqlite.SQLiteSession>` or one of the community-maintained\nimplementations work. You can find the relevant Python files under the\n``sessions/`` directory in the Telethon's repository.\n\nAfter you have made your own implementation, you can add it to the\ncommunity-maintained session implementation list above with a pull request.\n\n\nString Sessions\n===============\n\n`StringSession <string.StringSession>` are a convenient way to embed your\nlogin credentials directly into your code for extremely easy portability,\nsince all they take is a string to be able to login without asking for your\nphone and code (or faster start if you're using a bot token).\n\nThe easiest way to generate a string session is as follows:\n\n.. code-block:: python\n\n    from telethon.sync import TelegramClient\n    from telethon.sessions import StringSession\n\n    with TelegramClient(StringSession(), api_id, api_hash) as client:\n        print(client.session.save())\n\n\nThink of this as a way to export your authorization key (what's needed\nto login into your account). This will print a string in the standard\noutput (likely your terminal).\n\n.. warning::\n\n    **Keep this string safe!** Anyone with this string can use it\n    to login into your account and do anything they want to.\n\n    This is similar to leaking your ``*.session`` files online,\n    but it is easier to leak a string than it is to leak a file.\n\n\nOnce you have the string (which is a bit long), load it into your script\nsomehow. You can use a normal text file and ``open(...).read()`` it or\nyou can save it in a variable directly:\n\n.. code-block:: python\n\n    string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...'\n    with TelegramClient(StringSession(string), api_id, api_hash) as client:\n        client.loop.run_until_complete(client.send_message('me', 'Hi'))\n\n\nThese strings are really convenient for using in places like Heroku since\ntheir ephemeral filesystem will delete external files once your application\nis over.\n"
  },
  {
    "path": "readthedocs/concepts/strings.rst",
    "content": "======================\nString-based Debugging\n======================\n\nDebugging is *really* important. Telegram's API is really big and there\nare a lot of things that you should know. Such as, what attributes or fields\ndoes a result have? Well, the easiest thing to do is printing it:\n\n.. code-block:: python\n\n    entity = await client.get_entity('username')\n    print(entity)\n\nThat will show a huge **string** similar to the following:\n\n.. code-block:: python\n\n    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)\n\nThat's a lot of text. But as you can see, all the properties are there.\nSo if you want the title you **don't use regex** or anything like\nsplitting ``str(entity)`` to get what you want. You just access the\nattribute you need:\n\n.. code-block:: python\n\n    title = entity.title\n\nCan we get better than the shown string, though? Yes!\n\n.. code-block:: python\n\n    print(entity.stringify())\n\nWill show a much better representation:\n\n.. code-block:: python\n\n    Channel(\n        id=1066197625,\n        title='Telegram Usernames',\n        photo=ChatPhotoEmpty(\n        ),\n        date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc),\n        version=0,\n        creator=False,\n        left=True,\n        broadcast=True,\n        verified=True,\n        megagroup=False,\n        restricted=False,\n        signatures=False,\n        min=False,\n        scam=False,\n        has_link=False,\n        has_geo=False,\n        slowmode_enabled=False,\n        access_hash=-6309373984955162244,\n        username='username',\n        restriction_reason=[\n        ],\n        admin_rights=None,\n        banned_rights=None,\n        default_banned_rights=None,\n        participants_count=None\n    )\n\n\nNow it's easy to see how we could get, for example,\nthe ``year`` value. It's inside ``date``:\n\n.. code-block:: python\n\n    channel_year = entity.date.year\n\nYou don't need to print everything to see what all the possible values\ncan be. You can just search in http://tl.telethon.dev/.\n\nRemember that you can use Python's `isinstance\n<https://docs.python.org/3/library/functions.html#isinstance>`_\nto check the type of something. For example:\n\n.. code-block:: python\n\n    from telethon import types\n\n    if isinstance(entity.photo, types.ChatPhotoEmpty):\n        print('Channel has no photo')\n"
  },
  {
    "path": "readthedocs/concepts/updates.rst",
    "content": "================\nUpdates in Depth\n================\n\nProperties vs. Methods\n======================\n\nThe event shown above acts just like a `custom.Message\n<telethon.tl.custom.message.Message>`, which means you\ncan access all the properties it has, like ``.sender``.\n\n**However** events are different to other methods in the client, like\n`client.get_messages <telethon.client.messages.MessageMethods.get_messages>`.\nEvents *may not* send information about the sender or chat, which means it\ncan be `None`, but all the methods defined in the client always have this\ninformation so it doesn't need to be re-fetched. For this reason, you have\n``get_`` methods, which will make a network call if necessary.\n\nIn short, you should do this:\n\n.. code-block:: python\n\n    @client.on(events.NewMessage)\n    async def handler(event):\n        # event.input_chat may be None, use event.get_input_chat()\n        chat = await event.get_input_chat()\n        sender = await event.get_sender()\n        buttons = await event.get_buttons()\n\n    async def main():\n        async for message in client.iter_messages('me', 10):\n            # Methods from the client always have these properties ready\n            chat = message.input_chat\n            sender = message.sender\n            buttons = message.buttons\n\nNotice, properties (`message.sender\n<telethon.tl.custom.message.Message.sender>`) don't need an ``await``, but\nmethods (`message.get_sender\n<telethon.tl.custom.message.Message.get_sender>`) **do** need an ``await``,\nand you should use methods in events for these properties that may need network.\n\nEvents Without the client\n=========================\n\nThe code of your application starts getting big, so you decide to\nseparate the handlers into different files. But how can you access\nthe client from these files? You don't need to! Just `events.register\n<telethon.events.register>` them:\n\n.. code-block:: python\n\n    # handlers/welcome.py\n    from telethon import events\n\n    @events.register(events.NewMessage('(?i)hello'))\n    async def handler(event):\n        client = event.client\n        await event.respond('Hey!')\n        await client.send_message('me', 'I said hello to someone')\n\n\nRegistering events is a way of saying \"this method is an event handler\".\nYou can use `telethon.events.is_handler` to check if any method is a handler.\nYou can think of them as a different approach to Flask's blueprints.\n\nIt's important to note that this does **not** add the handler to any client!\nYou never specified the client on which the handler should be used. You only\ndeclared that it is a handler, and its type.\n\nTo actually use the handler, you need to `client.add_event_handler\n<telethon.client.updates.UpdateMethods.add_event_handler>` to the\nclient (or clients) where they should be added to:\n\n.. code-block:: python\n\n    # main.py\n    from telethon import TelegramClient\n    import handlers.welcome\n\n    with TelegramClient(...) as client:\n        client.add_event_handler(handlers.welcome.handler)\n        client.run_until_disconnected()\n\n\nThis also means that you can register an event handler once and\nthen add it to many clients without re-declaring the event.\n\n\nEvents Without Decorators\n=========================\n\nIf for any reason you don't want to use `telethon.events.register`,\nyou can explicitly pass the event handler to use to the mentioned\n`client.add_event_handler\n<telethon.client.updates.UpdateMethods.add_event_handler>`:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, events\n\n    async def handler(event):\n        ...\n\n    with TelegramClient(...) as client:\n        client.add_event_handler(handler, events.NewMessage)\n        client.run_until_disconnected()\n\n\nSimilarly, you also have `client.remove_event_handler\n<telethon.client.updates.UpdateMethods.remove_event_handler>`\nand `client.list_event_handlers\n<telethon.client.updates.UpdateMethods.list_event_handlers>`.\n\nThe ``event`` argument is optional in all three methods and defaults to\n`events.Raw <telethon.events.raw.Raw>` for adding, and `None` when\nremoving (so all callbacks would be removed).\n\n.. note::\n\n    The ``event`` type is ignored in `client.add_event_handler\n    <telethon.client.updates.UpdateMethods.add_event_handler>`\n    if you have used `telethon.events.register` on the ``callback``\n    before, since that's the point of using such method at all.\n\n\nStopping Propagation of Updates\n===============================\n\nThere might be cases when an event handler is supposed to be used solitary and\nit makes no sense to process any other handlers in the chain. For this case,\nit is possible to raise a `telethon.events.StopPropagation` exception which\nwill cause the propagation of the update through your handlers to stop:\n\n.. code-block:: python\n\n    from telethon.events import StopPropagation\n\n    @client.on(events.NewMessage)\n    async def _(event):\n        # ... some conditions\n        await event.delete()\n\n        # Other handlers won't have an event to work with\n        raise StopPropagation\n\n    @client.on(events.NewMessage)\n    async def _(event):\n        # Will never be reached, because it is the second handler\n        # in the chain.\n        pass\n\n\nRemember to check :ref:`telethon-events` if you're looking for\nthe methods reference.\n\nUnderstanding asyncio\n=====================\n\n\nWith `asyncio`, the library has several tasks running in the background.\nOne task is used for sending requests, another task is used to receive them,\nand a third one is used to handle updates.\n\nTo handle updates, you must keep your script running. You can do this in\nseveral ways. For instance, if you are *not* running `asyncio`'s event\nloop, you should use `client.run_until_disconnected\n<telethon.client.updates.UpdateMethods.run_until_disconnected>`:\n\n.. code-block:: python\n\n    import asyncio\n    from telethon import TelegramClient\n\n    client = TelegramClient(...)\n    ...\n    client.run_until_disconnected()\n\n\nBehind the scenes, this method is ``await``'ing on the `client.disconnected\n<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>` property,\nso the code above and the following are equivalent:\n\n.. code-block:: python\n\n    import asyncio\n    from telethon import TelegramClient\n\n    client = TelegramClient(...)\n\n    async def main():\n        await client.disconnected\n\n    asyncio.run(main())\n\n\nYou could also run `client.disconnected\n<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`\nuntil it completed.\n\nBut if you don't want to ``await``, then you should know what you want\nto be doing instead! What matters is that you shouldn't let your script\ndie. If you don't care about updates, you don't need any of this.\n\nNotice that unlike `client.disconnected\n<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,\n`client.run_until_disconnected\n<telethon.client.updates.UpdateMethods.run_until_disconnected>` will\nhandle ``KeyboardInterrupt`` for you. This method is special and can\nalso be ran while the loop is running, so you can do this:\n\n.. code-block:: python\n\n    async def main():\n        await client.run_until_disconnected()\n\n    loop.run_until_complete(main())\n\nSequential Updates\n==================\n\nIf you need to process updates sequentially (i.e. not in parallel),\nyou should set ``sequential_updates=True`` when creating the client:\n\n.. code-block:: python\n\n    with TelegramClient(..., sequential_updates=True) as client:\n        ...\n"
  },
  {
    "path": "readthedocs/conf.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# Telethon documentation build configuration file, created by\n# sphinx-quickstart on Fri Nov 17 15:36:11 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport re\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(os.curdir))\nsys.path.insert(0, os.path.abspath(os.pardir))\n\nroot = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))\n\ntl_ref_url = 'https://tl.telethon.dev'\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosummary',\n    'sphinx.ext.intersphinx',\n    'custom_roles'\n]\n\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/3', None)\n}\n\n# Change the default role so we can avoid prefixing everything with :obj:\ndefault_role = \"py:obj\"\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = 'Telethon'\ncopyright = '2017 - 2019, Lonami'\nauthor = 'Lonami'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nwith open(os.path.join(root, 'telethon', 'version.py'), 'r') as f:\n    version = re.search(r\"^__version__\\s+=\\s+'(.*)'$\",\n                        f.read(), flags=re.MULTILINE).group(1)\n\n# The full version, including alpha/beta/rc tags.\nrelease = version\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'friendly'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\ndef skip(app, what, name, obj, would_skip, options):\n    if name.endswith('__'):\n        # We want to show special methods names, except some which add clutter\n        return name in {\n            '__init__',\n            '__abstractmethods__',\n            '__module__',\n            '__doc__',\n            '__dict__'\n        }\n\n    return would_skip\n\n\ndef setup(app):\n    app.connect(\"autodoc-skip-member\", skip)\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\nhtml_theme_options = {\n    'collapse_navigation': True,\n    'display_version': True,\n    'navigation_depth': 3,\n}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n# html_static_path = ['_static']\n\n# Custom sidebar templates, must be a dictionary that maps document names\n# to template names.\n#\n# This is required for the alabaster theme\n# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars\nhtml_sidebars = {\n    '**': [\n        'globaltoc.html',\n        'relations.html',  # needs 'show_related': True theme option to display\n        'searchbox.html',\n    ]\n}\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'Telethondoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'Telethon.tex', 'Telethon Documentation',\n     author, 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'telethon', 'Telethon Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'Telethon', 'Telethon Documentation',\n     author, 'Telethon', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n"
  },
  {
    "path": "readthedocs/custom_roles.py",
    "content": "from docutils import nodes, utils\nfrom docutils.parsers.rst.roles import set_classes\n\n\ndef make_link_node(rawtext, app, name, options):\n    \"\"\"\n    Create a link to the TL reference.\n\n    :param rawtext: Text being replaced with link node.\n    :param app: Sphinx application context\n    :param name: Name of the object to link to\n    :param options: Options dictionary passed to role func.\n    \"\"\"\n    try:\n        base = app.config.tl_ref_url\n        if not base:\n            raise AttributeError\n    except AttributeError as e:\n        raise ValueError('tl_ref_url config value is not set') from e\n\n    if base[-1] != '/':\n        base += '/'\n\n    set_classes(options)\n    node = nodes.reference(rawtext, utils.unescape(name),\n                           refuri='{}?q={}'.format(base, name),\n                           **options)\n    return node\n\n\n# noinspection PyUnusedLocal\ndef tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):\n    \"\"\"\n    Link to the TL reference.\n\n    Returns 2 part tuple containing list of nodes to insert into the\n    document and a list of system messages. Both are allowed to be empty.\n\n    :param name: The role name used in the document.\n    :param rawtext: The entire markup snippet, with role.\n    :param text: The text marked with the role.\n    :param lineno: The line number where rawtext appears in the input.\n    :param inliner: The inliner instance that called us.\n    :param options: Directive options for customization.\n    :param content: The directive content for customization.\n    \"\"\"\n    if options is None:\n        options = {}\n\n    # TODO Report error on type not found?\n    # Usage:\n    #   msg = inliner.reporter.error(..., line=lineno)\n    #   return [inliner.problematic(rawtext, rawtext, msg)], [msg]\n    app = inliner.document.settings.env.app\n    node = make_link_node(rawtext, app, text, options)\n    return [node], []\n\n\ndef setup(app):\n    \"\"\"\n    Install the plugin.\n\n    :param app: Sphinx application context.\n    \"\"\"\n    app.add_role('tl', tl_role)\n    app.add_config_value('tl_ref_url', None, 'env')\n    return\n"
  },
  {
    "path": "readthedocs/developing/coding-style.rst",
    "content": "============\nCoding Style\n============\n\n\nBasically, make it **readable**, while keeping the style similar to the\ncode of whatever file you're working on.\n\nAlso note that not everyone has 4K screens for their primary monitors,\nso please try to stick to the 80-columns limit. This makes it easy to\n``git diff`` changes from a terminal before committing changes. If the\nline has to be long, please don't exceed 120 characters.\n\nFor the commit messages, please make them *explanatory*. Not only\nthey're helpful to troubleshoot when certain issues could have been\nintroduced, but they're also used to construct the change log once a new\nversion is ready.\n\nIf you don't know enough Python, I strongly recommend reading `Dive Into\nPython 3 <http://www.diveintopython3.net/>`__, available online for\nfree. For instance, remember to do ``if x is None`` or\n``if x is not None`` instead ``if x == None``!\n"
  },
  {
    "path": "readthedocs/developing/philosophy.rst",
    "content": "==========\nPhilosophy\n==========\n\n\nThe intention of the library is to have an existing MTProto library\nexisting with hardly any dependencies (indeed, wherever Python is\navailable, you can run this library).\n\nBeing written in Python means that performance will be nowhere close to\nother implementations written in, for instance, Java, C++, Rust, or\npretty much any other compiled language. However, the library turns out\nto actually be pretty decent for common operations such as sending\nmessages, receiving updates, or other scripting. Uploading files may be\nnotably slower, but if you would like to contribute, pull requests are\nappreciated!\n\nIf ``libssl`` is available on your system, the library will make use of\nit to speed up some critical parts such as encrypting and decrypting the\nmessages. Files will notably be sent and downloaded faster.\n\nThe main focus is to keep everything clean and simple, for everyone to\nunderstand how working with MTProto and Telegram works. Don't be afraid\nto read the source, the code won't bite you! It may prove useful when\nusing the library on your own use cases.\n"
  },
  {
    "path": "readthedocs/developing/project-structure.rst",
    "content": "=================\nProject Structure\n=================\n\n\nMain interface\n==============\n\nThe library itself is under the ``telethon/`` directory. The\n``__init__.py`` file there exposes the main ``TelegramClient``, a class\nthat servers as a nice interface with the most commonly used methods on\nTelegram such as sending messages, retrieving the message history,\nhandling updates, etc.\n\nThe ``TelegramClient`` inherits from several mixing ``Method`` classes,\nsince there are so many methods that having them in a single file would\nmake maintenance painful (it was three thousand lines before this separation\nhappened!). It's a \"god object\", but there is only a way to interact with\nTelegram really.\n\nThe ``TelegramBaseClient`` is an ABC which will support all of these mixins\nso they can work together nicely. It doesn't even know how to invoke things\nbecause they need to be resolved with user information first (to work with\ninput entities comfortably).\n\nThe client makes use of the ``network/mtprotosender.py``. The\n``MTProtoSender`` is responsible for connecting, reconnecting,\npacking, unpacking, sending and receiving items from the network.\nBasically, the low-level communication with Telegram, and handling\nMTProto-related functions and types such as ``BadSalt``.\n\nThe sender makes use of a ``Connection`` class which knows the format in\nwhich outgoing messages should be sent (how to encode their length and\ntheir body, if they're further encrypted).\n\nAuto-generated code\n===================\n\nThe files under ``telethon_generator/`` are used to generate the code\nthat gets placed under ``telethon/tl/``. The parsers take in files in\na specific format (such as ``.tl`` for objects and ``.json`` for errors)\nand spit out the generated classes which represent, as Python classes,\nthe request and types defined in the ``.tl`` file. It also constructs\nan index so that they can be imported easily.\n\nCustom documentation can also be generated to easily navigate through\nthe vast amount of items offered by the API.\n\nIf you clone the repository, you will have to run ``python setup.py gen``\nin order to generate the code. Installing the library runs the generator\ntoo, but the mentioned command will just generate code.\n"
  },
  {
    "path": "readthedocs/developing/telegram-api-in-other-languages.rst",
    "content": "===============================\nTelegram API in Other Languages\n===============================\n\nTelethon was made for **Python**, and it has inspired other libraries such as\n`gramjs <https://github.com/gram-js/gramjs>`__ (JavaScript) and `grammers\n<https://github.com/Lonami/grammers>`__ (Rust). But there is a lot more beyond\nthose, made independently by different developers.\n\nIf you're looking for something like Telethon but in a different programming\nlanguage, head over to `Telegram API in Other Languages in the official wiki\n<https://github.com/LonamiWebs/Telethon/wiki/Telegram-API-in-Other-Languages>`__\nfor a (mostly) up-to-date list.\n"
  },
  {
    "path": "readthedocs/developing/test-servers.rst",
    "content": "============\nTest Servers\n============\n\n\nTo run Telethon on a test server, use the following code:\n\n.. code-block:: python\n\n    client = TelegramClient(None, api_id, api_hash)\n    client.session.set_dc(dc_id, '149.154.167.40', 80)\n\nYou can check your ``'test ip'`` on https://my.telegram.org.\n\nYou should set `None` session so to ensure you're generating a new\nauthorization key for it (it would fail if you used a session where you\nhad previously connected to another data center).\n\nNote that port 443 might not work, so you can try with 80 instead.\n\nOnce you're connected, you'll likely be asked to either sign in or sign up.\nRemember `anyone can access the phone you\nchoose <https://core.telegram.org/api/datacenter#testing-redirects>`__,\nso don't store sensitive data here.\n\nValid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and\n``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would\nbe ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five\ntimes, in this case, ``22222`` so we can hardcode that:\n\n.. code-block:: python\n\n    client = TelegramClient(None, api_id, api_hash)\n    client.session.set_dc(2, '149.154.167.40', 80)\n    client.start(\n        phone='9996621234', code_callback=lambda: '22222'\n    )\n\nNote that Telegram has changed the length of login codes multiple times in the\npast, so if ``dc_id`` repeated five times does not work, try repeating it six\ntimes.\n"
  },
  {
    "path": "readthedocs/developing/testing.rst",
    "content": "=====\nTests\n=====\n\nTelethon uses `Pytest <https://pytest.org/>`__, for testing, `Tox\n<https://tox.readthedocs.io/en/latest/>`__ for environment setup, and\n`pytest-asyncio <https://pypi.org/project/pytest-asyncio/>`__ and `pytest-cov\n<https://pytest-cov.readthedocs.io/en/latest/>`__ for asyncio and \n`coverage <https://coverage.readthedocs.io/>`__ integration.\n\nWhile reading the full documentation for these is probably a good idea, there\nis a lot to read, so a brief summary of these tools is provided below for\nconvienience.\n\nBrief Introduction to Pytest\n============================\n\n`Pytest <https://pytest.org/>`__ is a tool for discovering and running python\ntests, as well as allowing modular reuse of test setup code using fixtures.\n\nMost Pytest tests will look something like this::\n\n    from module import my_thing, my_other_thing\n\n    def test_my_thing(fixture):\n        assert my_thing(fixture) == 42\n\n    @pytest.mark.asyncio\n    async def test_my_thing(event_loop):\n        assert await my_other_thing(loop=event_loop) == 42\n\nNote here:\n\n 1. The test imports one specific function. The role of unit tests is to test\n    that the implementation of some unit, like a function or class, works.\n    It's role is not so much to test that components interact well with each\n    other. I/O, such as connecting to remote servers, should be avoided. This\n    helps with quickly identifying the source of an error, finding silent\n    breakage, and makes it easier to cover all possible code paths.\n\n    System or integration tests can also be useful, but are currently out of\n    scope of Telethon's automated testing.\n\n 2. A function ``test_my_thing`` is declared. Pytest searches for files\n    starting with ``test_``, classes starting with ``Test`` and executes any\n    functions or methods starting with ``test_`` it finds.\n\n 3. The function is declared with a parameter ``fixture``. Fixtures are used to\n    request things required to run the test, such as temporary directories,\n    free TCP ports, Connections, etc. Fixtures are declared by simply adding\n    the fixture name as parameter. A full list of available fixtures can be\n    found with the ``pytest --fixtures`` command.\n\n 4. The test uses a simple ``assert`` to test some condition is valid.  Pytest\n    uses some magic to ensure that the errors from this are readable and easy\n    to debug.\n\n 5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It\n    starts a loop and executes a test function as coroutine. This should be\n    used for testing asyncio code. It also declares the ``event_loop``\n    fixture, which will request an ``asyncio`` event loop.\n\nBrief Introduction to Tox\n=========================\n\n`Tox <https://tox.readthedocs.io/en/latest/>`__ is a tool for automated setup\nof virtual environments for testing. While the tests can be run directly by\njust running ``pytest``, this only tests one specific python version in your\nexisting environment, which will not catch e.g. undeclared dependencies, or\nversion incompatabilities.\n\nTox environments are declared in the ``tox.ini`` file. The default\nenvironments, declared at the top, can be simply run with ``tox``. The option\n``tox -e py36,flake`` can be used to request specific environments to be run.\n\nBrief Introduction to Pytest-cov\n================================\n\nCoverage is a useful metric for testing. It measures the lines of code and\nbranches that are exercised by the tests. The higher the coverage, the more\nlikely it is that any coding errors will be caught by the tests.\n\nA brief coverage report can be generated with the ``--cov`` option to ``tox``,\nwhich will be passed on to ``pytest``. Additionally, the very useful HTML\nreport can be generated with ``--cov --cov-report=html``, which contains a\nbrowsable copy of the source code, annotated with coverage information for each\nline.\n"
  },
  {
    "path": "readthedocs/developing/tips-for-porting-the-project.rst",
    "content": "============================\nTips for Porting the Project\n============================\n\n\nIf you're going to use the code on this repository to guide you, please\nbe kind and don't forget to mention it helped you!\n\nYou should start by reading the source code on the `first\nrelease <https://github.com/LonamiWebs/Telethon/releases/tag/v0.1>`__ of\nthe project, and start creating a ``MTProtoSender``. Once this is made,\nyou should write by hand the code to authenticate on the Telegram's\nserver, which are some steps required to get the key required to talk to\nthem. Save it somewhere! Then, simply mimic, or reinvent other parts of\nthe code, and it will be ready to go within a few days.\n\nGood luck!\n"
  },
  {
    "path": "readthedocs/developing/understanding-the-type-language.rst",
    "content": "===============================\nUnderstanding the Type Language\n===============================\n\n\n`Telegram's Type Language <https://core.telegram.org/mtproto/TL>`__\n(also known as TL, found on ``.tl`` files) is a concise way to define\nwhat other programming languages commonly call classes or structs.\n\nEvery definition is written as follows for a Telegram object is defined\nas follows:\n\n    ``name#id argument_name:argument_type = CommonType``\n\nThis means that in a single line you know what the ``TLObject`` name is.\nYou know it's unique ID, and you know what arguments it has. It really\nisn't that hard to write a generator for generating code to any\nplatform!\n\nThe generated code should also be able to *encode* the ``TLObject`` (let\nthis be a request or a type) into bytes, so they can be sent over the\nnetwork. This isn't a big deal either, because you know how the\n``TLObject``\\ 's are made, and how the types should be serialized.\n\nYou can either write your own code generator, or use the one this\nlibrary provides, but please be kind and keep some special mention to\nthis project for helping you out.\n\nThis is only a introduction. The ``TL`` language is not *that* easy. But\nit's not that hard either. You're free to sniff the\n``telethon_generator/`` files and learn how to parse other more complex\nlines, such as ``flags`` (to indicate things that may or may not be\nwritten at all) and ``vector``\\ 's.\n"
  },
  {
    "path": "readthedocs/examples/chats-and-channels.rst",
    "content": "===============================\nWorking with Chats and Channels\n===============================\n\n\n.. note::\n\n    These examples assume you have read :ref:`full-api`.\n\n.. contents::\n\n\nJoining a chat or channel\n=========================\n\nNote that :tl:`Chat` are normal groups, and :tl:`Channel` are a\nspecial form of :tl:`Chat`, which can also be super-groups if\ntheir ``megagroup`` member is `True`.\n\n\nJoining a public channel\n========================\n\nOnce you have the :ref:`entity <entities>` of the channel you want to join\nto, you can make use of the :tl:`JoinChannelRequest` to join such channel:\n\n.. code-block:: python\n\n    from telethon.tl.functions.channels import JoinChannelRequest\n    await client(JoinChannelRequest(channel))\n\n    # In the same way, you can also leave such channel\n    from telethon.tl.functions.channels import LeaveChannelRequest\n    await client(LeaveChannelRequest(input_channel))\n\n\nFor more on channels, check the `channels namespace`__.\n\n\n__ https://tl.telethon.dev/methods/channels/index.html\n\n\nJoining a private chat or channel\n=================================\n\nIf all you have is a link like this one:\n``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have\nenough information to join! The part after the\n``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this\nexample, is the ``hash`` of the chat or channel. Now you can use\n:tl:`ImportChatInviteRequest` as follows:\n\n.. code-block:: python\n\n    from telethon.tl.functions.messages import ImportChatInviteRequest\n    updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))\n\n\nAdding someone else to such chat or channel\n===========================================\n\nIf you don't want to add yourself, maybe because you're already in,\nyou can always add someone else with the :tl:`AddChatUserRequest`, which\nuse is very straightforward, or :tl:`InviteToChannelRequest` for channels:\n\n.. code-block:: python\n\n    # For normal chats\n    from telethon.tl.functions.messages import AddChatUserRequest\n\n    # Note that ``user_to_add`` is NOT the name of the parameter.\n    # It's the user you want to add (``user_id=user_to_add``).\n    await client(AddChatUserRequest(\n        chat_id,\n        user_to_add,\n        fwd_limit=10  # Allow the user to see the 10 last messages\n    ))\n\n    # For channels (which includes megagroups)\n    from telethon.tl.functions.channels import InviteToChannelRequest\n\n    await client(InviteToChannelRequest(\n        channel,\n        [users_to_add]\n    ))\n\nNote that this method will only really work for friends or bot accounts.\nTrying to mass-add users with this approach will not work, and can put both\nyour account and group to risk, possibly being flagged as spam and limited.\n\n\nChecking a link without joining\n===============================\n\nIf you don't need to join but rather check whether it's a group or a\nchannel, you can use the :tl:`CheckChatInviteRequest`, which takes in\nthe hash of said channel or group.\n\n\nIncreasing View Count in a Channel\n==================================\n\nIt has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and\nwhile I don't understand why so many people ask this, the solution is to\nuse :tl:`GetMessagesViewsRequest`, setting ``increment=True``:\n\n.. code-block:: python\n\n\n    # Obtain `channel' through dialogs or through client.get_entity() or anyhow.\n    # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list.\n\n    await client(GetMessagesViewsRequest(\n        peer=channel,\n        id=msg_ids,\n        increment=True\n    ))\n\n\nNote that you can only do this **once or twice a day** per account,\nrunning this in a loop will obviously not increase the views forever\nunless you wait a day between each iteration. If you run it any sooner\nthan that, the views simply won't be increased.\n\n__ https://github.com/LonamiWebs/Telethon/issues/233\n__ https://github.com/LonamiWebs/Telethon/issues/305\n__ https://github.com/LonamiWebs/Telethon/issues/409\n__ https://github.com/LonamiWebs/Telethon/issues/447\n"
  },
  {
    "path": "readthedocs/examples/users.rst",
    "content": "=====\nUsers\n=====\n\n\n.. note::\n\n    These examples assume you have read :ref:`full-api`.\n\n.. contents::\n\n\nRetrieving full information\n===========================\n\nIf you need to retrieve the bio, biography or about information for a user\nyou should use :tl:`GetFullUser`:\n\n\n.. code-block:: python\n\n    from telethon.tl.functions.users import GetFullUserRequest\n\n    full = await client(GetFullUserRequest(user))\n    # or even\n    full = await client(GetFullUserRequest('username'))\n\n    bio = full.full_user.about\n\n\nSee :tl:`UserFull` to know what other fields you can access.\n\n\nUpdating your name and/or bio\n=============================\n\nThe first name, last name and bio (about) can all be changed with the same\nrequest. Omitted fields won't change after invoking :tl:`UpdateProfile`:\n\n.. code-block:: python\n\n    from telethon.tl.functions.account import UpdateProfileRequest\n\n    await client(UpdateProfileRequest(\n        about='This is a test from Telethon'\n    ))\n\n\nUpdating your username\n======================\n\nYou need to use :tl:`account.UpdateUsername`:\n\n.. code-block:: python\n\n    from telethon.tl.functions.account import UpdateUsernameRequest\n\n    await client(UpdateUsernameRequest('new_username'))\n\n\nUpdating your profile photo\n===========================\n\nThe easiest way is to upload a new file and use that as the profile photo\nthrough :tl:`UploadProfilePhoto`:\n\n\n.. code-block:: python\n\n    from telethon.tl.functions.photos import UploadProfilePhotoRequest\n\n    await client(UploadProfilePhotoRequest(\n        await client.upload_file('/path/to/some/file')\n    ))\n"
  },
  {
    "path": "readthedocs/examples/word-of-warning.rst",
    "content": "=================\nA Word of Warning\n=================\n\nFull API is **not** how you are intended to use the library. You **should**\nalways prefer the :ref:`client-ref`. However, not everything is implemented\nas a friendly method, so full API is your last resort.\n\nIf you select a method in :ref:`client-ref`, you will most likely find an\nexample for that method. This is how you are intended to use the library.\n\nFull API **will** break between different minor versions of the library,\nsince Telegram changes very often. The friendly methods will be kept\ncompatible between major versions.\n\nIf you need to see real-world examples, please refer to the\n`wiki page of projects using Telethon <https://github.com/LonamiWebs/Telethon/wiki/Projects-using-Telethon>`__.\n"
  },
  {
    "path": "readthedocs/examples/working-with-messages.rst",
    "content": "=====================\nWorking with messages\n=====================\n\n.. note::\n\n    These examples assume you have read :ref:`full-api`.\n\nThis section has been `moved to the wiki`_, where it can be easily edited as new\nfeatures arrive and the API changes. Please refer to the linked page to learn how\nto send spoilers, custom emoji, stickers, react to messages, and more things.\n\n.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages\n"
  },
  {
    "path": "readthedocs/index.rst",
    "content": "========================\nTelethon's Documentation\n========================\n\n.. code-block:: python\n\n   from telethon.sync import TelegramClient, events\n\n   with TelegramClient('name', api_id, api_hash) as client:\n      client.send_message('me', 'Hello, myself!')\n      print(client.download_profile_photo('me'))\n\n      @client.on(events.NewMessage(pattern='(?i).*Hello'))\n      async def handler(event):\n         await event.reply('Hey!')\n\n      client.run_until_disconnected()\n\n\n* Are you new here? Jump straight into :ref:`installation`!\n* Looking for the method reference? See :ref:`client-ref`.\n* Did you upgrade the library? Please read :ref:`changelog`.\n* Used Telethon before v1.0? See :ref:`compatibility-and-convenience`.\n* Coming from Bot API or want to create new bots? See :ref:`botapi`.\n* Need the full API reference? https://tl.telethon.dev/.\n\n\nWhat is this?\n-------------\n\nTelegram is a popular messaging application. This library is meant\nto make it easy for you to write Python programs that can interact\nwith Telegram. Think of it as a wrapper that has already done the\nheavy job for you, so you can focus on developing an application.\n\n\nHow should I use the documentation?\n-----------------------------------\n\nIf you are getting started with the library, you should follow the\ndocumentation in order by pressing the \"Next\" button at the bottom-right\nof every page.\n\nYou can also use the menu on the left to quickly skip over sections.\n\n.. toctree::\n    :hidden:\n    :caption: First Steps\n\n    basic/installation\n    basic/signing-in\n    basic/quick-start\n    basic/updates\n    basic/next-steps\n\n.. toctree::\n    :hidden:\n    :caption: Quick References\n\n    quick-references/faq\n    quick-references/client-reference\n    quick-references/events-reference\n    quick-references/objects-reference\n\n.. toctree::\n    :hidden:\n    :caption: Concepts\n\n    concepts/strings\n    concepts/entities\n    concepts/chats-vs-channels\n    concepts/updates\n    concepts/sessions\n    concepts/full-api\n    concepts/errors\n    concepts/botapi-vs-mtproto\n    concepts/asyncio\n\n.. toctree::\n    :hidden:\n    :caption: Full API Examples\n\n    examples/word-of-warning\n    examples/chats-and-channels\n    examples/users\n    examples/working-with-messages\n\n.. toctree::\n    :hidden:\n    :caption: Developing\n\n    developing/philosophy.rst\n    developing/test-servers.rst\n    developing/project-structure.rst\n    developing/coding-style.rst\n    developing/testing.rst\n    developing/understanding-the-type-language.rst\n    developing/tips-for-porting-the-project.rst\n    developing/telegram-api-in-other-languages.rst\n\n.. toctree::\n    :hidden:\n    :caption: Miscellaneous\n\n    misc/changelog\n    misc/compatibility-and-convenience\n\n.. toctree::\n    :hidden:\n    :caption: Telethon Modules\n\n    modules/client\n    modules/events\n    modules/custom\n    modules/utils\n    modules/errors\n    modules/sessions\n    modules/network\n    modules/helpers\n"
  },
  {
    "path": "readthedocs/make.bat",
    "content": "@ECHO OFF\n\npushd %~dp0\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset SOURCEDIR=.\nset BUILDDIR=_build\nset SPHINXPROJ=Telethon\n\nif \"%1\" == \"\" goto help\n\n%SPHINXBUILD% >NUL 2>NUL\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\ngoto end\n\n:help\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\n\n:end\npopd\n"
  },
  {
    "path": "readthedocs/misc/changelog.rst",
    "content": ".. _changelog:\n\n\n===========================\nChangelog (Version History)\n===========================\n\n\nThis page lists all the available versions of the library,\nin chronological order. You should read this when upgrading\nthe library to know where your code can break, and where\nit can take advantage of new goodies!\n\n.. contents:: List of All Versions\n\nNew layer (v1.42)\n=================\n\n+------------------------+\n| Scheme layer used: 216 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=214&to=216>`__.\n\nBug fixes\n~~~~~~~~~\n\n* Fixed support for Python 3.14.\n* Removed potential misuse when downloading files using inferred path.\n\n\nNew layer (v1.41)\n=================\n\n+------------------------+\n| Scheme layer used: 214 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=201&to=214>`__.\n\nAdditions\n~~~~~~~~~\n\n* ``send_as`` and ``effect`` added to ``send_file``.\n* ``mime_type`` added to ``send_file``.\n* ``tg-emoji`` now works with HTML parse mode.\n* Clicking a button now lets you choose whether to open the browser.\n* Persistent and placeholder buttons.\n* More separate RPC error classes.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Update entities should now be cached to session more reliably.\n* ``utils.get_display_name`` now handles more types.\n* Improved some type hints.\n* Reply properties for stories now behave as expected.\n* ``isal`` can now be used as an optional dependency for faster compression.\n* Potential slight speed improvements to deserialization.\n\nBug fixes\n~~~~~~~~~\n\n* Library was not saving update sequence from certain updates.\n* Input peer cache should no longer overwrite valid data with min peers.\n* Spoiler for input photos and documents was not being respected.\n\nNew layer (v1.40)\n=================\n\n+------------------------+\n| Scheme layer used: 201 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=199&to=201>`__.\n\nAdditions\n~~~~~~~~~\n\n* ``send_as`` and ``effect`` added to ``send_message`` and related methods.\n* :tl:`MessageMediaGeoLive` is now recognized for auto-input conversion.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Improved wording when using a likely unintended session file.\n* Improved behaviour for matching Markdown links.\n* A truly clean update-state is now fetched upon login. This was most notably important for bots.\n* Time offset is now updated more reliably after connecting. This should fix legitimate \"message too old/new\" issues.\n\nBug fixes\n~~~~~~~~~\n\n* :tl:`ChannelParticipantLeft` is now skipped in ``iter_participants``.\n* ``spoiler`` flag was lost on :tl:`MessageMediaPhoto` auto-input conversion.\n* :tl:`KeyboardButtonCopy` is now recognized as an inline button.\n* Downloading web-documents should now work again. Note that this still fetches the file from the original server.\n\n\nNew layer (v1.39)\n=================\n\n+------------------------+\n| Scheme layer used: 199 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=193&to=199>`__.\n\nAdditions\n~~~~~~~~~\n\n* ``drop_media_captions`` added to ``forward_messages``, and documented together with ``drop_author``.\n* :tl:`InputMediaDocumentExternal` is now recognized when sending albums.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``receive_updates=False`` now covers more cases, however, Telegram is still free to ignore it.\n* Better type-hints in several methods.\n* Markdown parsing of inline links should cover more cases.\n* ``range`` is now considered \"list-like\" and can be used on e.g. ``ids`` parameters.\n\nBug fixes\n~~~~~~~~~\n\n* Session is now saved after setting the DC.\n* Fixed rare crash in entity cache handling when iterating through dialogs.\n* Fixed IOError that could occur during automatic resizing of some photos.\n\n\nNew layer (v1.38)\n=================\n\n+------------------------+\n| Scheme layer used: 193 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=188&to=193>`__.\n\nBug fixes\n~~~~~~~~~\n\n* Formatting entities misbehaved with albums.\n* Sending a Message object with a file did not use the new file.\n\n\nNew layer (v1.37)\n=================\n\n+------------------------+\n| Scheme layer used: 188 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=181&to=188>`__.\n\nAdditions\n~~~~~~~~~\n\n* Support for CDN downloads should be back. Telethon still prefers no CDN by default.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``FloodWaitPremium`` should now be handled like any other floodwaits.\n\nBug fixes\n~~~~~~~~~\n\n* Fixed edge-case when using ``get_messages(..., reverse=True)``.\n* ``ConnectionError`` when using proxies should be raised properly.\n\n\nNew layer (v1.36)\n=================\n\n+------------------------+\n| Scheme layer used: 181 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=178&to=181>`__.\n\nBug fixes\n~~~~~~~~~\n\n* Certain updates, such as :tl:`UpdateBotStopped`, should now be processed reliably.\n\n\nNew layer (v1.35)\n=================\n\n+------------------------+\n| Scheme layer used: 178 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=173&to=178>`__.\n\nAdditions\n~~~~~~~~~\n\n* ``drop_author`` parameter now exposed in ``forward_messages``.\n\nEnhancements\n~~~~~~~~~~~~\n\n* \"Custom secret support\" should work with ``TcpMTProxy``.\n* Some type hints should now be more accurate.\n\nBug fixes\n~~~~~~~~~\n\n* Session path couldn't be a ``pathlib.Path`` or ``None``.\n* Python versions older than 3.9 should now be supported again.\n* Readthedocs should hopefully build the v1 documentation again.\n\n\nNew layer (v1.34)\n=================\n\n+------------------------+\n| Scheme layer used: 173 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=167&to=173>`__.\n\nAdditions\n~~~~~~~~~\n\n* ``reply_to_chat`` and ``reply_to_sender`` are now in ``Message``.\n  This is useful when you lack access to the chat, but Telegram still included some basic information.\n\nBug fixes\n~~~~~~~~~\n\n* ``parse_mode`` with a custom instance containing both ``parse`` and ``unparse`` should now work.\n* Parsing and unparsing message entities should now behave better in certain corner-cases.\n\n\nNew layer (v1.33)\n=================\n\n+------------------------+\n| Scheme layer used: 167 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=166&to=167>`__.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``webbrowser`` is now imported conditionally, to support niche environments.\n* Library should now retry on the suddenly-common ``TimedOutError``.\n\nBug fixes\n~~~~~~~~~\n\n* Sending photos which were automatically resized should work again (included in the v1.32 series).\n\n\nNew layer (v1.32)\n=================\n\n+------------------------+\n| Scheme layer used: 166 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=165&to=166>`__.\n\nThis enables you to use custom languages in preformatted blocks using HTML:\n\n.. code-block:: html\n\n  <pre>\n    <code class='language-python'>from telethon import TelegramClient</code>\n  </pre>\n\nNote that Telethon v1's markdown is a custom format and won't support language tags.\nIf you want to set a custom language, you have to use HTML or a custom formatter.\n\n\nDropped imghdr support (v1.31)\n==============================\n\n+------------------------+\n| Scheme layer used: 165 |\n+------------------------+\n\nThis release contains a breaking change in preparation for Python 3.12.\nIf you were sending photos from in-memory ``bytes`` or ``BytesIO`` containing images,\nyou should now use ``BytesIO`` and set the ``.name`` property to a dummy name.\nThis will allow Telethon to detect the correct extension (and file type).\n\n.. code-block:: python\n\n    # before\n    image_data = b'...'\n    client.send_file(chat, image_data)\n\n    # after\n    from io import BytesIO\n    image_data = BytesIO(b'...')\n    image_data.name = 'a.jpg'  # any name, only the extension matters\n    client.send_file(chat, image_data)\n\n\nBug fixes\n~~~~~~~~~\n\n* Code generation wasn't working under PyPy.\n* Obtaining markdown or HTML from message text could produce unexpected results sometimes.\n* Other fixes for bugs from the previous version, which were already fixed in patch versions.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* ``imghdr`` is deprecated in newer Python versions, so Telethon no longer uses it.\n  This means there might be some cases where Telethon fails to infer the file extension for buffers containing images.\n  If you were relying on this, add ``.name = 'a.jpg'`` (or other extension) to the ``BytesIO`` buffers you upload.\n\nLayer bump and small changes (v1.30)\n====================================\n\n+------------------------+\n| Scheme layer used: 162 |\n+------------------------+\n\nSome of the bug fixes were already present in patch versions of ``v1.29``, but\nthe new layer necessitated a minor bump.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Removed client-side checks for editing messages.\n  This only affects ``Message.edit``, as ``client.edit_message`` already had\n  no checks.\n* Library should not understand more server-side errors during update handling\n  which should reduce crashes.\n* Client-side image compression should behave better now.\n\nBug fixes\n~~~~~~~~~\n\n* Some updates such as ``UpdateChatParticipant`` were being missed due to the\n  order in which Telegram sent them. The library now more carefully checks for\n  the sequence and pts contained in them to avoid dropping them.\n* Fixed ``is_inline`` check for :tl:`KeyboardButtonWebView`.\n* Fixed some issues getting entity from cache by ID.\n* ``reply_to`` should now work when sending albums.\n\n\nMore bug fixing (v1.29)\n=======================\n\n+------------------------+\n| Scheme layer used: 160 |\n+------------------------+\n\nThis layer introduces the necessary raw API methods to work with stories.\n\nThe library is aiming to be \"feature-frozen\" for as long as v1 is active,\nso friendly client methods are not implemented, but example code to use\nstories can be found in the GitHub wiki of the project.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Removed client-side checks for methods dealing with chat permissions.\n  In particular, this means you can now ban channels.\n* Improved some error messages and added new classes for more RPC errors.\n* The client-side check for valid usernames has been loosened, so that\n  very short premium usernames are no longer considered invalid.\n\nBug fixes\n~~~~~~~~~\n\n* Attempting to download a thumbnail from documnets without one would fail,\n  rather than do nothing (since nothing can be downloaded if there is no thumb).\n* More errors are caught in the update handling loop.\n* HTML ``.text`` should now \"unparse\" any message contents correctly.\n* Fixed some problems related to logging.\n* ``comment_to`` should now work as expected with albums.\n* ``asyncio.CancelledError`` should now correctly propagate from the update loop.\n* Removed some absolute imports in favour of relative imports.\n* ``UserUpdate.last_seen`` should now behave correctly.\n* Fixed a rare ``ValueError`` during ``connect`` if the session cache was bad.\n\n\nNew Layer and housekeeping (v1.28)\n==================================\n\n+------------------------+\n| Scheme layer used: 155 |\n+------------------------+\n\nPlenty of stale issues closed, as well as improvements for some others.\n\nAdditions\n~~~~~~~~~\n\n* New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor.\n  This should help a bit in keeping memory usage in check.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``progress_callback`` is now called when dealing with albums. See the\n  documentation on `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`\n  for details.\n* Update state and entities are now periodically saved, so that the information\n  isn't lost in the case of crash or unexpected script terminations. You should\n  still be calling ``disconnect`` or using the context-manager, though.\n* The client should no longer unnecessarily call ``get_me`` every time it's started.\n\nBug fixes\n~~~~~~~~~\n\n* Messages obtained via raw API could not be used in ``forward_messages``.\n* ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details.\n  It is no longer possible for third-party applications, such as those made with\n  Telethon, to use those features.\n* ``events.ChatAction`` should now work in more cases in groups with hidden members.\n* Errors that occur at the connection level should now be properly propagated, so that\n  you can actually have a chance to handle them.\n* Update handling should be more resilient.\n* ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``.\n* In patch ``v1.28.2``, :tl:`InputBotInlineMessageID64` can now be used\n  to edit inline messages.\n\n\n.. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050\n\n\nNew Layer and some Bug fixes (v1.27)\n====================================\n\n+------------------------+\n| Scheme layer used: 152 |\n+------------------------+\n\nBug fixes\n~~~~~~~~~\n\n* When the account is logged-out, the library should now correctly propagate\n  an error through ``run_until_disconnected`` to let you handle it.\n* The library no longer uses ``asyncio.get_event_loop()`` in newer Python\n  versions, which should get rid of some deprecation warnings.\n* It could happen that bots would receive messages sent by themselves,\n  very often right after they deleted a message. This should happen far\n  less often now (but might still happen with unlucky timings).\n* Maximum photo size for automatic image resizing is now larger.\n* The initial request is now correctly wrapped in ``invokeWithoutUpdates``\n  when updates are disabled after constructing the client instance.\n* Using a ``pathlib.Path`` to download contacts and web documents should\n  now work correctly.\n\nNew Layer and some Bug fixes (v1.26)\n====================================\n\n+------------------------+\n| Scheme layer used: 149 |\n+------------------------+\n\nThis new layer includes things such as emoji status, more admin log events,\nforum topics and message reactions, among other things. You can access these\nusing raw API. It also contains a few bug fixes.\n\nThese were fixed in the v1.25 series:\n\n* ``client.edit_admin`` did not work on small group chats.\n* ``client.get_messages`` could stop early in some channels.\n* ``client.download_profile_photo`` now should work even if ``User.min``.\n* ``client.disconnect`` should no longer hang when being called from within\n  an event handlers.\n* ``client.get_dialogs`` now initializes the update state for channels.\n* The message sender should not need to be fetched in more cases.\n* Lowered the severity of some log messages to be less spammy.\n\nThese are new to v1.26.0:\n\n* Layer update.\n* New documented RPC errors.\n* Sometimes the first message update to a channel could be missed if said\n  message was read immediately.\n* ``client.get_dialogs`` would fail when the total count evenly divided\n  the chunk size of 100.\n* ``client.get_messages`` could get stuck during a global search.\n* Potentially fixed some issues when sending certain videos.\n* Update handling should be more resilient.\n* The client should handle having its auth key destroyed more gracefully.\n* Fixed some issues when logging certain messages.\n\n\nBug fixes (v1.25.1)\n===================\n\nThis version should fix some of the problems that came with the revamped\nupdate handling.\n\n* Some inline URLs were not parsing correctly with markdown.\n* ``events.Raw`` was handling :tl:`UpdateShort` which it shouldn't do.\n* ``events.Album`` should now work again.\n* ``CancelledError`` was being incorrectly logged as a fatal error.\n* Some fixes to update handling primarly aimed for bot accounts.\n* Update handling now can deal with more errors without crashing.\n* Unhandled errors from update handling will now be propagated through\n  ``client.run_until_disconnected``.\n* Invite links with ``+`` are now recognized.\n* Added new known RPC errors.\n* ``telethon.types`` could not be used as a module.\n* 0-length message entities are now stripped to avoid errors.\n* ``client.send_message`` was not returning a message with ``reply_to``\n  in some cases.\n* ``aggressive`` in ``client.iter_participants`` now does nothing (it did\n  not really work anymore anyway, and this should prevent other errors).\n* ``client.iter_participants`` was failing in some groups.\n* Text with HTML URLs could sometimes fail to parse.\n* Added a hard timeout during disconnect in order to prevent the program\n  from freezing.\n\nPlease be sure to report issues with update handling if you still encounter\nsome errors!\n\n\nUpdate handling overhaul (v1.25)\n================================\n\n+------------------------+\n| Scheme layer used: 144 |\n+------------------------+\n\nI had plans to release v2 way earlier, but my motivation drained off, so that\ndidn't happen. The reason for another v1 release is that there was a clear\nneed to fix some things regarding update handling (which were present in v2).\nI did not want to make this release. But with the release date for v2 still\nbeing unclear, I find it necessary to release another v1 version. I apologize\nfor the delay (I should've done this a lot sooner but didn't because in my\nhead I would've pushed through and finished v2, but I underestimated how much\nwork that was and I probably experienced burn-out).\n\nI still don't intend to make new additions to the v1 series (beyond updating\nthe Telegram layer being used). I still have plans to finish v2 some day.\nBut in the meantime, new features, such as reactions, will have to be used\nthrough raw API.\n\nThis update also backports the update overhaul from v2. If you experience\nissues with updates, please report them on the GitHub page for the project.\nHowever, this new update handling should be more reliable, and ``catch_up``\nshould actually work properly.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* In order for ``catch_up`` to work (new flag in the ``TelegramClient``\n  constructor), sessions need to impleemnt the new ``get_update_states``.\n  Third-party session storages won't have this implemented by the time\n  this version released, so ``catch_up`` may not work with those.\n\nRushed release to fix login (v1.24)\n===================================\n\n+------------------------+\n| Scheme layer used: 133 |\n+------------------------+\n\nThis is a rushed release. It contains a layer recent enough to not fail with\n``UPDATE_APP_TO_LOGIN``, but still not the latest, to avoid breaking more\nthan necessary.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The biggest change is user identifiers (and chat identifiers, and others)\n  **now use up to 64 bits**, rather than 32. If you were storing them in some\n  storage with fixed size, you may need to update (such as database tables\n  storing only integers).\n\nThere have been other changes which I currently don't have the time to document.\nYou can refer to the following link to see them early:\nhttps://github.com/LonamiWebs/Telethon/compare/v1.23.0...v1.24.0\n\n\nNew schema and bug fixes (v1.23)\n================================\n\n+------------------------+\n| Scheme layer used: 130 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=129&to=130>`__.\n\nEnhancements\n~~~~~~~~~~~~\n\n* `client.pin_message() <telethon.client.messages.MessageMethods.pin_message>`\n  can now pin on a single side in PMs.\n* Iterating participants should now be less expensive floodwait-wise.\n\nBug fixes\n~~~~~~~~~\n\n* The QR login URL was being encoded incorrectly.\n* ``force_document`` was being ignored in inline queries for document.\n* ``manage_call`` permission was accidentally set to ``True`` by default.\n\nNew schema and bug fixes (v1.22)\n================================\n\n+------------------------+\n| Scheme layer used: 129 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=125&to=129>`__.\n\nEnhancements\n~~~~~~~~~~~~\n\n* You can now specify a message in `client.get_stats()\n  <telethon.client.chats.ChatMethods.get_stats>`.\n* Metadata extraction from audio files with ``hachoir`` now recognises \"artist\".\n* Get default chat permissions by not supplying a user to `client.get_permissions()\n  <telethon.client.chats.ChatMethods.get_permissions>`.\n* You may now use ``thumb`` when editing messages.\n\nBug fixes\n~~~~~~~~~\n\n* Fixes regarding bot markup in messages.\n* Gracefully handle :tl:`ChannelForbidden` in ``get_sender``.\n\nAnd from v1.21.1:\n\n* ``file.width`` and ``.height`` was not working correctly in photos.\n* Raw API was mis-interpreting ``False`` values on boolean flag parameters.\n\nNew schema and QoL improvements (v1.21)\n=======================================\n\n+------------------------+\n| Scheme layer used: 125 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=124&to=125>`__.\n\nNot many changes in this release, mostly the layer change. Lately quite a few\npeople have been reporting `TypeNotFoundError`, which occurs when the server\n**sends types that it shouldn't**. This can happen when Telegram decides to\nadd a new, incomplete layer, and then they change the layer without bumping\nthe layer number (so some constructor IDs no longer match and the error\noccurs). This layer change\n`should fix it <https://github.com/LonamiWebs/Telethon/issues/1724>`__.\n\nAdditions\n~~~~~~~~~\n\n* `Message.click() <telethon.tl.custom.message.Message.click>` now supports\n  a ``password`` parameter, needed when doing things like changing the owner\n  of a bot via `@BotFather <https://t.me/BotFather>`__.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``tgcrypto`` will now be used for encryption when installed.\n\nBug fixes\n~~~~~~~~~\n\n* `Message.edit <telethon.tl.custom.message.Message.edit>` wasn't working in\n  your own chat on events other than ``NewMessage``.\n* `client.delete_dialog() <telethon.client.dialogs.DialogMethods.delete_dialog>`\n  was not working on chats.\n* ``events.UserUpdate`` should now handle channels' typing status.\n* :tl:`InputNotifyPeer` auto-cast should now work on other ``TLObject``.\n* For some objects, ``False`` was not correctly serialized.\n\n\nNew schema and QoL improvements (v1.20)\n=======================================\n\n+------------------------+\n| Scheme layer used: 124 |\n+------------------------+\n\n`View new and changed raw API methods <https://diff.telethon.dev/?from=122&to=124>`__.\n\nA bit late to the party, but Telethon now offers a convenient way to comment\non channel posts. It works very similar to ``reply_to``:\n\n.. code-block:: python\n\n    client.send_message(channel, 'Great update!', comment_to=1134)\n\nThis code will leave a comment to the channel post with ID ``1134`` in\n``channel``.\n\nIn addition, the library now logs warning or error messages to ``stderr`` by\ndefault! You no longer should be left wondering \"why isn't my event handler\nworking\" if you forgot to configure logging. It took so long for this change\nto arrive because nobody noticed that Telethon was using a\n``logging.NullHandler`` when it really shouldn't have.\n\nIf you want the old behaviour of no messages being logged, you can configure\n`logging` to ``CRITICAL`` severity:\n\n.. code-block:: python\n\n    import logging\n    logging.basicConfig(level=logging.CRITICAL)\n\nThis is not considered a breaking change because ``stderr`` should only be\nused for logging purposes, not to emit information others may consume (use\n``stdout`` for that).\n\nAdditions\n~~~~~~~~~\n\n* New ``comment_to`` parameter in `client.send_message()\n  <telethon.client.messages.MessageMethods.send_message>`, and\n  `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`\n  to comment on channel posts.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``utils.resolve_invite_link`` handles the newer link format.\n* Downloading files now retries once on `TimeoutError`, which has been\n  happening recently. It is not guaranteed to work, but it should help.\n* Sending albums of photo URLs is now supported.\n* EXIF metadata is respected when automatically resizing photos, so the\n  orientation information should no longer be lost.\n* Downloading a thumbnail by index should now use the correct size ordering.\n\nBug fixes\n~~~~~~~~~\n\n* Fixed a `KeyError` on certain cases with ``Conversation``.\n* Thumbnails should properly render on more clients. Installing ``hachoir``\n  may help.\n* Message search was broken when using a certain combination of parameters.\n* ``utils.resolve_id`` was misbehaving with some identifiers.\n* Fix ``TypeNotFoundError`` was not being propagated, causing deadlocks.\n* Invoking multiple requests at once with ``ordered=True`` was deadlocking.\n\n\nNew raw API call methods (v1.19)\n================================\n\n+------------------------+\n| Scheme layer used: 122 |\n+------------------------+\n\nTelegram has had group calls for some weeks now. This new version contains the\nraw API methods needed to initiate and manage these group calls, however, the\nlibrary will likely **not offer ways to stream audio directly**.\n\nTelethon's focus is being an asyncio-based, pure-Python implementation to\ninteract with Telegram's API. Streaming audio is beyond the current scope of\nthe project and would be a big undertaking.\n\nHowever, that doesn't mean calls are not possible with Telethon. If you want\nto help design a Python library to perform audio calls, which can then be used\nwith Telethon (so you can use Telethon + that new library to perform calls\nwith Telethon), please refer to `@pytgcallschat <https://t.me/pytgcallschat/>`__\nand join the relevant chat to discuss and help with the implementation!\n\nThe above message was also `posted in the official Telegram group\n<https://t.me/TelethonChat/284717>`__, if you wish to discuss it further.\n\nWith that out of the way, let's list the additions and bug fixes in this\nrelease:\n\nAdditions\n~~~~~~~~~\n\n* New ``has_left`` property for user permissions on `client.get_permissions()\n  <telethon.client.chats.ChatMethods.get_permissions>`.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Updated documentation and list of known RPC errors.\n* The library now treats a lack of ping responses as a network error.\n* `client.kick_participant() <telethon.client.chats.ChatMethods.kick_participant>`\n  now returns the service message about the user being kicked, so you can\n  delete it.\n\nBug fixes\n~~~~~~~~~\n\n* When editing inline messages, the text parameter is preferred if provided.\n* Additional senders are unconditionally disconnected when disconnecting the\n  main client, which should reduce the amount of asyncio warnings.\n* Automatic reconnection with no retries was failing.\n* :tl:`PhotoPathSize` is now ignored when determining a download size, since\n  this \"size\" is not a JPEG thumbnail unlike the rest.\n* `events.ChatAction <telethon.events.chataction.ChatAction>` should misbehave\n  less.\n\n\nNew layer and QoL improvements (v1.18)\n======================================\n\n+------------------------+\n| Scheme layer used: 120 |\n+------------------------+\n\nMostly fixes, and added some new things that can be done in this new layer.\n\nFor proxy users, a pull request was merged that will use the ``python-socks``\nlibrary when available for proxy support. This library natively supports\n`asyncio`, so it should work better than the old ``pysocks``. ``pysocks`` will\nstill be used if the new library is not available, and both will be handled\ntransparently by Telethon so you don't need to worry about it.\n\nAdditions\n~~~~~~~~~\n\n* New `client.set_proxy()\n  <telethon.client.telegrambaseclient.TelegramBaseClient.set_proxy>` method\n  which lets you change the proxy without recreating the client. You will need\n  to reconnect for it to take effect, but you won't need to recreate the\n  client. This is also an external contribution.\n* New method to unpin messages `client.unpin_message()\n  <telethon.client.messages.MessageMethods.unpin_message>`.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Empty peers are excluded from the list of dialogs.\n* If the ``python-socks`` library is installed (new optional requirement), it\n  will be used instead of ``pysocks`` for proxy support. This should fix some\n  issues with proxy timeouts, because the new library natively supports\n  `asyncio`.\n* `client.send_file() <telethon.client.uploads.UploadMethods.send_file>` will\n  now group any media type, instead of sending non-image documents separatedly.\n  This lets you create music albums, for example.\n* You can now search messages with a ``from_user`` that's not a user. This is\n  a Telegram feature, we know the name isn't great, but backwards-compatibility\n  has to be kept.\n\nBug fixes\n~~~~~~~~~\n\n* Fixes related to conversation timeouts.\n* Large dates (over year 2038) now wrap around a 32-bit integer, which is the\n  only way we can represent them to Telegram. Even if \"wrong\", it makes things\n  not crash, and it's the best we can do with 32-bit dates.\n* The library was accidentally using a deprecated argument in one of its\n  friendly methods, producing a warning.\n* Improvements to the way marked IDs are parsed.\n* ``SlowModeWaitError`` floods are no longer cached.\n* Getting the buttons for a message could fail sometimes.\n* Getting the display name for \"forbidden\" chats now works.\n* Better handling of errors in some internal methods.\n\n\nChannel comments and Anonymous Admins (v1.17)\n=============================================\n\n+------------------------+\n| Scheme layer used: 119 |\n+------------------------+\n\nNew minor version, new layer change! This time is a good one to remind every\nconsumer of Python libraries that **you should always specify fixed versions\nof your dependencies**! If you're using a ``requirements.txt`` file and you\nwant to stick with the old version (or any version) for the time being, you\ncan `use the following syntax <https://pip.pypa.io/en/stable/user_guide/>`__:\n\n.. code-block:: text\n\n    telethon~=1.16.0\n\nThis will install any version compatible with the written version (so, any in\nthe ``1.16`` series). Patch releases will never break your code (and if they\ndo, it's a bug). You can also use that syntax in ``pip install``. Your code\ncan't know what new versions will look like, so saying it will work with all\nversions is a lie and will cause issues.\n\nThe reason to bring this up is that Telegram has changed things again, and\nwith the introduction of anonymous administrators and channel comments, the\nsender of a message may not be a :tl:`User`! To accomodate for this, the field\nis now a :tl:`Peer` and not `int`. As a reminder, it's always a good idea to\nuse Telethon's friendly methods and custom properties, which have a higher\nstability guarantee than accessing raw API fields.\n\nEven if you don't update, your code will still need to account for the fact\nthat the sender of a message might be one of the accounts Telegram introduced\nto preserve backwards compatibility, because this is a server-side change, so\nit's better to update and not lag behind. As it's mostly just a single person\ndriving the project on their free time, bug-fixes are not backported.\n\nThis version also updates the format of SQLite sessions (the default), so\nafter upgrading and using an old session, the session will be updated, which\nmeans trying to use it back in older versions of the library won't work.\n\nFor backwards-compatibility sake, the library has introduced the properties\n`Message.reply_to_msg_id <telethon.tl.custom.message.Message.reply_to_msg_id>`\nand `Message.to_id <telethon.tl.custom.message.Message.to_id>` that behave\nlike they did before (Telegram has renamed and changed how these fields work).\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* ``Message.from_id`` is now a :tl:`Peer`, not `int`! If you want the marked\n  sender ID (much like old behaviour), replace all uses of ``.from_id`` with\n  ``.sender_id``. This will mostly work, but of course in old and new versions\n  you have to account for the fact that this sender may no longer be a user.\n* You can no longer assign to `Message.reply_to_msg_id\n  <telethon.tl.custom.message.Message.reply_to_msg_id>` and `Message.to_id\n  <telethon.tl.custom.message.Message.to_id>` because these are now properties\n  that offer a \"view\" to the real value from a different field.\n* Answering inline queries with a ``photo`` or ``document`` will now send the\n  photo or document used in the resulting message by default. Not sending the\n  media was technically a bug, but some people may be relying on this old\n  behaviour. You can use the old behaviour with ``include_media=False``.\n\nAdditions\n~~~~~~~~~\n\n* New ``raise_last_call_error`` parameter in the client constructor to raise\n  the same error produced by the last failing call, rather than a generic\n  `ValueError`.\n* New ``formatting_entities`` parameter in `client.send_message()\n  <telethon.client.messages.MessageMethods.send_message>`, and\n  `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`\n  to bypass the parse mode and manually specify the formatting entities.\n* New `client.get_permissions() <telethon.client.chats.ChatMethods.get_permissions>`\n  method to query a participant's permissions in a group or channel. This\n  request is slightly expensive in small group chats because it has to fetch\n  the entire chat to check just a user, so use of a cache is advised.\n* `Message.click() <telethon.tl.custom.message.Message.click>` now works on\n  normal polls!\n* New ``local_addr`` parameter in the client constructor to use a specific\n  local network address when connecting to Telegram.\n* `client.inline_query() <telethon.client.bots.BotMethods.inline_query>` now\n  lets you specify the chat where the query is being made from, which some\n  bots need to provide certain functionality.\n* You can now get comments in a channel post with the ``reply_to`` parameter in\n  `client.iter_messages() <telethon.client.messages.MessageMethods.iter_messages>`.\n  Comments are messages that \"reply to\" a specific channel message, hence the\n  name (which is consistent with how Telegram's API calls it).\n\nEnhancements\n~~~~~~~~~~~~\n\n* Updated documentation and list of known errors.\n* If ``hachoir`` is available, the file metadata can now be extracted from\n  streams and in-memory bytes.\n* The default parameters used to initialize a connection now match the format\n  of those used by Telegram Desktop.\n* Specifying 0 retries will no longer cause the library to attempt to reconnect.\n* The library should now be able to reliably download very large files.\n* Global search should work more reliably now.\n* Old usernames are evicted from cache, so getting entities by cached username\n  should now be more reliable.\n* Slightly less noisy logs.\n* Stability regarding transport-level errors (transport flood, authorization\n  key not found) should be improved. In particular, you should no longer be\n  getting unnecessarily logged out.\n* Reconnection should no longer occur if the client gets logged out (for\n  example, another client revokes the session).\n\nBug fixes\n~~~~~~~~~\n\n* In some cases, there were issues when using `events.Album\n  <telethon.events.album.Album>` together with `events.Raw\n  <telethon.events.raw.Raw>`.\n* For some channels, one of their channel photos would not show up in\n  `client.iter_profile_photos() <telethon.client.chats.ChatMethods.iter_profile_photos>`.\n* In some cases, a request that failed to be sent would be forgotten, causing\n  the original caller to be \"locked\" forever for a response that would never\n  arrive. Failing requests should now consistently be automatically re-sent.\n* The library should more reliably handle certain updates with \"empty\" data.\n* Sending documents in inline queries should now work fine.\n* Manually using `client.sign_up <telethon.client.auth.AuthMethods.sign_up>`\n  should now work correctly, instead of claiming \"code invalid\".\n\nSpecial mention to some of the other changes in the 1.16.x series:\n\n* The ``thumb`` for ``download_media`` now supports both `str` and :tl:`VideoSize`.\n* Thumbnails are sorted, so ``-1`` is always the largest.\n\n\nBug Fixes (v1.16.1)\n===================\n\nThe last release added support to ``force_file`` on any media, including\nthings that were not possible before like ``.webp`` files. However, the\n``force_document`` toggle commonly used for photos was applied \"twice\"\n(one told the library to send it as a document, and then to send that\ndocument as file), which prevented Telegram for analyzing the images. Long\nstory short, sending files to the stickers bot stopped working, but that's\nbeen fixed now, and sending photos as documents include the size attribute\nagain as long as Telegram adds it.\n\nEnhancements\n~~~~~~~~~~~~\n\n* When trying to `client.start() <telethon.client.auth.AuthMethods.start>` to\n  another account if you were previously logged in, the library will now warn\n  you because this is probably not intended. To avoid the warning, make sure\n  you're logging in to the right account or logout from the other first.\n* Sending a copy of messages with polls will now work when possible.\n* The library now automatically retries on inter-dc call errors (which occur\n  when Telegram has internal issues).\n\nBug Fixes\n~~~~~~~~~\n\n* The aforementioned issue with ``force_document``.\n* Square brackets removed from IPv6 addresses. This may fix IPv6 support.\n\n\nChannel Statistics (v1.16)\n==========================\n\n+------------------------+\n| Scheme layer used: 116 |\n+------------------------+\n\nThe newest Telegram update has a new method to also retrieve megagroup\nstatistics, which can now be used with `client.get_stats()\n<telethon.client.chats.ChatMethods.get_stats>`. This way you'll be able\nto access the raw data about your channel or megagroup statistics.\n\nThe maximum file size limit has also been increased to 2GB on the server,\nso you can send even larger files.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* Besides the obvious layer change, the ``loop`` argument **is now ignored**.\n  It has been deprecated since Python 3.8 and will be removed in Python 3.10,\n  and also caused some annoying warning messages when using certain parts of\n  the library. If you were (incorrectly) relying on using a different loop\n  from the one that was set, things may break.\n\nEnhancements\n~~~~~~~~~~~~\n\n* `client.upload_file() <telethon.client.uploads.UploadMethods.upload_file>`\n  now works better when streaming files (anything that has a ``.read()``),\n  instead of reading it all into memory when possible.\n\n\nQR login (v1.15)\n================\n\n*Published at 2020/07/04*\n\n+------------------------+\n| Scheme layer used: 114 |\n+------------------------+\n\nThe library now has a friendly method to perform QR-login, as detailed in\nhttps://core.telegram.org/api/qr-login. It won't generate QR images, but it\nprovides a way for you to easily do so with any other library of your choice.\n\nAdditions\n~~~~~~~~~\n\n* New `client.qr_login() <telethon.client.auth.AuthMethods.qr_login>`.\n* `message.click <telethon.tl.custom.message.Message.click>` now lets you\n  click on buttons requesting phone or location.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Updated documentation and list of known errors.\n* `events.Album <telethon.events.album.Album>` should now handle albums from\n  different data centers more gracefully.\n* `client.download_file()\n  <telethon.client.downloads.DownloadMethods.download_file>` now supports\n  `pathlib.Path` as the destination.\n\nBug fixes\n~~~~~~~~~\n\n* No longer crash on updates received prior to logging in.\n* Server-side changes caused clicking on inline buttons to trigger a different\n  error, which is now handled correctly.\n\n\nMinor quality of life improvements (v1.14)\n==========================================\n\n*Published at 2020/05/26*\n\n+------------------------+\n| Scheme layer used: 113 |\n+------------------------+\n\nSome nice things that were missing, along with the usual bug-fixes.\n\nAdditions\n~~~~~~~~~\n\n* New `Message.dice <telethon.tl.custom.message.Message.dice>` property.\n* The ``func=`` parameter of events can now be an ``async`` function.\n\nBug fixes\n~~~~~~~~~\n\n* Fixed `client.action() <telethon.client.chats.ChatMethods.action>`\n  having an alias wrong.\n* Fixed incorrect formatting of some errors.\n* Probably more reliable detection of pin events in small groups.\n* Fixed send methods on `client.conversation()\n  <telethon.client.dialogs.DialogMethods.conversation>` were not honoring\n  cancellation.\n* Flood waits of zero seconds are handled better.\n* Getting the pinned message in a chat was failing.\n* Fixed the return value when forwarding messages if some were missing\n  and also the return value of albums.\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``.tgs`` files are now recognised as animated stickers.\n* The service message produced by `Message.pin()\n  <telethon.tl.custom.message.Message.pin>` is now returned.\n* Sending a file with `client.send_file()\n  <telethon.client.uploads.UploadMethods.send_file>` now works fine when\n  you pass an existing dice media (e.g. sending a message copy).\n* `client.edit_permissions() <telethon.client.chats.ChatMethods.edit_permissions>`\n  now has the ``embed_links`` parameter which was missing.\n\nBug Fixes (v1.13)\n=================\n\n*Published at 2020/04/25*\n\n+------------------------+\n| Scheme layer used: 112 |\n+------------------------+\n\nBug fixes and layer bump.\n\nBug fixes\n~~~~~~~~~\n\n* Passing ``None`` as the entity to `client.delete_messages()\n  <telethon.client.messages.MessageMethods.delete_messages>` would fail.\n* When downloading a thumbnail, the name inferred was wrong.\n\nBug Fixes (v1.12)\n=================\n\n*Published at 2020/04/20*\n\n+------------------------+\n| Scheme layer used: 111 |\n+------------------------+\n\nOnce again nothing major, but a few bug fixes and primarily the new layer\ndeserves a new minor release.\n\nBug fixes\n~~~~~~~~~\n\nThese were already included in the ``v1.11.3`` patch:\n\n* ``libssl`` check was failing on macOS.\n* Getting input users would sometimes fail on `events.ChatAction\n  <telethon.events.chataction.ChatAction>`.\n\nThese bug fixes are available in this release and beyond:\n\n* Avoid another occurrence of `MemoryError`.\n* Sending large files in albums would fail because it tried to cache them.\n* The ``thumb`` was being ignored when sending files from :tl:`InputFile`.\n* Fixed editing inline messages from callback queries in some cases.\n* Proxy connection is now blocking which should help avoid some errors.\n\n\nBug Fixes (v1.11)\n=================\n\n*Published at 2020/02/20*\n\n+------------------------+\n| Scheme layer used: 110 |\n+------------------------+\n\nIt has been a while since the last release, and a few bug fixes have been\nmade since then. This release includes them and updates the scheme layer.\n\nNote that most of the bug-fixes are available in the ``v1.10.10`` patch.\n\nBug fixes\n~~~~~~~~~\n\n* Fix ``MemoryError`` when casting certain media.\n* Fix `client.get_entity() <telethon.client.users.UserMethods.get_entity>`\n  on small group chats.\n* `client.delete_dialog() <telethon.client.dialogs.DialogMethods.delete_dialog>`\n  now handles deactivated chats more gracefully.\n* Sending a message with ``file=`` would ignore some of the parameters.\n* Errors are now un-pickle-able once again.\n* Fixed some issues regarding markdown and HTML (un)parsing.\n\nThe following are also present in ``v1.10.10``:\n\n* Fixed some issues with `events.Album <telethon.events.album.Album>`.\n* Fixed some issues with `client.kick_participant()\n  <telethon.client.chats.ChatMethods.kick_participant>` and\n  `client.edit_admin() <telethon.client.chats.ChatMethods.edit_admin>`.\n* Fixed sending albums and more within `client.conversation()\n  <telethon.client.dialogs.DialogMethods.conversation>`.\n* Fixed some import issues.\n* And a lot more minor stuff.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Videos can now be included when sending albums.\n* Getting updates after reconnect should be more reliable.\n* Updated documentation and added more examples.\n* More security checks during the generation of the authorization key.\n\nThe following are also present in ``v1.10.10``:\n\n* URLs like ``t.me/@username`` are now valid.\n* Auto-sleep now works for slow-mode too.\n* Improved some error messages.\n* Some internal improvements and updating.\n* `client.pin_message() <telethon.client.messages.MessageMethods.pin_message>`\n  now also works with message objects.\n* Asynchronous file descriptors are now allowed during download and upload.\n\n\nScheduled Messages (v1.10)\n==========================\n\n*Published at 2019/09/08*\n\n+------------------------+\n| Scheme layer used: 105 |\n+------------------------+\n\nYou can now schedule messages to be sent (or edited, or forwarded…) at a later\ntime, which can also work as reminders for yourself when used in your own chat!\n\n.. code-block:: python\n\n    from datetime import timedelta\n\n    # Remind yourself to walk the dog in 10 minutes (after you play with Telethon's update)\n    await client.send_message('me', 'Walk the dog',\n                              schedule=timedelta(minutes=10))\n\n    # Remind your friend tomorrow to update Telethon\n    await client.send_message(friend, 'Update Telethon!',\n                              schedule=timedelta(days=1))\n\nAdditions\n~~~~~~~~~\n\n* New `Button.auth <telethon.tl.custom.button.Button.auth>` friendly button\n  you can use to ask users to login to your bot.\n* Telethon's repository now contains ``*.nix`` expressions that you can use.\n* New `client.kick_participant() <telethon.client.chats.ChatMethods.kick_participant>`\n  method to truly kick (not ban) participants.\n* New ``schedule`` parameter in `client.send_message()\n  <telethon.client.messages.MessageMethods.send_message>`, `client.edit_message()\n  <telethon.client.messages.MessageMethods.edit_message>`, `client.forward_messages()\n  <telethon.client.messages.MessageMethods.forward_messages>` and `client.send_file()\n  <telethon.client.uploads.UploadMethods.send_file>`.\n\nBug fixes\n~~~~~~~~~\n\n* Fix calling ``flush`` on file objects which lack this attribute.\n* Fix `CallbackQuery <telethon.events.callbackquery.CallbackQuery>` pattern.\n* Fix `client.action() <telethon.client.chats.ChatMethods.action>` not returning\n  itself when used in a context manager (so the ``as`` would be `None`).\n* Fix sending :tl:`InputKeyboardButtonUrlAuth` as inline buttons.\n* Fix `client.edit_permissions() <telethon.client.chats.ChatMethods.edit_permissions>`\n  defaults.\n* Fix `Forward <telethon.tl.custom.forward.Forward>` had its ``client`` as `None`.\n* Fix (de)serialization of negative timestamps (caused by the information in some\n  sites with instant view, where the date could be very old).\n* Fix HTML un-parsing.\n* Fix ``to/from_id`` in private messages when using multiple clients.\n* Stop disconnecting from `None` (incorrect logging).\n* Fix double-read on double-connect.\n* Fix `client.get_messages() <telethon.client.messages.MessageMethods.get_messages>`\n  when being passed more than 100 IDs.\n* Fix `Message.document <telethon.tl.custom.message.Message.document>`\n  for documents coming from web-pages.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Some documentation improvements, including the TL reference.\n* Documentation now avoids ``telethon.sync``, which should hopefully be less confusing.\n* Better error messages for flood wait.\n* You can now `client.get_drafts() <telethon.client.dialogs.DialogMethods.get_drafts>`\n  for a single entity (which means you can now get a single draft from a single chat).\n* New-style file IDs now work with Telethon.\n* The ``progress_callback`` for `client.upload_file()\n  <telethon.client.uploads.UploadMethods.upload_file>` can now be an ``async def``.\n\n\nAnimated Stickers (v1.9)\n========================\n\n*Published at 2019/07/06*\n\n+------------------------+\n| Scheme layer used: 103 |\n+------------------------+\n\nWith the layer 103, Telethon is now able to send and receive animated\nstickers! These use the ``'application/x-tgsticker'`` mime-type and for\nnow, you can access its raw data, which is a gzipped JSON.\n\n\nAdditions\n~~~~~~~~~\n\n* New `events.Album <telethon.events.album.Album>` to easily receive entire albums!\n* New `client.edit_admin() <telethon.client.chats.ChatMethods.edit_admin>`\n  and `client.edit_permissions() <telethon.client.chats.ChatMethods.edit_permissions>`\n  methods to more easily manage your groups.\n* New ``pattern=`` in `CallbackQuery\n  <telethon.events.callbackquery.CallbackQuery>`.\n* New `conversation.cancel_all()\n  <telethon.tl.custom.conversation.Conversation.cancel>` method,\n  to cancel all currently-active conversations in a particular chat.\n* New `telethon.utils.encode_waveform` and `telethon.utils.decode_waveform`\n  methods as implemented by Telegram Desktop, which lets you customize how\n  voice notes will render.\n* New ``ignore_pinned`` parameter in `client.iter_dialogs()\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>`.\n* New `Message.mark_read() <telethon.tl.custom.message.Message.mark_read>`\n  method.\n* You can now use strike-through in markdown with ``~~text~~``, and the\n  corresponding HTML tags for strike-through, quotes and underlined text.\n* You can now nest entities, as in ``**__text__**``.\n\nBug fixes\n~~~~~~~~~\n\n* Fixed downloading contacts.\n* Fixed `client.iter_dialogs()\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>` missing some under\n  certain circumstances.\n* Fixed incredibly slow imports under some systems due to expensive path\n  resolution when searching for ``libssl``.\n* Fixed captions when sending albums.\n* Fixed invalid states in `Conversation\n  <telethon.tl.custom.conversation.Conversation>`.\n* Fixes to some methods in utils regarding extensions.\n* Fixed memory cycle in `Forward <telethon.tl.custom.forward.Forward>`\n  which let you do things like the following:\n\n  .. code-block:: python\n\n      original_fwd = message.forward.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd\n\n  Hopefully you didn't rely on that in your code.\n* Fixed `File.ext <telethon.tl.custom.file.File.ext>` not working on\n  unknown mime-types, despite the file name having the extension.\n* Fixed ``ids=..., reverse=True`` in `client.iter_messages()\n  <telethon.client.messages.MessageMethods.iter_messages>`.\n* Fixed `Draft <telethon.tl.custom.draft.Draft>` not being aware\n  of the entity.\n* Added missing re-exports in ``telethon.sync``.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Improved `conversation.cancel()\n  <telethon.tl.custom.conversation.Conversation.cancel>`\n  behaviour. Now you can use it from anywhere.\n* The ``progress_callback`` in `client.download_media()\n  <telethon.client.downloads.DownloadMethods.download_media>`\n  now lets you use ``async def``.\n* Improved documentation and the online\n  method reference at https://tl.telethon.dev.\n\n\nDocumentation Overhaul (v1.8)\n=============================\n\n*Published at 2019/05/30*\n\n+------------------------+\n| Scheme layer used: 100 |\n+------------------------+\n\nThe documentation has been completely reworked from the ground up,\nwith awesome new quick references such as :ref:`client-ref` to help\nyou quickly find what you need!\n\nRaw methods also warn you when a friendly variant is available, so\nthat you don't accidentally make your life harder than it has to be.\n\nIn addition, all methods in the client now are fully annotated with type\nhints! More work needs to be done, but this should already help a lot when\nusing Telethon from any IDEs.\n\nYou may have noticed that the patch versions between ``v1.7.2`` to ``v1.7.7``\nhave not been documented. This is because patch versions should only contain\nbug fixes, no new features or breaking changes. This hasn't been the case in\nthe past, but from now on, the library will try to adhere more strictly to\nthe `Semantic Versioning <https://semver.org>`_ principles.\n\nIf you ever want to look at those bug fixes, please use the appropriated\n``git`` command, such as ``git shortlog v1.7.1...v1.7.4``, but in general,\nthey probably just fixed your issue.\n\nWith that out of the way, let's look at the full change set:\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The layer changed, so take note if you use the raw API, as it's usual.\n* The way photos are downloaded changed during the layer update of the\n  previous version, and fixing that bug as a breaking change in itself.\n  `client.download_media() <telethon.client.downloads.DownloadMethods.download_media>`\n  now offers a different way to deal with thumbnails.\n\n\nAdditions\n~~~~~~~~~\n\n* New `Message.file <telethon.tl.custom.message.Message.file>` property!\n  Now you can trivially access `message.file.id  <telethon.tl.custom.file.File.id>`\n  to get the file ID of some media, or even ``print(message.file.name)``.\n* Archiving dialogs with `Dialog.archive() <telethon.tl.custom.dialog.Dialog.archive>`\n  or `client.edit_folder() <telethon.client.dialogs.DialogMethods.edit_folder>`\n  is now possible.\n* New cleaned-up method to stream downloads with `client.iter_download()\n  <telethon.client.downloads.DownloadMethods.iter_download>`, which offers\n  a lot of flexibility, such as arbitrary offsets for efficient seeking.\n* `Dialog.delete() <telethon.tl.custom.dialog.Dialog.delete>` has existed\n  for a while, and now `client.delete_dialog()\n  <telethon.client.dialogs.DialogMethods.delete_dialog>` exists too so you\n  can easily leave chats or delete dialogs without fetching all dialogs.\n* Some people or chats have a lot of profile photos. You can now iterate\n  over all of them with the new `client.iter_profile_photos()\n  <telethon.client.chats.ChatMethods.iter_profile_photos>` method.\n* You can now annoy everyone with the new `Message.pin(notify=True)\n  <telethon.tl.custom.message.Message.pin>`! The client has its own\n  variant too, called `client.pin_message()\n  <telethon.client.messages.MessageMethods.pin_message>`.\n\n\nBug fixes\n~~~~~~~~~\n\n* Correctly catch and raise all RPC errors.\n* Downloading stripped photos wouldn't work correctly.\n* Under some systems, ``libssl`` would fail to load earlier than\n  expected, causing the library to fail when being imported.\n* `conv.get_response() <telethon.tl.custom.conversation.Conversation.get_response>`\n  after ID 0 wasn't allowed when it should.\n* `InlineBuilder <telethon.tl.custom.inlinebuilder.InlineBuilder>` only worked\n  with local files, but files from anywhere are supported.\n* Accessing the text property from a raw-API call to fetch :tl:`Message` would fail\n  (any any other property that needed the client).\n* Database is now upgraded if the version was lower, not different.\n  From now on, this should help with upgrades and downgrades slightly.\n* Fixed saving ``pts`` and session-related stuff.\n* Disconnection should not raise any errors.\n* Invite links of the form ``tg://join?invite=`` now work.\n* `client.iter_participants(search=...) <telethon.client.chats.ChatMethods.iter_participants>`\n  now works on private chats again.\n* Iterating over messages in reverse with a date as offset wouldn't work.\n* The conversation would behave weirdly when a timeout occurred.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n* ``telethon`` now re-export all the goodies that you commonly need when\n  using the library, so e.g. ``from telethon import Button`` will now work.\n* ``telethon.sync`` now re-exports everything from ``telethon``, so that\n  you can trivially import from just one place everything that you need.\n* More attempts at reducing CPU usage after automatically fetching missing\n  entities on events. This isn't a big deal, even if it sounds like one.\n* Hexadecimal invite links are now supported. You didn't need them, but\n  they will now work.\n\nInternal Changes\n~~~~~~~~~~~~~~~~\n\n* Deterministic code generation. This is good for ``diff``.\n* On Python 3.7 and above, we properly close the connection.\n* A lot of micro-optimization.\n* Fixes to bugs introduced while making this release.\n* Custom commands on ``setup.py`` are nicer to use.\n\n\n\nFix-up for Photo Downloads (v1.7.1)\n===================================\n\n*Published at 2019/04/24*\n\nTelegram changed the way thumbnails (which includes photos) are downloaded,\nso you can no longer use a :tl:`PhotoSize` alone to download a particular\nthumbnail size (this is a **breaking change**).\n\nInstead, you will have to specify the new ``thumb`` parameter in\n`client.download_media() <telethon.client.downloads.DownloadMethods.download_media>`\nto download a particular thumbnail size. This addition enables you to easily\ndownload thumbnails from documents, something you couldn't do easily before.\n\n\nEasier Events (v1.7)\n====================\n\n*Published at 2019/04/22*\n\n+-----------------------+\n| Scheme layer used: 98 |\n+-----------------------+\n\nIf you have been using Telethon for a while, you probably know how annoying\nthe \"Could not find the input entity for…\" error can be. In this new version,\nthe library will try harder to find the input entity for you!\n\nThat is, instead of doing:\n\n.. code-block:: python\n\n    @client.on(events.NewMessage)\n    async def handler(event):\n        await client.download_profile_photo(await event.get_input_sender())\n        # ...... needs await, it's a method ^^^^^                       ^^\n\nYou can now do:\n\n.. code-block:: python\n\n    @client.on(events.NewMessage)\n    async def handler(event):\n        await client.download_profile_photo(event.input_sender)\n        # ...... no await, it's a property! ^\n        # It's also 12 characters shorter :)\n\nAnd even the following will hopefully work:\n\n.. code-block:: python\n\n    @client.on(events.NewMessage)\n    async def handler(event):\n        await client.download_profile_photo(event.sender_id)\n\nA lot of people use IDs thinking this is the right way of doing it. Ideally,\nyou would always use ``input_*``, not ``sender`` or ``sender_id`` (and the\nsame applies to chats). But, with this change, IDs will work just the same as\n``input_*`` inside events.\n\n**This feature still needs some more testing**, so please do open an issue\nif you find strange behaviour.\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The layer changed, and a lot of things did too. If you are using\n  raw API, you should be careful with this. In addition, some attributes\n  weren't of type ``datetime`` when they should be, which has been fixed.\n* Due to the layer change, you can no longer download photos with just\n  their :tl:`PhotoSize`. Version 1.7.1 introduces a new way to download\n  thumbnails to work around this issue.\n* `client.disconnect()\n  <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`\n  is now asynchronous again. This means you need to ``await`` it. You\n  don't need to worry about this if you were using ``with client`` or\n  `client.run_until_disconnected\n  <telethon.client.updates.UpdateMethods.run_until_disconnected>`.\n  This should prevent the \"pending task was destroyed\" errors.\n\nAdditions\n~~~~~~~~~\n\n* New in-memory cache for input entities. This should mean a lot less\n  of disk look-ups.\n* New `client.action <telethon.client.chats.ChatMethods.action>` method\n  to easily indicate that you are doing some chat action:\n\n  .. code-block:: python\n\n        async with client.action(chat, 'typing'):\n            await asyncio.sleep(2)  # type for 2 seconds\n            await client.send_message(chat, 'Hello world! I type slow ^^')\n\n  You can also easily use this for sending files, playing games, etc.\n\n\nNew bugs\n~~~~~~~~\n\n* Downloading photos is broken. This is fixed in v1.7.1.\n\nBug fixes\n~~~~~~~~~\n\n* Fix sending photos from streams/bytes.\n* Fix unhandled error when sending requests that were too big.\n* Fix edits that arrive too early on conversations.\n* Fix `client.edit_message()\n  <telethon.client.messages.MessageMethods.edit_message>`\n  when trying to edit a file.\n* Fix method calls on the objects returned by `client.iter_dialogs()\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>`.\n* Attempt at fixing `client.iter_dialogs()\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>` missing many dialogs.\n* ``offset_date`` in `client.iter_messages()\n  <telethon.client.messages.MessageMethods.iter_messages>` was being\n  ignored in some cases. This has been worked around.\n* Fix `callback_query.edit()\n  <telethon.events.callbackquery.CallbackQuery.Event.edit>`.\n* Fix `CallbackQuery(func=...) <telethon.events.callbackquery.CallbackQuery>`\n  was being ignored.\n* Fix `UserUpdate <telethon.events.userupdate.UserUpdate>` not working for\n  \"typing\" (and uploading file, etc.) status.\n* Fix library was not expecting ``IOError`` from PySocks.\n* Fix library was raising a generic ``ConnectionError``\n  and not the one that actually occurred.\n* Fix the ``blacklist_chats`` parameter in `MessageRead\n  <telethon.events.messageread.MessageRead>` not working as intended.\n* Fix `client.download_media(contact)\n  <telethon.client.downloads.DownloadMethods.download_media>`.\n* Fix mime type when sending ``mp3`` files.\n* Fix forcibly getting the sender or chat from events would\n  not always return all their information.\n* Fix sending albums with `client.send_file()\n  <telethon.client.uploads.UploadMethods.send_file>` was not returning\n  the sent messages.\n* Fix forwarding albums with `client.forward_messages()\n  <telethon.client.messages.MessageMethods.forward_messages>`.\n* Some fixes regarding filtering updates from chats.\n* Attempt at preventing duplicated updates.\n* Prevent double auto-reconnect.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n* Some improvements related to proxy connections.\n* Several updates and improvements to the documentation,\n  such as optional dependencies now being properly listed.\n* You can now forward messages from different chats directly with\n  `client.forward_messages <telethon.client.messages.MessageMethods.forward_messages>`.\n\n\nTidying up Internals (v1.6)\n===========================\n\n*Published at 2019/02/27*\n\n+-----------------------+\n| Scheme layer used: 95 |\n+-----------------------+\n\nFirst things first, sorry for updating the layer in the previous patch\nversion. That should only be done between major versions ideally, but\ndue to how Telegram works, it's done between minor versions. However raw\nAPI has and will always be considered \"unsafe\", this meaning that you\nshould always use the convenience client methods instead. These methods\ndon't cover the full API yet, so pull requests are welcome.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The layer update, of course. This didn't really need a mention here.\n* You can no longer pass a ``batch_size`` when iterating over messages.\n  No other method exposed this parameter, and it was only meant for testing\n  purposes. Instead, it's now a private constant.\n* ``client.iter_*`` methods no longer have a ``_total`` parameter which\n  was supposed to be private anyway. Instead, they return a new generator\n  object which has a ``.total`` attribute:\n\n  .. code-block:: python\n\n      it = client.iter_messages(chat)\n      for i, message in enumerate(it, start=1):\n          percentage = i / it.total\n          print('{:.2%} {}'.format(percentage, message.text))\n\nAdditions\n~~~~~~~~~\n\n* You can now pass ``phone`` and ``phone_code_hash`` in `client.sign_up\n  <telethon.client.auth.AuthMethods.sign_up>`, although you probably don't\n  need that.\n* Thanks to the overhaul of all ``client.iter_*`` methods, you can now do:\n\n  .. code-block:: python\n\n      for message in reversed(client.iter_messages('me')):\n          print(message.text)\n\nBug fixes\n~~~~~~~~~\n\n* Fix `telethon.utils.resolve_bot_file_id`, which wasn't working after\n  the layer update (so you couldn't send some files by bot file IDs).\n* Fix sending albums as bot file IDs (due to image detection improvements).\n* Fix `takeout() <telethon.client.account.AccountMethods.takeout>` failing\n  when they need to download media from other DCs.\n* Fix repeatedly calling `conversation.get_response()\n  <telethon.tl.custom.conversation.Conversation.get_response>` when many\n  messages arrived at once (i.e. when several of them were forwarded).\n* Fixed connecting with `ConnectionTcpObfuscated\n  <telethon.network.connection.tcpobfuscated.ConnectionTcpObfuscated>`.\n* Fix `client.get_peer_id('me')\n  <telethon.client.users.UserMethods.get_peer_id>`.\n* Fix warning of \"missing sqlite3\" when in reality it just had wrong tables.\n* Fix a strange error when using too many IDs in `client.delete_messages()\n  <telethon.client.messages.MessageMethods.delete_messages>`.\n* Fix `client.send_file <telethon.client.uploads.UploadMethods.send_file>`\n  with the result of `client.upload_file\n  <telethon.client.uploads.UploadMethods.upload_file>`.\n* When answering inline results, their order was not being preserved.\n* Fix `events.ChatAction <telethon.events.chataction.ChatAction>`\n  detecting user leaves as if they were kicked.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Cleared up some parts of the documentation.\n* Improved some auto-casts to make life easier.\n* Improved image detection. Now you can easily send `bytes`\n  and streams of images as photos, unless you force document.\n* Sending images as photos that are too large will now be resized\n  before uploading, reducing the time it takes to upload them and\n  also avoiding errors when the image was too large (as long as\n  ``pillow`` is installed). The images will remain unchanged if you\n  send it as a document.\n* Treat ``errors.RpcMcgetFailError`` as a temporary server error\n  to automatically retry shortly. This works around most issues.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n* New common way to deal with retries (``retry_range``).\n* Cleaned up the takeout client.\n* Completely overhauled asynchronous generators.\n\nLayer Update (v1.5.5)\n=====================\n\n*Published at 2019/01/14*\n\n+-----------------------+\n| Scheme layer used: 93 |\n+-----------------------+\n\nThere isn't an entry for v1.5.4 because it contained only one hot-fix\nregarding loggers. This update is slightly bigger so it deserves mention.\n\nAdditions\n~~~~~~~~~\n\n* New ``supports_streaming`` parameter in `client.send_file\n  <telethon.client.uploads.UploadMethods.send_file>`.\n\nBug fixes\n~~~~~~~~~\n\n* Dealing with mimetypes should cause less issues in systems like Windows.\n* Potentially fix alternative session storages that had issues with dates.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Saner timeout defaults for conversations.\n* ``Path``-like files are now supported for thumbnails.\n* Added new hot-keys to the online documentation at\n  https://tl.telethon.dev/ such as ``/`` to search.\n  Press ``?`` to view them all.\n\n\nBug Fixes (v1.5.3)\n==================\n\n*Published at 2019/01/14*\n\nSeveral bug fixes and some quality of life enhancements.\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* `message.edit <telethon.tl.custom.message.Message.edit>` now respects\n  the previous message buttons or link preview being hidden. If you want to\n  toggle them you need to explicitly set them. This is generally the desired\n  behaviour, but may cause some bots to have buttons when they shouldn't.\n\nAdditions\n~~~~~~~~~\n\n* You can now \"hide_via\" when clicking on results from `client.inline_query\n  <telethon.client.bots.BotMethods.inline_query>` to @bing and @gif.\n* You can now further configure the logger Telethon uses to suit your needs.\n\nBug fixes\n~~~~~~~~~\n\n* Fixes for ReadTheDocs to correctly build the documentation.\n* Fix :tl:`UserEmpty` not being expected when getting the input variant.\n* The message object returned when sending a message with buttons wouldn't\n  always contain the :tl:`ReplyMarkup`.\n* Setting email when configuring 2FA wasn't properly supported.\n* ``utils.resolve_bot_file_id`` now works again for photos.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Chat and channel participants can now be used as peers.\n* Reworked README and examples at\n  https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples\n\n\nTakeout Sessions (v1.5.2)\n=========================\n\n*Published at 2019/01/05*\n\nYou can now easily start takeout sessions (also known as data export sessions)\nthrough `client.takeout() <telethon.client.account.AccountMethods.takeout>`.\nSome of the requests will have lower flood limits when done through the\ntakeout session.\n\nBug fixes\n~~~~~~~~~\n\n* The new `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>`\n  had a bug that made it unusable.\n* `client.iter_dialogs() <telethon.client.dialogs.DialogMethods.iter_dialogs>`\n  will now locally check for the offset date, since Telegram ignores it.\n* Answering inline queries with media no works properly. You can now use\n  the library to create inline bots and send stickers through them!\n\n\nobject.to_json() (v1.5.1)\n=========================\n\n*Published at 2019/01/03*\n\nThe library already had a way to easily convert the objects the API returned\ninto dictionaries through ``object.to_dict()``, but some of the fields are\ndates or `bytes` which JSON can't serialize directly.\n\nFor convenience, a new ``object.to_json()`` has been added which will by\ndefault format both of those problematic types into something sensible.\n\nAdditions\n~~~~~~~~~\n\n* New `client.iter_admin_log()\n  <telethon.client.chats.ChatMethods.iter_admin_log>` method.\n\nBug fixes\n~~~~~~~~~\n\n* `client.is_connected()\n  <telethon.client.telegrambaseclient.TelegramBaseClient.is_connected>`\n  would be wrong when the initial connection failed.\n* Fixed ``UnicodeDecodeError`` when accessing the text of messages\n  with malformed offsets in their entities.\n* Fixed `client.get_input_entity()\n  <telethon.client.users.UserMethods.get_input_entity>` for integer IDs\n  that the client has not seen before.\n\nEnhancements\n~~~~~~~~~~~~\n\n* You can now configure the reply markup when using `Button\n  <telethon.tl.custom.button.Button>` as a bot.\n* More properties for `Message\n  <telethon.tl.custom.message.Message>` to make accessing media convenient.\n* Downloading to ``file=bytes`` will now return a `bytes` object\n  with the downloaded media.\n\n\nPolls with the Latest Layer (v1.5)\n==================================\n\n*Published at 2018/12/25*\n\n+-----------------------+\n| Scheme layer used: 91 |\n+-----------------------+\n\nThis version doesn't really bring many new features, but rather focuses on\nupdating the code base to support the latest available Telegram layer, 91.\nThis layer brings polls, and you can create and manage them through Telethon!\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The layer change from 82 to 91 changed a lot of things in the raw API,\n  so be aware that if you rely on raw API calls, you may need to update\n  your code, in particular **if you work with files**. They have a new\n  ``file_reference`` parameter that you must provide.\n\nAdditions\n~~~~~~~~~\n\n* New `client.is_bot() <telethon.client.users.UserMethods.is_bot>` method.\n\nBug fixes\n~~~~~~~~~\n\n* Markdown and HTML parsing now behave correctly with leading whitespace.\n* HTTP connection should now work correctly again.\n* Using ``caption=None`` would raise an error instead of setting no caption.\n* ``KeyError`` is now handled properly when forwarding messages.\n* `button.click() <telethon.tl.custom.messagebutton.MessageButton.click>`\n  now works as expected for :tl:`KeyboardButtonGame`.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Some improvements to the search in the full API and generated examples.\n* Using entities with ``access_hash = 0`` will now work in more cases.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n* Some changes to the documentation and code generation.\n* 2FA code was updated to work under the latest layer.\n\n\nError Descriptions in CSV files (v1.4.3)\n========================================\n\n*Published at 2018/12/04*\n\nWhile this may seem like a minor thing, it's a big usability improvement.\n\nAnyone who wants to update the documentation for known errors, or whether\nsome methods can be used as a bot, user or both, can now be easily edited.\nEveryone is encouraged to help document this better!\n\nBug fixes\n~~~~~~~~~\n\n* ``TimeoutError`` was not handled during automatic reconnects.\n* Getting messages by ID using :tl:`InputMessageReplyTo` could fail.\n* Fixed `message.get_reply_message\n  <telethon.tl.custom.message.Message.get_reply_message>`\n  as a bot when a user replied to a different bot.\n* Accessing some document properties in a `Message\n  <telethon.tl.custom.message.Message>` would fail.\n\nEnhancements\n~~~~~~~~~~~~\n\n* Accessing `events.ChatAction <telethon.events.chataction.ChatAction>`\n  properties such as input users may now work in more cases.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n* Error descriptions and information about methods is now loaded\n  from a CSV file instead of being part of several messy JSON files.\n\n\nBug Fixes (v1.4.2)\n==================\n\n*Published at 2018/11/24*\n\nThis version also includes the v1.4.1 hot-fix, which was a single\nquick fix and didn't really deserve an entry in the changelog.\n\nBug fixes\n~~~~~~~~~\n\n* Authorization key wouldn't be saved correctly, requiring re-login.\n* Conversations with custom events failed to be cancelled.\n* Fixed ``telethon.sync`` when using other threads.\n* Fix markdown/HTML parser from failing with leading/trailing whitespace.\n* Fix accessing ``chat_action_event.input_user`` property.\n* Potentially improved handling unexpected disconnections.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n* Better default behaviour for `client.send_read_acknowledge\n  <telethon.client.messages.MessageMethods.send_read_acknowledge>`.\n* Clarified some points in the documentation.\n* Clearer errors for ``utils.get_peer*``.\n\n\nConnection Overhaul (v1.4)\n==========================\n\n*Published at 2018/11/03*\n\nYet again, a lot of work has been put into reworking the low level connection\nclasses. This means ``asyncio.open_connection`` is now used correctly and the\nerrors it can produce are handled properly. The separation between packing,\nencrypting and network is now abstracted away properly, so reasoning about\nthe code is easier, making it more maintainable.\n\nAs a user, you shouldn't worry about this, other than being aware that quite\na few changes were made in the insides of the library and you should report\nany issues that you encounter with this version if any.\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* The threaded version of the library will no longer be maintained, primarily\n  because it never was properly maintained anyway. If you have old code, stick\n  with old versions of the library, such as ``0.19.1.6``.\n* Timeouts no longer accept ``timedelta``. Simply use seconds.\n* The ``callback`` parameter from `telethon.tl.custom.button.Button.inline()`\n  was removed, since it had always been a bad idea. Adding the callback there\n  meant a lot of extra work for every message sent, and only registering it\n  after the first message was sent! Instead, use\n  `telethon.events.callbackquery.CallbackQuery`.\n\n\nAdditions\n~~~~~~~~~\n\n* New `dialog.delete() <telethon.tl.custom.dialog.Dialog.delete>` method.\n* New `conversation.cancel()\n  <telethon.tl.custom.conversation.Conversation.cancel>` method.\n* New ``retry_delay`` delay for the client to be used on auto-reconnection.\n\n\nBug fixes\n~~~~~~~~~\n\n* Fixed `Conversation.wait_event()\n  <telethon.tl.custom.conversation.Conversation.wait_event>`.\n* Fixed replying with photos/documents on inline results.\n* `client.is_user_authorized()\n  <telethon.client.users.UserMethods.is_user_authorized>` now works\n  correctly after `client.log_out()\n  <telethon.client.auth.AuthMethods.log_out>`.\n* `dialog.is_group <telethon.tl.custom.dialog.Dialog>` now works for\n  :tl:`ChatForbidden`.\n* Not using ``async with`` when needed is now a proper error.\n* `events.CallbackQuery <telethon.events.callbackquery.CallbackQuery>`\n  with string regex was not working properly.\n* `client.get_entity('me') <telethon.client.users.UserMethods.get_entity>`\n  now works again.\n* Empty codes when signing in are no longer valid.\n* Fixed file cache for in-memory sessions.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n* Support ``next_offset`` in `inline_query.answer()\n  <telethon.events.inlinequery.InlineQuery.Event.answer>`.\n* Support ``<a href=\"tg://user?id=123\">`` mentions in HTML parse mode.\n* New auto-casts for :tl:`InputDocument` and :tl:`InputChatPhoto`.\n* Conversations are now exclusive per-chat by default.\n* The request that caused a RPC error is now shown in the error message.\n* New full API examples in the generated documentation.\n* Fixed some broken links in the documentation.\n* `client.disconnect()\n  <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`\n  is now synchronous, but you can still ``await`` it for consistency\n  or compatibility.\n\n\nEvent Templates (v1.3)\n======================\n\n*Published at 2018/09/22*\n\n\nIf you have worked with Flask templates, you will love this update,\nsince it gives you the same features but even more conveniently:\n\n.. code-block:: python\n\n    # handlers/welcome.py\n    from telethon import events\n\n    @events.register(events.NewMessage('(?i)hello'))\n    async def handler(event):\n        client = event.client\n        await event.respond('Hi!')\n        await client.send_message('me', 'Sent hello to someone')\n\n\nThis will `register <telethon.events.register>` the ``handler`` callback\nto handle new message events. Note that you didn't add this to any client\nyet, and this is the key point: you don't need a client to define handlers!\nYou can add it later:\n\n.. code-block:: python\n\n    # main.py\n    from telethon import TelegramClient\n    import handlers.welcome\n\n    with TelegramClient(...) as client:\n        # This line adds the handler we defined before for new messages\n        client.add_event_handler(handlers.welcome.handler)\n        client.run_until_disconnected()\n\n\nThis should help you to split your big code base into a more modular design.\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n* ``.sender`` is the ``.chat`` when the message is sent in a broadcast\n  channel. This makes sense, because the sender of the message was the\n  channel itself, but you now must take into consideration that it may\n  be either a :tl:`User` or :tl:`Channel` instead of being `None`.\n\n\nAdditions\n~~~~~~~~~\n\n* New ``MultiError`` class when invoking many requests at once\n  through ``client([requests])``.\n* New custom ``func=`` on all events. These will receive the entire\n  event, and a good usage example is ``func=lambda e: e.is_private``.\n* New ``.web_preview`` field on messages. The ``.photo`` and ``.document``\n  will also return the media in the web preview if any, for convenience.\n* Callback queries now have a ``.chat`` in most circumstances.\n\n\nBug fixes\n~~~~~~~~~\n\n* Running code with `python3 -O` would remove critical code from asserts.\n* Fix some rare ghost disconnections after reconnecting.\n* Fix strange behavior for `send_message(chat, Message, reply_to=foo)\n  <telethon.client.messages.MessageMethods.send_message>`.\n* The ``loop=`` argument was being pretty much ignored.\n* Fix ``MemorySession`` file caching.\n* The logic for getting entities from their username is now correct.\n* Fixes for sending stickers from ``.webp`` files in Windows, again.\n* Fix disconnection without being logged in.\n* Retrieving media from messages would fail.\n* Getting some messages by ID on private chats.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n* `iter_participants <telethon.client.chats.ChatMethods.iter_participants>`\n  will now use its ``search=`` as a symbol set when ``aggressive=True``,\n  so you can do ``client.get_participants(group, aggressive=True,\n  search='абвгдеёжзийклмнопрст')``.\n* The ``StringSession`` supports custom encoding.\n* Callbacks for `telethon.client.auth.AuthMethods.start` can be ``async``.\n\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n* Cherry-picked a commit to use ``asyncio.open_connection`` in the lowest\n  level of the library. Do open issues if this causes trouble, but it should\n  otherwise improve performance and reliability.\n* Building and resolving events overhaul.\n\n\nConversations, String Sessions and More (v1.2)\n==============================================\n\n*Published at 2018/08/14*\n\n\nThis is a big release! Quite a few things have been added to the library,\nsuch as the new `Conversation <telethon.tl.custom.conversation.Conversation>`.\nThis makes it trivial to get tokens from `@BotFather <https://t.me/BotFather>`_:\n\n.. code-block:: python\n\n    from telethon.tl import types\n\n    with client.conversation('BotFather') as conv:\n        conv.send_message('/mybots')\n        message = conv.get_response()\n        message.click(0)\n        message = conv.get_edit()\n        message.click(0)\n        message = conv.get_edit()\n        for _, token in message.get_entities_text(types.MessageEntityCode):\n            print(token)\n\n\nIn addition to that, you can now easily load and export session files\nwithout creating any on-disk file thanks to the ``StringSession``:\n\n.. code-block:: python\n\n    from telethon.sessions import StringSession\n    string = StringSession.save(client.session)\n\nCheck out :ref:`sessions` for more details.\n\nFor those who aren't able to install ``cryptg``, the support for ``libssl``\nhas been added back. While interfacing ``libssl`` is not as fast, the speed\nwhen downloading and sending files should really be noticeably faster.\n\nWhile those are the biggest things, there are still more things to be\nexcited about.\n\n\nAdditions\n~~~~~~~~~\n\n- The mentioned method to start a new `client.conversation\n  <telethon.client.dialogs.DialogMethods.conversation>`.\n- Implemented global search through `client.iter_messages\n  <telethon.client.messages.MessageMethods.iter_messages>`\n  with `None` entity.\n- New `client.inline_query <telethon.client.bots.BotMethods.inline_query>`\n  method to perform inline queries.\n- Bot-API-style ``file_id`` can now be used to send files and download media.\n  You can also access `telethon.utils.resolve_bot_file_id` and\n  `telethon.utils.pack_bot_file_id` to resolve and create these\n  file IDs yourself. Note that each user has its own ID for each file\n  so you can't use a bot's ``file_id`` with your user, except stickers.\n- New `telethon.utils.get_peer`, useful when you expect a :tl:`Peer`.\n\nBug fixes\n~~~~~~~~~\n\n- UTC timezone for `telethon.events.userupdate.UserUpdate`.\n- Bug with certain input parameters when iterating messages.\n- RPC errors without parent requests caused a crash, and better logging.\n- ``incoming = outgoing = True`` was not working properly.\n- Getting a message's ID was not working.\n- File attributes not being inferred for ``open()``'ed files.\n- Use ``MemorySession`` if ``sqlite3`` is not installed by default.\n- Self-user would not be saved to the session file after signing in.\n- `client.catch_up() <telethon.client.updates.UpdateMethods.catch_up>`\n  seems to be functional again.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n- Updated documentation.\n- Invite links will now use cache, so using them as entities is cheaper.\n- You can reuse message buttons to send new messages with those buttons.\n- ``.to_dict()`` will now work even on invalid ``TLObject``'s.\n\n\nBetter Custom Message (v1.1.1)\n==============================\n\n*Published at 2018/07/23*\n\nThe `custom.Message <telethon.tl.custom.message.Message>` class has been\nrewritten in a cleaner way and overall feels less hacky in the library.\nThis should perform better than the previous way in which it was patched.\n\nThe release is primarily intended to test this big change, but also fixes\n**Python 3.5.2 compatibility** which was broken due to a trailing comma.\n\n\nBug fixes\n~~~~~~~~~\n\n- Using ``functools.partial`` on event handlers broke updates\n  if they had uncaught exceptions.\n- A bug under some session files where the sender would export\n  authorization for the same data center, which is unsupported.\n- Some logical bugs in the custom message class.\n\n\nBot Friendly (v1.1)\n===================\n\n*Published at 2018/07/21*\n\nTwo new event handlers to ease creating normal bots with the library,\nnamely `events.InlineQuery <telethon.events.inlinequery.InlineQuery>`\nand `events.CallbackQuery <telethon.events.callbackquery.CallbackQuery>`\nfor handling ``@InlineBot queries`` or reacting to a button click. For\nthis second option, there is an even better way:\n\n.. code-block:: python\n\n    from telethon.tl.custom import Button\n\n    async def callback(event):\n        await event.edit('Thank you!')\n\n    bot.send_message(chat, 'Hello!',\n                     buttons=Button.inline('Click me', callback))\n\n\nYou can directly pass the callback when creating the button.\n\nThis is fine for small bots but it will add the callback every time\nyou send a message, so you probably should do this instead once you\nare done testing:\n\n.. code-block:: python\n\n    markup = bot.build_reply_markup(Button.inline('Click me', callback))\n    bot.send_message(chat, 'Hello!', buttons=markup)\n\n\nAnd yes, you can create more complex button layouts with lists:\n\n.. code-block:: python\n\n    from telethon import events\n\n    global phone = ''\n\n    @bot.on(events.CallbackQuery)\n    async def handler(event):\n        global phone\n        if event.data == b'<':\n            phone = phone[:-1]\n        else:\n            phone += event.data.decode('utf-8')\n\n        await event.answer('Phone is now {}'.format(phone))\n\n    markup = bot.build_reply_markup([\n        [Button.inline('1'), Button.inline('2'), Button.inline('3')],\n        [Button.inline('4'), Button.inline('5'), Button.inline('6')],\n        [Button.inline('7'), Button.inline('8'), Button.inline('9')],\n        [Button.inline('+'), Button.inline('0'), Button.inline('<')],\n    ])\n    bot.send_message(chat, 'Enter a phone', buttons=markup)\n\n\n(Yes, there are better ways to do this). Now for the rest of things:\n\n\nAdditions\n~~~~~~~~~\n\n- New `custom.Button <telethon.tl.custom.button.Button>` class\n  to help you create inline (or normal) reply keyboards. You\n  must sign in as a bot to use the ``buttons=`` parameters.\n- New events usable if you sign in as a bot: `events.InlineQuery\n  <telethon.events.inlinequery.InlineQuery>` and `events.CallbackQuery\n  <telethon.events.callbackquery.CallbackQuery>`.\n- New ``silent`` parameter when sending messages, usable in broadcast channels.\n- Documentation now has an entire section dedicate to how to use\n  the client's friendly methods at *(removed broken link)*.\n\nBug fixes\n~~~~~~~~~\n\n- Empty ``except`` are no longer used which means\n  sending a keyboard interrupt should now work properly.\n- The ``pts`` of incoming updates could be `None`.\n- UTC timezone information is properly set for read ``datetime``.\n- Some infinite recursion bugs in the custom message class.\n- :tl:`Updates` was being dispatched to raw handlers when it shouldn't.\n- Using proxies and HTTPS connection mode may now work properly.\n- Less flood waits when downloading media from different data centers,\n  and the library will now detect them even before sending requests.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Interactive sign in now supports signing in with a bot token.\n- ``timedelta`` is now supported where a date is expected, which\n  means you can e.g. ban someone for ``timedelta(minutes=5)``.\n- Events are only built once and reused many times, which should\n  save quite a few CPU cycles if you have a lot of the same type.\n- You can now click inline buttons directly if you know their data.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- When downloading media, the right sender is directly\n  used without previously triggering migrate errors.\n- Code reusing for getting the chat and the sender,\n  which easily enables this feature for new types.\n\n\nNew HTTP(S) Connection Mode (v1.0.4)\n====================================\n\n*Published at 2018/07/09*\n\nThis release implements the HTTP connection mode to the library, which\nmeans certain proxies that only allow HTTP connections should now work\nproperly. You can use it doing the following, like any other mode:\n\n.. code-block:: python\n\n    from telethon import TelegramClient, sync\n    from telethon.network import ConnectionHttp\n\n    client = TelegramClient(..., connection=ConnectionHttp)\n    with client:\n        client.send_message('me', 'Hi!')\n\n\nAdditions\n~~~~~~~~~\n\n- ``add_mark=`` is now back on ``utils.get_input_peer`` and also on\n  `client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`.\n- New `client.get_peer_id <telethon.client.users.UserMethods.get_peer_id>`\n  convenience for ``utils.get_peer_id(await client.get_input_entity(peer))``.\n\n\nBug fixes\n~~~~~~~~~\n\n- If several `TLMessage` in a `MessageContainer` exceeds 1MB, it will no\n  longer be automatically turned into one. This basically means that e.g.\n  uploading 10 file parts at once will work properly again.\n- Documentation fixes and some missing ``await``.\n- Revert named argument for `client.forward_messages\n  <telethon.client.messages.MessageMethods.forward_messages>`\n\nEnhancements\n~~~~~~~~~~~~\n\n- New auto-casts to :tl:`InputNotifyPeer` and ``chat_id``.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Outgoing `TLMessage` are now pre-packed so if there's an error when\n  serializing the raw requests, the library will no longer swallow it.\n  This also means re-sending packets doesn't need to re-pack their bytes.\n\n\n\nIterate Messages in Reverse (v1.0.3)\n====================================\n\n*Published at 2018/07/04*\n\n+-----------------------+\n| Scheme layer used: 82 |\n+-----------------------+\n\nMostly bug fixes, but now there is a new parameter on `client.iter_messages\n<telethon.client.messages.MessageMethods.iter_messages>` to support reversing\nthe order in which messages are returned.\n\nAdditions\n~~~~~~~~~\n\n- The mentioned ``reverse`` parameter when iterating over messages.\n- A new ``sequential_updates`` parameter when creating the client\n  for updates to be processed sequentially. This is useful when you\n  need to make sure that all updates are processed in order, such\n  as a script that only forwards incoming messages somewhere else.\n\nBug fixes\n~~~~~~~~~\n\n- Count was always `None` for `message.button_count\n  <telethon.tl.custom.message.Message.button_count>`.\n- Some fixes when disconnecting upon dropping the client.\n- Support for Python 3.4 in the sync version, and fix media download.\n- Some issues with events when accessing the input chat or their media.\n- Hachoir wouldn't automatically close the file after reading its metadata.\n- Signing in required a named ``code=`` parameter, but usage\n  without a name was really widespread so it has been reverted.\n\n\nBug Fixes (v1.0.2)\n==================\n\n*Published at 2018/06/28*\n\nUpdated some asserts and parallel downloads, as well as some fixes for sync.\n\n\nBug Fixes (v1.0.1)\n==================\n\n*Published at 2018/06/27*\n\nAnd as usual, every major release has a few bugs that make the library\nunusable! This quick update should fix those, namely:\n\nBug fixes\n~~~~~~~~~\n\n- `client.start() <telethon.client.auth.AuthMethods.start>` was completely\n  broken due to a last-time change requiring named arguments everywhere.\n- Since the rewrite, if your system clock was wrong, the connection would\n  get stuck in an infinite \"bad message\" loop of responses from Telegram.\n- Accessing the buttons of a custom message wouldn't work in channels,\n  which lead to fix a completely different bug regarding starting bots.\n- Disconnecting could complain if the magic ``telethon.sync`` was imported.\n- Successful automatic reconnections now ask Telegram to send updates to us\n  once again as soon as the library is ready to listen for them.\n\n\nSynchronous magic (v1.0)\n========================\n\n*Published at 2018/06/27*\n\n.. important::\n\n    If you come from Telethon pre-1.0 you **really** want to read\n    :ref:`compatibility-and-convenience` to port your scripts to\n    the new version.\n\nThe library has been around for well over a year. A lot of improvements have\nbeen made, a lot of user complaints have been fixed, and a lot of user desires\nhave been implemented. It's time to consider the public API as stable, and\nremove some of the old methods that were around until now for compatibility\nreasons. But there's one more surprise!\n\nThere is a new magic ``telethon.sync`` module to let you use **all** the\nmethods in the :ref:`TelegramClient <telethon-client>` (and the types returned\nfrom its functions) in a synchronous way, while using `asyncio` behind\nthe scenes! This means you're now able to do both of the following:\n\n.. code-block:: python\n\n    import asyncio\n\n    async def main():\n      await client.send_message('me', 'Hello!')\n\n    asyncio.run(main())\n\n    # ...can be rewritten as:\n\n    from telethon import sync\n    client.send_message('me', 'Hello!')\n\nBoth ways can coexist (you need to ``await`` if the loop is running).\n\nYou can also use the magic ``sync`` module in your own classes, and call\n``sync.syncify(cls)`` to convert all their ``async def`` into magic variants.\n\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n- ``message.get_fwd_sender`` is now in `message.forward\n  <telethon.tl.custom.message.Message.forward>`.\n- ``client.idle`` is now `client.run_until_disconnected()\n  <telethon.client.updates.UpdateMethods.run_until_disconnected>`\n- ``client.add_update_handler`` is now `client.add_event_handler\n  <telethon.client.updates.UpdateMethods.add_event_handler>`\n- ``client.remove_update_handler`` is now `client.remove_event_handler\n  <telethon.client.updates.UpdateMethods.remove_event_handler>`\n- ``client.list_update_handlers`` is now `client.list_event_handlers\n  <telethon.client.updates.UpdateMethods.list_event_handlers>`\n- ``client.get_message_history`` is now `client.get_messages\n  <telethon.client.messages.MessageMethods.get_messages>`\n- ``client.send_voice_note`` is now `client.send_file\n  <telethon.client.uploads.UploadMethods.send_file>` with ``is_voice=True``.\n- ``client.invoke()`` is now ``client(...)``.\n- ``report_errors`` has been removed since it's currently not used,\n  and ``flood_sleep_threshold`` is now part of the client.\n- The ``update_workers`` and ``spawn_read_thread`` arguments are gone.\n  Simply remove them from your code when you create the client.\n- Methods with a lot of arguments can no longer be used without specifying\n  their argument. Instead you need to use named arguments. This improves\n  readability and not needing to learn the order of the arguments, which\n  can also change.\n\n\nAdditions\n~~~~~~~~~\n\n- `client.send_file <telethon.client.uploads.UploadMethods.send_file>` now\n  accepts external ``http://`` and ``https://`` URLs.\n- You can use the :ref:`TelegramClient <telethon-client>` inside of ``with``\n  blocks, which will `client.start() <telethon.client.auth.AuthMethods.start>`\n  and `disconnect() <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`\n  the client for you:\n\n  .. code-block:: python\n\n      from telethon import TelegramClient, sync\n\n      with TelegramClient(name, api_id, api_hash) as client:\n          client.send_message('me', 'Hello!')\n\n  Convenience at its maximum! You can even chain the `.start()\n  <telethon.client.auth.AuthMethods.start>` method since\n  it returns the instance of the client:\n\n  .. code-block:: python\n\n      with TelegramClient(name, api_id, api_hash).start(bot_token=token) as bot:\n          bot.send_message(chat, 'Hello!')\n\n\nBug fixes\n~~~~~~~~~\n\n- There were some ``@property async def`` left, and some ``await property``.\n- \"User joined\" event was being treated as \"User was invited\".\n- SQLite's cursor should not be closed properly after usage.\n- ``await`` the updates task upon disconnection.\n- Some bug in Python 3.5.2's `asyncio` causing 100% CPU load if you\n  forgot to call `client.disconnect()\n  <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`.\n  The method is called for you on object destruction, but you still should\n  disconnect manually or use a ``with`` block.\n- Some fixes regarding disconnecting on client deletion and properly\n  saving the authorization key.\n- Passing a class to `message.get_entities_text\n  <telethon.tl.custom.message.Message.get_entities_text>` now works properly.\n- Iterating messages from a specific user in private messages now works.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Both `client.start() <telethon.client.auth.AuthMethods.start>` and\n  `client.run_until_disconnected()\n  <telethon.client.updates.UpdateMethods.run_until_disconnected>` can\n  be ran in both a synchronous way (without starting the loop manually)\n  or from an ``async def`` where they need to have an ``await``.\n\n\nCore Rewrite in asyncio (v1.0-rc1)\n==================================\n\n*Published at 2018/06/24*\n\n+-----------------------+\n| Scheme layer used: 81 |\n+-----------------------+\n\nThis version is a major overhaul of the library internals. The core has\nbeen rewritten, cleaned up and refactored to fix some oddities that have\nbeen growing inside the library.\n\nThis means that the code is easier to understand and reason about,\nincluding the code flow such as conditions, exceptions, where to\nreconnect, how the library should behave, and separating different\nretry types such as disconnections or call fails, but it also means\nthat **some things will necessarily break** in this version.\n\nAll requests that touch the network are now methods and need to\nhave their ``await`` (or be ran until their completion).\n\nAlso, the library finally has the simple logo it deserved: a carefully\nhand-written ``.svg`` file representing a T following Python's colours.\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n- If you relied on internals like the ``MtProtoSender`` and the\n  ``TelegramBareClient``, both are gone. They are now `MTProtoSender\n  <telethon.network.mtprotosender.MTProtoSender>` and `TelegramBaseClient\n  <telethon.client.telegrambaseclient.TelegramBaseClient>` and they behave\n  differently.\n- Underscores have been renamed from filenames. This means\n  ``telethon.errors.rpc_error_list`` won't work, but you should\n  have been using `telethon.errors` all this time instead.\n- `client.connect <telethon.client.telegrambaseclient.TelegramBaseClient.connect>`\n  no longer returns `True` on success. Instead, you should ``except`` the\n  possible ``ConnectionError`` and act accordingly. This makes it easier to\n  not ignore the error.\n- You can no longer set ``retries=n`` when calling a request manually. The\n  limit works differently now, and it's done on a per-client basis.\n- Accessing `.sender <telethon.tl.custom.message.Message.sender>`,\n  `.chat <telethon.tl.custom.message.Message.chat>` and similar may *not* work\n  in events anymore, since previously they could access the network. The new\n  rule is that properties are not allowed to make API calls. You should use\n  `.get_sender() <telethon.tl.custom.message.Message.get_sender>`,\n  `.get_chat() <telethon.tl.custom.message.Message.get_chat>` instead while\n  using events. You can safely access properties if you get messages through\n  `client.get_messages() <telethon.client.messages.MessageMethods.get_messages>`\n  or other methods in the client.\n- The above point means ``reply_message`` is now `.get_reply_message()\n  <telethon.tl.custom.message.Message.get_reply_message>`, and ``fwd_from_entity``\n  is now `get_fwd_sender() <telethon.tl.custom.message.Message.get_fwd_sender>`.\n  Also ``forward`` was gone in the previous version, and you should be using\n  ``fwd_from`` instead.\n\n\nAdditions\n~~~~~~~~~\n\n- Telegram's Terms Of Service are now accepted when creating a new account.\n  This can possibly help avoid bans. This has no effect for accounts that\n  were created before.\n- The `method reference <https://tl.telethon.dev/>`_ now shows\n  which methods can be used if you sign in with a ``bot_token``.\n- There's a new `client.disconnected\n  <telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>` future\n  which you can wait on. When a disconnection occurs, you will now, instead\n  letting it happen in the background.\n- More configurable retries parameters, such as auto-reconnection, retries\n  when connecting, and retries when sending a request.\n- You can filter `events.NewMessage <telethon.events.newmessage.NewMessage>`\n  by sender ID, and also whether they are forwards or not.\n- New ``ignore_migrated`` parameter for `client.iter_dialogs\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>`.\n\nBug fixes\n~~~~~~~~~\n\n- Several fixes to `telethon.events.newmessage.NewMessage`.\n- Removed named ``length`` argument in ``to_bytes`` for PyPy.\n- Raw events failed due to not having ``._set_client``.\n- `message.get_entities_text\n  <telethon.tl.custom.message.Message.get_entities_text>` properly\n  supports filtering, even if there are no message entities.\n- `message.click <telethon.tl.custom.message.Message.click>` works better.\n- The server started sending :tl:`DraftMessageEmpty` which the library\n  didn't handle correctly when getting dialogs.\n- The \"correct\" chat is now always returned from returned messages.\n- ``to_id`` was not validated when retrieving messages by their IDs.\n- ``'__'`` is no longer considered valid in usernames.\n- The ``fd`` is removed from the reader upon closing the socket. This\n  should be noticeable in Windows.\n- :tl:`MessageEmpty` is now handled when searching messages.\n- Fixed a rare infinite loop bug in `client.iter_dialogs\n  <telethon.client.dialogs.DialogMethods.iter_dialogs>` for some people.\n- Fixed ``TypeError`` when there is no `.sender\n  <telethon.tl.custom.message.Message.sender>`.\n\nEnhancements\n~~~~~~~~~~~~\n\n- You can now delete over 100 messages at once with `client.delete_messages\n  <telethon.client.messages.MessageMethods.delete_messages>`.\n- Signing in now accounts for ``AuthRestartError`` itself, and also handles\n  ``PasswordHashInvalidError``.\n- ``__all__`` is now defined, so ``from telethon import *`` imports sane\n  defaults (client, events and utils). This is however discouraged and should\n  be used only in quick scripts.\n- ``pathlib.Path`` is now supported for downloading and uploading media.\n- Messages you send to yourself are now considered outgoing, unless they\n  are forwarded.\n- The documentation has been updated with a brand new `asyncio` crash\n  course to encourage you use it. You can still use the threaded version\n  if you want though.\n- ``.name`` property is now properly supported when sending and downloading\n  files.\n- Custom ``parse_mode``, which can now be set per-client, support\n  :tl:`MessageEntityMentionName` so you can return those now.\n- The session file is saved less often, which could result in a noticeable\n  speed-up when working with a lot of incoming updates.\n\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- The flow for sending a request is as follows: the ``TelegramClient`` creates\n  a ``MTProtoSender`` with a ``Connection``, and the sender starts send and\n  receive loops. Sending a request means enqueueing it in the sender, which\n  will eventually pack and encrypt it with its ``ConnectionState`` instead\n  of using the entire ``Session`` instance. When the data is packed, it will\n  be sent over the ``Connection`` and ultimately over the ``TcpClient``.\n\n- Reconnection occurs at the ``MTProtoSender`` level, and receiving responses\n  follows a similar process, but now ``asyncio.Future`` is used for the results\n  which are no longer part of all ``TLObject``, instead are part of the\n  ``TLMessage`` which simplifies things.\n\n- Objects can no longer be ``content_related`` and instead subclass\n  ``TLRequest``, making the separation of concerns easier.\n\n- The ``TelegramClient`` has been split into several mixin classes to avoid\n  having a 3,000-lines-long file with all the methods.\n\n- More special cases in the ``MTProtoSender`` have been cleaned up, and also\n  some attributes from the ``Session`` which didn't really belong there since\n  they weren't being saved.\n\n- The ``telethon_generator/`` can now convert ``.tl`` files into ``.json``,\n  mostly as a proof of concept, but it might be useful for other people.\n\n\nCustom Message class (v0.19.1)\n==============================\n\n*Published at 2018/06/03*\n\n+-----------------------+\n| Scheme layer used: 80 |\n+-----------------------+\n\n\nThis update brings a new `telethon.tl.custom.message.Message` object!\n\nAll the methods in the `telethon.telegram_client.TelegramClient` that\nused to return a :tl:`Message` will now return this object instead, which\nmeans you can do things like the following:\n\n.. code-block:: python\n\n    msg = client.send_message(chat, 'Hello!')\n    msg.edit('Hello there!')\n    msg.reply('Good day!')\n    print(msg.sender)\n\nRefer to its documentation to see all you can do, again, click\n`telethon.tl.custom.message.Message` to go to its page.\n\n\nBreaking Changes\n~~~~~~~~~~~~~~~~\n\n- The `telethon.network.connection.common.Connection` class is now an ABC,\n  and the old ``ConnectionMode`` is now gone. Use a specific connection (like\n  `telethon.network.connection.tcpabridged.ConnectionTcpAbridged`) instead.\n\nAdditions\n~~~~~~~~~\n\n- You can get messages by their ID with\n  `telethon.telegram_client.TelegramClient.get_messages`'s ``ids`` parameter:\n\n  .. code-block:: python\n\n      message = client.get_messages(chats, ids=123)  # Single message\n      message_list = client.get_messages(chats, ids=[777, 778])  # Multiple\n\n- More convenience properties for `telethon.tl.custom.dialog.Dialog`.\n- New default `telethon.telegram_client.TelegramClient.parse_mode`.\n- You can edit the media of messages that already have some media.\n- New dark theme in the online ``tl`` reference, check it out at\n  https://tl.telethon.dev/.\n\nBug fixes\n~~~~~~~~~\n\n- Some IDs start with ``1000`` and these would be wrongly treated as channels.\n- Some short usernames like ``@vote`` were being ignored.\n- `telethon.telegram_client.TelegramClient.iter_messages`'s ``from_user``\n  was failing if no filter had been set.\n- `telethon.telegram_client.TelegramClient.iter_messages`'s ``min_id/max_id``\n  was being ignored by Telegram. This is now worked around.\n- `telethon.telegram_client.TelegramClient.catch_up` would fail with empty\n  states.\n- `telethon.events.newmessage.NewMessage` supports ``incoming=False``\n  to indicate ``outgoing=True``.\n\nEnhancements\n~~~~~~~~~~~~\n\n- You can now send multiple requests at once while preserving the order:\n\n  .. code-block:: python\n\n      from telethon.tl.functions.messages import SendMessageRequest\n      client([SendMessageRequest(chat, 'Hello 1!'),\n              SendMessageRequest(chat, 'Hello 2!')], ordered=True)\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- ``without rowid`` is not used in SQLite anymore.\n- Unboxed serialization would fail.\n- Different default limit for ``iter_messages`` and ``get_messages``.\n- Some clean-up in the ``telethon_generator/`` package.\n\n\nCatching up on Updates (v0.19)\n==============================\n\n*Published at 2018/05/07*\n\n+-----------------------+\n| Scheme layer used: 76 |\n+-----------------------+\n\nThis update prepares the library for catching up with updates with the new\n`telethon.telegram_client.TelegramClient.catch_up` method. This feature needs\nmore testing, but for now it will let you \"catch up\" on some old updates that\noccurred while the library was offline, and brings some new features and bug\nfixes.\n\n\nAdditions\n~~~~~~~~~\n\n- Add ``search``, ``filter`` and ``from_user`` parameters to\n  `telethon.telegram_client.TelegramClient.iter_messages`.\n- `telethon.telegram_client.TelegramClient.download_file` now\n  supports a `None` path to return the file in memory and\n  return its `bytes`.\n- Events now have a ``.original_update`` field.\n\nBug fixes\n~~~~~~~~~\n\n- Fixed a race condition when receiving items from the network.\n- A disconnection is made when \"retries reached 0\". This hasn't been\n  tested but it might fix the bug.\n- ``reply_to`` would not override :tl:`Message` object's reply value.\n- Add missing caption when sending :tl:`Message` with media.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Retry automatically on ``RpcCallFailError``. This error happened a lot\n  when iterating over many messages, and retrying often fixes it.\n- Faster `telethon.telegram_client.TelegramClient.iter_messages` by\n  sleeping only as much as needed.\n- `telethon.telegram_client.TelegramClient.edit_message` now supports\n  omitting the entity if you pass a :tl:`Message`.\n- `telethon.events.raw.Raw` can now be filtered by type.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- The library now distinguishes between MTProto and API schemas.\n- :tl:`State` is now persisted to the session file.\n- Connection won't retry forever.\n- Fixed some errors and cleaned up the generation of code.\n- Fixed typos and enhanced some documentation in general.\n- Add auto-cast for :tl:`InputMessage` and :tl:`InputLocation`.\n\n\nPickle-able objects (v0.18.3)\n=============================\n\n*Published at 2018/04/15*\n\n\nNow you can use Python's ``pickle`` module to serialize ``RPCError`` and\nany other ``TLObject`` thanks to **@vegeta1k95**! A fix that was fairly\nsimple, but still might be useful for many people.\n\nAs a side note, the documentation at https://tl.telethon.dev\nnow lists known ``RPCError`` for all requests, so you know what to expect.\nThis required a major rewrite, but it was well worth it!\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n- `telethon.telegram_client.TelegramClient.forward_messages` now returns\n  a single item instead of a list if the input was also a single item.\n\nAdditions\n~~~~~~~~~\n\n- New `telethon.events.messageread.MessageRead` event, to find out when\n  and who read which messages as soon as it happens.\n- Now you can access ``.chat_id`` on all events and ``.sender_id`` on some.\n\nBug fixes\n~~~~~~~~~\n\n- Possibly fix some bug regarding lost ``GzipPacked`` requests.\n- The library now uses the \"real\" layer 75, hopefully.\n- Fixed ``.entities`` name collision on updates by making it private.\n- ``AUTH_KEY_DUPLICATED`` is handled automatically on connection.\n- Markdown parser's offset uses ``match.start()`` to allow custom regex.\n- Some filter types (as a type) were not supported by\n  `telethon.telegram_client.TelegramClient.iter_participants`.\n- `telethon.telegram_client.TelegramClient.remove_event_handler` works.\n- `telethon.telegram_client.TelegramClient.start` works on all terminals.\n- :tl:`InputPeerSelf` case was missing from\n  `telethon.telegram_client.TelegramClient.get_input_entity`.\n\nEnhancements\n~~~~~~~~~~~~\n\n- The ``parse_mode`` for messages now accepts a callable.\n- `telethon.telegram_client.TelegramClient.download_media` accepts web previews.\n- `telethon.tl.custom.dialog.Dialog` instances can now be casted into\n  :tl:`InputPeer`.\n- Better logging when reading packages \"breaks\".\n- Better and more powerful ``setup.py gen`` command.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- The library won't call ``.get_dialogs()`` on entity not found. Instead,\n  it will ``raise ValueError()`` so you can properly ``except`` it.\n- Several new examples and updated documentation.\n- ``py:obj`` is the default Sphinx's role which simplifies ``.rst`` files.\n- ``setup.py`` now makes use of ``python_requires``.\n- Events now live in separate files.\n- Other minor changes.\n\n\nSeveral bug fixes (v0.18.2)\n===========================\n\n*Published at 2018/03/27*\n\nJust a few bug fixes before they become too many.\n\nAdditions\n~~~~~~~~~\n\n- Getting an entity by its positive ID should be enough, regardless of their\n  type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although\n  wrapping them inside a ``Peer`` is still recommended, it's not necessary.\n- New ``client.edit_2fa`` function to change your Two Factor Authentication\n  settings.\n- ``.stringify()`` and string representation for custom ``Dialog/Draft``.\n\nBug fixes\n~~~~~~~~~\n\n- Some bug regarding ``.get_input_peer``.\n- ``events.ChatAction`` wasn't picking up all the pins.\n- ``force_document=True`` was being ignored for albums.\n- Now you're able to send ``Photo`` and ``Document`` as files.\n- Wrong access to a member on chat forbidden error for ``.get_participants``.\n  An empty list is returned instead.\n- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if\n  someone has \"me\" or \"self\" as their name they won't be retrieved.\n\n\nIterator methods (v0.18.1)\n==========================\n\n*Published at 2018/03/17*\n\nAll the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_``\ncounterpart, so you can do operations while retrieving items from them.\nFor instance, you can ``client.iter_dialogs()`` and ``break`` once you\nfind what you're looking for instead fetching them all at once.\n\nAnother big thing, you can get entities by just their positive ID. This\nmay cause some collisions (although it's very unlikely), and you can (should)\nstill be explicit about the type you want. However, it's a lot more convenient\nand less confusing.\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n- The library only offers the default ``SQLiteSession`` again.\n  See :ref:`sessions` for more on how to use a different storage from now on.\n\nAdditions\n~~~~~~~~~\n\n- Events now override ``__str__`` and implement ``.stringify()``, just like\n  every other ``TLObject`` does.\n- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and\n  :meth:`delete` for the message that triggered it.\n- :meth:`client.iter_participants` (and its :meth:`client.get_participants`\n  counterpart) now expose the ``filter`` argument, and the returned users\n  also expose the ``.participant`` they are.\n- You can now use :meth:`client.remove_event_handler` and\n  :meth:`client.list_event_handlers` similar how you could with normal updates.\n- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif``\n  to access only specific types of documents.\n- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a\n  new :meth:`Draft.send` to send it.\n\nBug fixes\n~~~~~~~~~\n\n- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments.\n- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``.\n- Bot API style IDs not working on :meth:`client.get_input_entity`.\n- :meth:`client.download_media` didn't support ``PhotoSize``.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some\n  events (mostly those that occur in a channel).\n- You can send albums larger than 10 items (they will be sliced for you),\n  as well as mixing normal files with photos.\n- ``TLObject`` now have Python type hints.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Several documentation corrections.\n- :meth:`client.get_dialogs` is only called once again when an entity is\n  not found to avoid flood waits.\n\n\nSessions overhaul (v0.18)\n=========================\n\n*Published at 2018/03/04*\n\n+-----------------------+\n| Scheme layer used: 75 |\n+-----------------------+\n\nThe ``Session``'s have been revisited thanks to the work of **@tulir** and\nthey now use an `ABC <https://docs.python.org/3/library/abc.html>`__ so you\ncan easily implement your own!\n\nThe default will still be a ``SQLiteSession``, but you might want to use\nthe new ``AlchemySessionContainer`` if you need. Refer to the section of\nthe documentation on :ref:`sessions` for more.\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n- ``events.MessageChanged`` doesn't exist anymore. Use the new\n  ``events.MessageEdited`` and ``events.MessageDeleted`` instead.\n\nAdditions\n~~~~~~~~~\n\n- The mentioned addition of new session types.\n- You can omit the event type on ``client.add_event_handler`` to use ``Raw``.\n- You can ``raise StopPropagation`` of events if you added several of them.\n- ``.get_participants()`` can now get up to 90,000 members from groups with\n  100,000 if when ``aggressive=True``, \"bypassing\" Telegram's limit.\n- You now can access ``NewMessage.Event.pattern_match``.\n- Multiple captions are now supported when sending albums.\n- ``client.send_message()`` has an optional ``file=`` parameter, so\n  you can do ``events.reply(file='/path/to/photo.jpg')`` and similar.\n- Added ``.input_`` versions to ``events.ChatAction``.\n- You can now access the public ``.client`` property on ``events``.\n- New ``client.forward_messages``, with its own wrapper on ``events``,\n  called ``event.forward_to(...)``.\n\n\nBug fixes\n~~~~~~~~~\n\n- Silly bug regarding ``client.get_me(input_peer=True)``.\n- ``client.send_voice_note()`` was missing some parameters.\n- ``client.send_file()`` plays better with streams now.\n- Incoming messages from bots weren't working with whitelists.\n- Markdown's URL regex was not accepting newlines.\n- Better attempt at joining background update threads.\n- Use the right peer type when a marked integer ID is provided.\n\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Resolving ``events.Raw`` is now a no-op.\n- Logging calls in the ``TcpClient`` to spot errors.\n- ``events`` resolution is postponed until you are successfully connected,\n  so you can attach them before starting the client.\n- When an entity is not found, it is searched in *all* dialogs. This might\n  not always be desirable but it's more comfortable for legitimate uses.\n- Some non-persisting properties from the ``Session`` have been moved out.\n\n\nFurther easing library usage (v0.17.4)\n======================================\n\n*Published at 2018/02/24*\n\nSome new things and patches that already deserved their own release.\n\n\nAdditions\n~~~~~~~~~\n\n- New ``pattern`` argument to ``NewMessage`` to easily filter messages.\n- New ``.get_participants()`` convenience method to get members from chats.\n- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter.\n- You can now ``.get_entity()`` through exact name match instead username.\n- Raise ``ProxyConnectionError`` instead looping forever so you can\n  ``except`` it on your own code and behave accordingly.\n\nBug fixes\n~~~~~~~~~\n\n- ``.parse_username`` would fail with ``www.`` or a trailing slash.\n- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``.\n- You can now send ``b'byte strings'`` directly as files again.\n- ``.send_file()`` was not respecting the original captions when passing\n  another message (or media) as the file.\n- Downloading media from a different data center would always log a warning\n  for the first time.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``.\n- You can use ``.get_me(input_peer=True)`` if all you need is your self ID.\n- New addition to the interactive client example to show peer information.\n- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so\n  you can always safely rely on ``.sender`` to get the right ID.\n\n\nNew small convenience functions (v0.17.3)\n=========================================\n\n*Published at 2018/02/18*\n\nMore bug fixes and a few others addition to make events easier to use.\n\nAdditions\n~~~~~~~~~\n\n- Use ``hachoir`` to extract video and audio metadata before upload.\n- New ``.add_event_handler``, ``.add_update_handler`` now deprecated.\n\nBug fixes\n~~~~~~~~~\n\n- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password``\n  (now it will ask you for it if you don't provide it, as docstring hinted).\n- ``.edit_message()`` was ignoring the formatting (e.g. markdown).\n- Added missing case to the ``NewMessage`` event for normal groups.\n- Accessing the ``.text`` of the ``NewMessage`` event was failing due\n  to a bug with the markdown unparser.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead,\n  which you can find on https://pypi.org/project/cryptg/.\n\n\n\nNew small convenience functions (v0.17.2)\n=========================================\n\n*Published at 2018/02/15*\n\nPrimarily bug fixing and a few welcomed additions.\n\nAdditions\n~~~~~~~~~\n\n- New convenience ``.edit_message()`` method on the ``TelegramClient``.\n- New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event.\n- Default to markdown parsing when sending and editing messages.\n- Support for inline mentions when sending and editing messages. They work\n  like inline urls (e.g. ``[text](@username)``) and also support the Bot-API\n  style (see `here <https://core.telegram.org/bots/api#formatting-options>`__).\n\nBug fixes\n~~~~~~~~~\n\n- Periodically send ``GetStateRequest`` automatically to keep the server\n  sending updates even if you're not invoking any request yourself.\n- HTML parsing was failing due to not handling surrogates properly.\n- ``.sign_up`` was not accepting ``int`` codes.\n- Whitelisting more than one chat on ``events`` wasn't working.\n- Video files are sent as a video by default unless ``force_document``.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- More ``logging`` calls to help spot some bugs in the future.\n- Some more logic to retrieve input entities on events.\n- Clarified a few parts of the documentation.\n\n\nUpdates as Events (v0.17.1)\n===========================\n\n*Published at 2018/02/09*\n\nOf course there was more work to be done regarding updates, and it's here!\nThe library comes with a new ``events`` module (which you will often import\nas ``from telethon import TelegramClient, events``). This are pretty much\nall the additions that come with this version change, but they are a nice\naddition. Refer to *(removed broken link)* to get started with events.\n\n\nTrust the Server with Updates (v0.17)\n=====================================\n\n*Published at 2018/02/03*\n\nThe library trusts the server with updates again. The library will *not*\ncheck for duplicates anymore, and when the server kicks us, it will run\n``GetStateRequest`` so the server starts sending updates again (something\nit wouldn't do unless you invoked something, it seems). But this update\nalso brings a few more changes!\n\nAdditions\n~~~~~~~~~\n\n- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them.\n- Added some missing cases on ``.get_input_entity()`` and peer functions.\n- ``obj.to_dict()`` now has a ``'_'`` key with the type used.\n- ``.start()`` can also sign up now.\n- More parameters for ``.get_message_history()``.\n- Updated list of RPC errors.\n- HTML parsing thanks to **@tulir**! It can be used similar to markdown:\n  ``client.send_message(..., parse_mode='html')``.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n- ``client.send_file()`` now accepts ``Message``'s and\n  ``MessageMedia``'s as the ``file`` parameter.\n- Some documentation updates and fixed to clarify certain things.\n- New exact match feature on https://tl.telethon.dev.\n- Return as early as possible from ``.get_input_entity()`` and similar,\n  to avoid penalizing you for doing this right.\n\nBug fixes\n~~~~~~~~~\n\n- ``.download_media()`` wouldn't accept a ``Document`` as parameter.\n- The SQLite is now closed properly on disconnection.\n- IPv6 addresses shouldn't use square braces.\n- Fix regarding ``.log_out()``.\n- The time offset wasn't being used (so having wrong system time would\n  cause the library not to work at all).\n\n\nNew ``.resolve()`` method (v0.16.2)\n===================================\n\n*Published at 2018/01/19*\n\nThe ``TLObject``'s (instances returned by the API and ``Request``'s) have\nnow acquired a new ``.resolve()`` method. While this should be used by the\nlibrary alone (when invoking a request), it means that you can now use\n``Peer`` types or even usernames where a ``InputPeer`` is required. The\nobject now has access to the ``client``, so that it can fetch the right\ntype if needed, or access the session database. Furthermore, you can\nreuse requests that need \"autocast\" (e.g. you put :tl:`User` but ``InputPeer``\nwas needed), since ``.resolve()`` is called when invoking. Before, it was\nonly done on object construction.\n\nAdditions\n~~~~~~~~~\n\n- Album support. Just pass a list, tuple or any iterable to ``.send_file()``.\n\n\nEnhancements\n~~~~~~~~~~~~\n\n- ``.start()`` asks for your phone only if required.\n- Better file cache. All files under 10MB, once uploaded, should never be\n  needed to be re-uploaded again, as the sent media is cached to the session.\n\n\nBug fixes\n~~~~~~~~~\n\n- ``setup.py`` now calls ``gen_tl`` when installing the library if needed.\n\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- The mentioned ``.resolve()`` to perform \"autocast\", more powerful.\n- Upload and download methods are no longer part of ``TelegramBareClient``.\n- Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``.\n  Only override ``.on_response()`` if necessary (small amount of cases).\n- Reduced \"autocast\" overhead as much as possible.\n  You shouldn't be penalized if you've provided the right type.\n\n\nMtProto 2.0 (v0.16.1)\n=====================\n\n*Published at 2018/01/11*\n\n+-----------------------+\n| Scheme layer used: 74 |\n+-----------------------+\n\nThe library is now using MtProto 2.0! This shouldn't really affect you\nas an end user, but at least it means the library will be ready by the\ntime MtProto 1.0 is deprecated.\n\nAdditions\n~~~~~~~~~\n\n- New ``.start()`` method, to make the library avoid boilerplate code.\n- ``.send_file`` accepts a new optional ``thumbnail`` parameter, and\n  returns the ``Message`` with the sent file.\n\n\nBug fixes\n~~~~~~~~~\n\n- The library uses again only a single connection. Less updates are\n  be dropped now, and the performance is even better than using temporary\n  connections.\n- ``without rowid`` will only be used on the ``*.session`` if supported.\n- Phone code hash is associated with phone, so you can change your mind\n  when calling ``.sign_in()``.\n\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- File cache now relies on the hash of the file uploaded instead its path,\n  and is now persistent in the ``*.session`` file. Report any bugs on this!\n- Clearer error when invoking without being connected.\n- Markdown parser doesn't work on bytes anymore (which makes it cleaner).\n\n\nSessions as sqlite databases (v0.16)\n====================================\n\n*Published at 2017/12/28*\n\nIn the beginning, session files used to be pickle. This proved to be bad\nas soon as one wanted to add more fields. For this reason, they were\nmigrated to use JSON instead. But this proved to be bad as soon as one\nwanted to save things like entities (usernames, their ID and hash), so\nnow it properly uses\n`sqlite3 <https://docs.python.org/3/library/sqlite3.html>`__,\nwhich has been well tested, to save the session files! Calling\n``.get_input_entity`` using a ``username`` no longer will need to fetch\nit first, so it's really 0 calls again. Calling ``.get_entity`` will\nalways fetch the most up to date version.\n\nFurthermore, nearly everything has been documented, thus preparing the\nlibrary for `Read the Docs <https://readthedocs.org/>`__ (although there\nare a few things missing I'd like to polish first), and the\n`logging <https://docs.python.org/3/library/logging.html>`__ are now\nbetter placed.\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n-  ``.get_dialogs()`` now returns a **single list** instead a tuple\n   consisting of a **custom class** that should make everything easier\n   to work with.\n-  ``.get_message_history()`` also returns a **single list** instead a\n   tuple, with the ``Message`` instances modified to make them more\n   convenient.\n\nBoth lists have a ``.total`` attribute so you can still know how many\ndialogs/messages are in total.\n\nAdditions\n~~~~~~~~~\n\n-  The mentioned use of ``sqlite3`` for the session file.\n-  ``.get_entity()`` now supports lists too, and it will make as little\n   API calls as possible if you feed it ``InputPeer`` types. Usernames\n   will always be resolved, since they may have changed.\n-  ``.set_proxy()`` method, to avoid having to create a new\n   ``TelegramClient``.\n-  More ``date`` types supported to represent a date parameter.\n\nBug fixes\n~~~~~~~~~\n\n-  Empty strings weren't working when they were a flag parameter (e.g.,\n   setting no last name).\n-  Fix invalid assertion regarding flag parameters as well.\n-  Avoid joining the background thread on disconnect, as it would be\n   `None` due to a race condition.\n-  Correctly handle `None` dates when downloading media.\n-  ``.download_profile_photo`` was failing for some channels.\n-  ``.download_media`` wasn't handling ``Photo``.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  ``date`` was being serialized as local date, but that was wrong.\n-  ``date`` was being represented as a ``float`` instead of an ``int``.\n-  ``.tl`` parser wasn't stripping inline comments.\n-  Removed some redundant checks on ``update_state.py``.\n-  Use a `synchronized\n   queue <https://docs.python.org/3/library/queue.html>`__ instead a\n   hand crafted version.\n-  Use signed integers consistently (e.g. ``salt``).\n-  Always read the corresponding ``TLObject`` from API responses, except\n   for some special cases still.\n-  A few more ``except`` low level to correctly wrap errors.\n-  More accurate exception types.\n-  ``invokeWithLayer(initConnection(X))`` now wraps every first request\n   after ``.connect()``.\n\nAs always, report if you have issues with some of the changes!\n\nIPv6 support (v0.15.5)\n======================\n\n*Published at 2017/11/16*\n\n+-----------------------+\n| Scheme layer used: 73 |\n+-----------------------+\n\nIt's here, it has come! The library now **supports IPv6**! Just pass\n``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could\n*not* test this feature because my machine doesn't have IPv6 setup. If\nyou know IPv6 works in your machine but the library doesn't, please\nrefer to `#425 <https://github.com/LonamiWebs/Telethon/issues/425>`_.\n\nAdditions\n~~~~~~~~~\n\n-  IPv6 support.\n-  New method to extract the text surrounded by ``MessageEntity``\\ 's,\n   in the ``extensions.markdown`` module.\n\nEnhancements\n~~~~~~~~~~~~\n\n-  Markdown parsing is Done Right.\n-  Reconnection on failed invoke. Should avoid \"number of retries\n   reached 0\" (#270).\n-  Some missing autocast to ``Input*`` types.\n-  The library uses the ``NullHandler`` for ``logging`` as it should\n   have always done.\n-  ``TcpClient.is_connected()`` is now more reliable.\n\n.. bug-fixes-1:\n\nBug fixes\n~~~~~~~~~\n\n-  Getting an entity using their phone wasn't actually working.\n-  Full entities aren't saved unless they have an ``access_hash``, to\n   avoid some `None` errors.\n-  ``.get_message_history`` was failing when retrieving items that had\n   messages forwarded from a channel.\n\nGeneral enhancements (v0.15.4)\n==============================\n\n*Published at 2017/11/04*\n\n+-----------------------+\n| Scheme layer used: 72 |\n+-----------------------+\n\nThis update brings a few general enhancements that are enough to deserve\na new release, with a new feature: beta **markdown-like parsing** for\n``.send_message()``!\n\n.. additions-1:\n\nAdditions\n~~~~~~~~~\n\n-  ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It\n   works in a similar fashion to the official clients (defaults to\n   double underscore/asterisk, like ``**this**``). Please report any\n   issues with emojies or enhancements for the parser!\n-  New ``.idle()`` method so your main thread can do useful job (listen\n   for updates).\n-  Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for\n   ``TLMessage`` and ``MessageContainer``.\n\n.. bug-fixes-2:\n\nBug fixes\n~~~~~~~~~\n\n-  The list of known peers could end \"corrupted\" and have users with\n   ``access_hash=None``, resulting in ``struct`` error for it not being\n   an integer. You shouldn't encounter this issue anymore.\n-  The warning for \"added update handler but no workers set\" wasn't\n   actually working.\n-  ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``.\n-  There used to be an exception when logging exceptions (whoops) on\n   update handlers.\n-  \"Downloading contacts\" would produce strange output if they had\n   semicolons (``;``) in their name.\n-  Fix some cyclic imports and installing dependencies from the ``git``\n   repository.\n-  Code generation was using f-strings, which are only supported on\n   Python ≥3.6.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  The ``auth_key`` generation has been moved from ``.connect()`` to\n   ``.invoke()``. There were some issues were ``.connect()`` failed and\n   the ``auth_key`` was `None` so this will ensure to have a valid\n   ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised.\n-  Support for higher limits on ``.get_history()`` and\n   ``.get_dialogs()``.\n-  Much faster integer factorization when generating the required\n   ``auth_key``. Thanks @delivrance for making me notice this, and for\n   the pull request.\n\nBug fixes with updates (v0.15.3)\n================================\n\n*Published at 2017/10/20*\n\nHopefully a very ungrateful bug has been removed. When you used to\ninvoke some request through update handlers, it could potentially enter\nan infinite loop. This has been mitigated and it's now safe to invoke\nthings again! A lot of updates were being dropped (all those gzipped),\nand this has been fixed too.\n\nMore bug fixes include a `correct\nparsing <https://github.com/LonamiWebs/Telethon/commit/ee01724cdb7027c1e38625d31446ba1ea7bade92>`__\nof certain TLObjects thanks to @stek29, and\n`some <https://github.com/LonamiWebs/Telethon/commit/ed77ba6f8ff115ac624f02f691c9991e5b37be60>`__\n`wrong\ncalls <https://github.com/LonamiWebs/Telethon/commit/16cf94c9add5e94d70c4eee2ac142d8e76af48b9>`__\nthat would cause the library to crash thanks to @andr-04, and the\n``ReadThread`` not re-starting if you were already authorized.\n\nInternally, the ``.to_bytes()`` function has been replaced with\n``__bytes__`` so now you can do ``bytes(tlobject)``.\n\nBug fixes and new small features (v0.15.2)\n==========================================\n\n*Published at 2017/10/14*\n\nThis release primarly focuses on a few bug fixes and enhancements.\nAlthough more stuff may have broken along the way.\n\nEnhancements\n~~~~~~~~~~~~\n\n-  You will be warned if you call ``.add_update_handler`` with no\n   ``update_workers``.\n-  New customizable threshold value on the session to determine when to\n   automatically sleep on flood waits. See\n   ``client.session.flood_sleep_threshold``.\n-  New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa.\n-  Join all threads when calling ``.disconnect()``, to assert no\n   dangling thread is left alive.\n-  Larger chunk when downloading files should result in faster\n   downloads.\n-  You can use a callable key for the ``EntityDatabase``, so it can be\n   any filter you need.\n\n.. bug-fixes-3:\n\nBug fixes\n~~~~~~~~~\n\n-  ``.get_input_entity`` was failing for IDs and other cases, also\n   making more requests than it should.\n-  Use ``basename`` instead ``abspath`` when sending a file. You can now\n   also override the attributes.\n-  ``EntityDatabase.__delitem__`` wasn't working.\n-  ``.send_message()`` was failing with channels.\n-  ``.get_dialogs(limit=None)`` should now return all the dialogs\n   correctly.\n-  Temporary fix for abusive duplicated updates.\n\n.. enhancements-1:\n\n.. internal-changes-1:\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  MsgsAck is now sent in a container rather than its own request.\n-  ``.get_input_photo`` is now used in the generated code.\n-  ``.process_entities`` was being called from more places than only\n   ``__call__``.\n-  ``MtProtoSender`` now relies more on the generated code to read\n   responses.\n\nCustom Entity Database (v0.15.1)\n================================\n\n*Published at 2017/10/05*\n\nThe main feature of this release is that Telethon now has a custom\ndatabase for all the entities you encounter, instead depending on\n``@lru_cache`` on the ``.get_entity()`` method.\n\nThe ``EntityDatabase`` will, by default, **cache** all the users, chats\nand channels you find in memory for as long as the program is running.\nThe session will, by default, save all key-value pairs of the entity\nidentifiers and their hashes (since Telegram may send an ID that it\nthinks you already know about, we need to save this information).\n\nYou can **prevent** the ``EntityDatabase`` from saving users by setting\n``client.session.entities.enabled = False``, and prevent the ``Session``\nfrom saving input entities at all by setting\n``client.session.save_entities = False``. You can also clear the cache\nfor a certain user through\n``client.session.entities.clear_cache(entity=None)``, which will clear\nall if no entity is given.\n\n\nAdditions\n~~~~~~~~~\n\n- New method to ``.delete_messages()``.\n- New ``ChannelPrivateError`` class.\n\nEnhancements\n~~~~~~~~~~~~\n\n- ``.sign_in`` accepts phones as integers.\n- Changing the IP to which you connect to is as simple as\n  ``client.session.server_address = 'ip'``, since now the\n  server address is always queried from the session.\n\nBug fixes\n~~~~~~~~~\n\n- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the\n  right amount of dialogs.\n- ``GeneralProxyError`` should be passed to the main thread\n  again, so that you can handle it.\n\nUpdates Overhaul Update (v0.15)\n===============================\n\n*Published at 2017/10/01*\n\nAfter hundreds of lines changed on a major refactor, *it's finally\nhere*. It's the **Updates Overhaul Update**; let's get right into it!\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n-  ``.create_new_connection()`` is gone for good. No need to deal with\n   this manually since new connections are now handled on demand by the\n   library itself.\n\nEnhancements\n~~~~~~~~~~~~\n\n-  You can **invoke** requests from **update handlers**. And **any other\n   thread**. A new temporary will be made, so that you can be sending\n   even several requests at the same time!\n-  **Several worker threads** for your updates! By default, `None`\n   will spawn. I recommend you to work with ``update_workers=4`` to get\n   started, these will be polling constantly for updates.\n-  You can also change the number of workers at any given time.\n-  The library can now run **in a single thread** again, if you don't\n   need to spawn any at all. Simply set ``spawn_read_thread=False`` when\n   creating the ``TelegramClient``!\n-  You can specify ``limit=None`` on ``.get_dialogs()`` to get **all**\n   of them[1].\n-  **Updates are expanded**, so you don't need to check if the update\n   has ``.updates`` or an inner ``.update`` anymore.\n-  All ``InputPeer`` entities are **saved in the session** file, but you\n   can disable this by setting ``save_entities=False``.\n-  New ``.get_input_entity`` method, which makes use of the above\n   feature. You **should use this** when a request needs a\n   ``InputPeer``, rather than the whole entity (although both work).\n-  Assert that either all or None dependent-flag parameters are set\n   before sending the request.\n-  Phone numbers can have dashes, spaces, or parenthesis. They'll be\n   removed before making the request.\n-  You can override the phone and its hash on ``.sign_in()``, if you're\n   creating a new ``TelegramClient`` on two different places.\n\nBug fixes\n~~~~~~~~~\n\n-  ``.log_out()`` was consuming all retries. It should work just fine\n   now.\n-  The session would fail to load if the ``auth_key`` had been removed\n   manually.\n-  ``Updates.check_error`` was popping wrong side, although it's been\n   completely removed.\n-  ``ServerError``\\ 's will be **ignored**, and the request will\n   immediately be retried.\n-  Cross-thread safety when saving the session file.\n-  Some things changed on a matter of when to reconnect, so please\n   report any bugs!\n\n.. internal-changes-2:\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  ``TelegramClient`` is now only an abstraction over the\n   ``TelegramBareClient``, which can only do basic things, such as\n   invoking requests, working with files, etc. If you don't need any of\n   the abstractions the ``TelegramClient``, you can now use the\n   ``TelegramBareClient`` in a much more comfortable way.\n-  ``MtProtoSender`` is not thread-safe, but it doesn't need to be since\n   a new connection will be spawned when needed.\n-  New connections used to be cached and then reused. Now only their\n   sessions are saved, as temporary connections are spawned only when\n   needed.\n-  Added more RPC errors to the list.\n\n**[1]:** Broken due to a condition which should had been the opposite\n(sigh), fixed 4 commits ahead on\nhttps://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c.\n\n--------------\n\n**That's pretty much it**, although there's more work to be done to make\nthe overall experience of working with updates *even better*. Stay\ntuned!\n\nSerialization bug fixes (v0.14.2)\n=================================\n\n*Published at 2017/09/29*\n\nBug fixes\n~~~~~~~~~\n\n- **Important**, related to the serialization. Every object or request\n  that had to serialize a ``True/False`` type was always being serialized\n  as `false`!\n- Another bug that didn't allow you to leave as `None` flag parameters\n  that needed a list has been fixed.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Other internal changes include a somewhat more readable ``.to_bytes()``\n  function and pre-computing the flag instead using bit shifting. The\n  ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``,\n  and ``.subclass_of_id`` is also uppercase now.\n\nFarewell, BinaryWriter (v0.14.1)\n================================\n\n*Published at 2017/09/28*\n\nVersion ``v0.14`` had started working on the new ``.to_bytes()`` method\nto dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when\nserializing TLObjects, and this release finally removes it. The speed up\nwhen serializing things to bytes should now be over twice as fast\nwherever it's needed.\n\nBug fixes\n~~~~~~~~~\n\n- This version is again compatible with Python 3.x versions **below 3.5**\n  (there was a method call that was Python 3.5 and above).\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- Using proper classes (including the generated code) for generating\n  authorization keys and to write out ``TLMessage``\\ 's.\n\n\nSeveral requests at once and upload compression (v0.14)\n=======================================================\n\n*Published at 2017/09/27*\n\nNew major release, since I've decided that these two features are big\nenough:\n\nAdditions\n~~~~~~~~~\n\n- Requests larger than 512 bytes will be **compressed through\n  gzip**, and if the result is smaller, this will be uploaded instead.\n- You can now send **multiple requests at once**, they're simply\n  ``*var_args`` on the ``.invoke()``. Note that the server doesn't\n  guarantee the order in which they'll be executed!\n\nInternally, another important change. The ``.on_send`` function on the\n``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From\nmy tests, this has always been over twice as fast serializing objects,\nalthough more replacements need to be done, so please report any issues.\n\nEnhancements\n~~~~~~~~~~~~\n- Implemented ``.get_input_media`` helper methods. Now you can even use\n  another message as input media!\n\n\nBug fixes\n~~~~~~~~~\n\n- Downloading media from CDNs wasn't working (wrong\n  access to a parameter).\n- Correct type hinting.\n- Added a tiny sleep when trying to perform automatic reconnection.\n- Error reporting is done in the background, and has a shorter timeout.\n- ``setup.py`` used to fail with wrongly generated code.\n\nQuick fix-up (v0.13.6)\n======================\n\n*Published at 2017/09/23*\n\nBefore getting any further, here's a quick fix-up with things that\nshould have been on ``v0.13.5`` but were missed. Specifically, the\n**timeout when receiving** a request will now work properly.\n\nSome other additions are a tiny fix when **handling updates**, which was\nignoring some of them, nicer ``__str__`` and ``.stringify()`` methods\nfor the ``TLObject``\\ 's, and not stopping the ``ReadThread`` if you try\ninvoking something there (now it simply returns `None`).\n\nAttempts at more stability (v0.13.5)\n====================================\n\n*Published at 2017/09/23*\n\nYet another update to fix some bugs and increase the stability of the\nlibrary, or, at least, that was the attempt!\n\nThis release should really **improve the experience with the background\nthread** that the library starts to read things from the network as soon\nas it can, but I can't spot every use case, so please report any bug\n(and as always, minimal reproducible use cases will help a lot).\n\n.. bug-fixes-4:\n\nBug fixes\n~~~~~~~~~\n\n-  ``setup.py`` was failing on Python < 3.5 due to some imports.\n-  Duplicated updates should now be ignored.\n-  ``.send_message`` would crash in some cases, due to having a typo\n   using the wrong object.\n-  ``\"socket is None\"`` when calling ``.connect()`` should not happen\n   anymore.\n-  ``BrokenPipeError`` was still being raised due to an incorrect order\n   on the ``try/except`` block.\n\n.. enhancements-2:\n\nEnhancements\n~~~~~~~~~~~~\n\n-  **Type hinting** for all the generated ``Request``\\ 's and\n   ``TLObjects``! IDEs like PyCharm will benefit from this.\n-  ``ProxyConnectionError`` should properly be passed to the main thread\n   for you to handle.\n-  The background thread will only be started after you're authorized on\n   Telegram (i.e. logged in), and several other attempts at polishing\n   the experience with this thread.\n-  The ``Connection`` instance is only created once now, and reused\n   later.\n-  Calling ``.connect()`` should have a better behavior now (like\n   actually *trying* to connect even if we seemingly were connected\n   already).\n-  ``.reconnect()`` behavior has been changed to also be more consistent\n   by making the assumption that we'll only reconnect if the server has\n   disconnected us, and is now private.\n\n.. other-changes-1:\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  ``TLObject.__repr__`` doesn't show the original TL definition\n   anymore, it was a lot of clutter. If you have any complaints open an\n   issue and we can discuss it.\n-  Internally, the ``'+'`` from the phone number is now stripped, since\n   it shouldn't be included.\n-  Spotted a new place where ``BrokenAuthKeyError`` would be raised, and\n   it now is raised there.\n\nMore bug fixes and enhancements (v0.13.4)\n=========================================\n\n*Published at 2017/09/18*\n\n.. new-stuff-1:\n\nAdditions\n~~~~~~~~~\n\n-  ``TelegramClient`` now exposes a ``.is_connected()`` method.\n-  Initial authorization on a new data center will retry up to 5 times\n   by default.\n-  Errors that couldn't be handled on the background thread will be\n   raised on the next call to ``.invoke()`` or ``updates.poll()``.\n\n.. bugs-fixed-1:\n\nBug fixes\n~~~~~~~~~~\n\n-  Now you should be able to sign in even if you have\n   ``process_updates=True`` and no previous session.\n-  Some errors and methods are documented a bit clearer.\n-  ``.send_message()`` could randomly fail, as the returned type was not\n   expected.\n-  ``TimeoutError`` is now ignored, since the request will be retried up\n   to 5 times by default.\n-  \"-404\" errors (``BrokenAuthKeyError``\\ 's) are now detected when\n   first connecting to a new data center.\n-  ``BufferError`` is handled more gracefully, in the same way as\n   ``InvalidCheckSumError``\\ 's.\n-  Attempt at fixing some \"NoneType has no attribute…\" errors (with the\n   ``.sender``).\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  Calling ``GetConfigRequest`` is now made less often.\n-  The ``initial_query`` parameter from ``.connect()`` is gone, as it's\n   not needed anymore.\n-  Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since\n   it's a constant).\n-  The message from ``BufferError`` is now more useful.\n\nBug fixes and enhancements (v0.13.3)\n====================================\n\n*Published at 2017/09/14*\n\n.. bugs-fixed-2:\n\nBug fixes\n~~~~~~~~~\n\n-  **Reconnection** used to fail because it tried invoking things from\n   the ``ReadThread``.\n-  Inferring **random ids** for ``ForwardMessagesRequest`` wasn't\n   working.\n-  Downloading media from **CDNs** failed due to having forgotten to\n   remove a single line.\n-  ``TcpClient.close()`` now has a **``threading.Lock``**, so\n   ``NoneType has no close()`` should not happen.\n-  New **workaround** for ``msg seqno too low/high``. Also, both\n   ``Session.id/seq`` are not saved anymore.\n\n.. enhancements-3:\n\nEnhancements\n~~~~~~~~~~~~\n\n-  **Request will be retried** up to 5 times by default rather than\n   failing on the first attempt.\n-  ``InvalidChecksumError``\\ 's are now **ignored** by the library.\n-  ``TelegramClient.get_entity()`` is now **public**, and uses the\n   ``@lru_cache()`` decorator.\n-  New method to **``.send_voice_note()``**\\ 's.\n-  Methods to send message and media now support a **``reply_to``\n   parameter**.\n-  ``.send_message()`` now returns the **full message** which was just\n   sent.\n\nNew way to work with updates (v0.13.2)\n======================================\n\n*Published at 2017/09/08*\n\nThis update brings a new way to work with updates, and it's begging for\nyour **feedback**, or better names or ways to do what you can do now.\n\nPlease refer to the `wiki/Usage\nModes <https://github.com/LonamiWebs/Telethon/wiki/Usage-Modes>`__ for\nan in-depth description on how to work with updates now. Notice that you\ncannot invoke requests from within handlers anymore, only the\n``v.0.13.1`` patch allowed you to do so.\n\nBug fixes\n~~~~~~~~~\n\n- Periodic pings are back.\n- The username regex mentioned on ``UsernameInvalidError`` was invalid,\n  but it has now been fixed.\n- Sending a message to a phone number was failing because the type used\n  for a request had changed on layer 71.\n- CDN downloads weren't working properly, and now a few patches have been\n  applied to ensure more reliability, although I couldn't personally test\n  this, so again, report any feedback.\n\nInvoke other requests from within update callbacks (v0.13.1)\n============================================================\n\n*Published at 2017/09/04*\n\n.. warning::\n\n    This update brings some big changes to the update system,\n    so please read it if you work with them!\n\nA silly \"bug\" which hadn't been spotted has now been fixed. Now you can\ninvoke other requests from within your update callbacks. However **this\nis not advised**. You should post these updates to some other thread,\nand let that thread do the job instead. Invoking a request from within a\ncallback will mean that, while this request is being invoked, no other\nthings will be read.\n\nInternally, the generated code now resides under a *lot* less files,\nsimply for the sake of avoiding so many unnecessary files. The generated\ncode is not meant to be read by anyone, simply to do its job.\n\nUnused attributes have been removed from the ``TLObject`` class too, and\n``.sign_up()`` returns the user that just logged in in a similar way to\n``.sign_in()`` now.\n\nConnection modes (v0.13)\n========================\n\n*Published at 2017/09/04*\n\n+-----------------------+\n| Scheme layer used: 71 |\n+-----------------------+\n\nThe purpose of this release is to denote a big change, now you can\nconnect to Telegram through different `**connection\nmodes** <https://github.com/LonamiWebs/Telethon/blob/v0.13/telethon/network/connection.py>`__.\nAlso, a **second thread** will *always* be started when you connect a\n``TelegramClient``, despite whether you'll be handling updates or\nignoring them, whose sole purpose is to constantly read from the\nnetwork.\n\nThe reason for this change is as simple as *\"reading and writing\nshouldn't be related\"*. Even when you're simply ignoring updates, this\nway, once you send a request you will only need to read the result for\nthe request. Whatever Telegram sent before has already been read and\noutside the buffer.\n\n.. additions-2:\n\nAdditions\n~~~~~~~~~\n\n-  The mentioned different connection modes, and a new thread.\n-  You can modify the ``Session`` attributes through the\n   ``TelegramClient`` constructor (using ``**kwargs``).\n-  ``RPCError``\\ 's now belong to some request you've made, which makes\n   more sense.\n-  ``get_input_*`` now handles `None` (default) parameters more\n   gracefully (it used to crash).\n\n.. enhancements-4:\n\nEnhancements\n~~~~~~~~~~~~\n\n-  The low-level socket doesn't use a handcrafted timeout anymore, which\n   should benefit by avoiding the arbitrary ``sleep(0.1)`` that there\n   used to be.\n-  ``TelegramClient.sign_in`` will call ``.send_code_request`` if no\n   ``code`` was provided.\n\nDeprecation\n~~~~~~~~~~~\n\n-  ``.sign_up`` does *not* take a ``phone`` argument anymore. Change\n   this or you will be using ``phone`` as ``code``, and it will fail!\n   The definition looks like\n   ``def sign_up(self, code, first_name, last_name='')``.\n-  The old ``JsonSession`` finally replaces the original ``Session``\n   (which used pickle). If you were overriding any of these, you should\n   only worry about overriding ``Session`` now.\n\nAdded verification for CDN file (v0.12.2)\n=========================================\n\n*Published at 2017/08/28*\n\nSince the Content Distributed Network (CDN) is not handled by Telegram\nitself, the owners may tamper these files. Telegram sends their sha256\nsum for clients to implement this additional verification step, which\nnow the library has. If any CDN has altered the file you're trying to\ndownload, ``CdnFileTamperedError`` will be raised to let you know.\n\nBesides this. ``TLObject.stringify()`` was showing bytes as lists (now\nfixed) and RPC errors are reported by default:\n\n    In an attempt to help everyone who works with the Telegram API,\n    Telethon will by default report all Remote Procedure Call errors to\n    `PWRTelegram <https://pwrtelegram.xyz/>`__, a public database anyone can\n    query, made by `Daniil <https://github.com/danog>`__. All the information\n    sent is a GET request with the error code, error message and method used.\n\n\n.. note::\n\n    If you still would like to opt out, simply set\n    ``client.session.report_errors = False`` to disable this feature.\n    However Daniil would really thank you if you helped him (and everyone)\n    by keeping it on!\n\nCDN support (v0.12.1)\n=====================\n\n*Published at 2017/08/24*\n\nThe biggest news for this update are that downloading media from CDN's\n(you'll often encounter this when working with popular channels) now\n**works**.\n\nBug fixes\n~~~~~~~~~\n\n- The method used to download documents crashed because\n  two lines were swapped.\n- Determining the right path when downloading any file was\n  very weird, now it's been enhanced.\n- The ``.sign_in()`` method didn't support integer values for the code!\n  Now it does again.\n\nSome important internal changes are that the old way to deal with RSA\npublic keys now uses a different module instead the old strange\nhand-crafted version.\n\nHope the new, super simple ``README.rst`` encourages people to use\nTelethon and make it better with either suggestions, or pull request.\nPull requests are *super* appreciated, but showing some support by\nleaving a star also feels nice ⭐️.\n\nNewbie friendly update (v0.12)\n==============================\n\n*Published at 2017/08/22*\n\n+-----------------------+\n| Scheme layer used: 70 |\n+-----------------------+\n\nThis update is overall an attempt to make Telethon a bit more user\nfriendly, along with some other stability enhancements, although it\nbrings quite a few changes.\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n-  The ``TelegramClient`` methods ``.send_photo_file()``,\n   ``.send_document_file()`` and ``.send_media_file()`` are now a\n   **single method** called ``.send_file()``. It's also important to\n   note that the **order** of the parameters has been **swapped**: first\n   to *who* you want to send it, then the file itself.\n\n-  The same applies to ``.download_msg_media()``, which has been renamed\n   to ``.download_media()``. The method now supports a ``Message``\n   itself too, rather than only ``Message.media``. The specialized\n   ``.download_photo()``, ``.download_document()`` and\n   ``.download_contact()`` still exist, but are private.\n\nAdditions\n~~~~~~~~~\n\n-  Updated to **layer 70**!\n-  Both downloading and uploading now support **stream-like objects**.\n-  A lot **faster initial connection** if ``sympy`` is installed (can be\n   installed through ``pip``).\n-  ``libssl`` will also be used if available on your system (likely on\n   Linux based systems). This speed boost should also apply to uploading\n   and downloading files.\n-  You can use a **phone number** or an **username** for methods like\n   ``.send_message()``, ``.send_file()``, and all the other quick-access\n   methods provided by the ``TelegramClient``.\n\n.. bug-fixes-5:\n\nBug fixes\n~~~~~~~~~\n\n-  Crashing when migrating to a new layer and receiving old updates\n   should not happen now.\n-  ``InputPeerChannel`` is now casted to ``InputChannel`` automtically\n   too.\n-  ``.get_new_msg_id()`` should now be thread-safe. No promises.\n-  Logging out on macOS caused a crash, which should be gone now.\n-  More checks to ensure that the connection is flagged correctly as\n   either connected or not.\n\n.. note::\n\n   Downloading files from CDN's will **not work** yet (something new\n   that comes with layer 70).\n\n--------------\n\nThat's it, any new idea or suggestion about how to make the project even\nmore friendly is highly appreciated.\n\n.. note::\n\n    Did you know that you can pretty print any result Telegram returns\n    (called ``TLObject``\\ 's) by using their ``.stringify()`` function?\n    Great for debugging!\n\nget_input_* now works with vectors (v0.11.5)\n=============================================\n\n*Published at 2017/07/11*\n\nQuick fix-up of a bug which hadn't been encountered until now. Auto-cast\nby using ``get_input_*`` now works.\n\nget_input_* everywhere (v0.11.4)\n=================================\n\n*Published at 2017/07/10*\n\nFor some reason, Telegram doesn't have enough with the\n`InputPeer <https://tl.telethon.dev/types/input_peer.html>`__.\nThere also exist\n`InputChannel <https://tl.telethon.dev/types/input_channel.html>`__\nand\n`InputUser <https://tl.telethon.dev/types/input_user.html>`__!\nYou don't have to worry about those anymore, it's handled internally\nnow.\n\nBesides this, every Telegram object now features a new default\n``.__str__`` look, and also a `.stringify()\nmethod <https://github.com/LonamiWebs/Telethon/commit/8fd0d7eadd944ff42e18aaf06228adc7aba794b5>`__\nto pretty format them, if you ever need to inspect them.\n\nThe library now uses `the DEBUG\nlevel <https://github.com/LonamiWebs/Telethon/commit/1f7ac7118750ed84e2165dce9c6aca2e6ea0c6a4>`__\neverywhere, so no more warnings or information messages if you had\nlogging enabled.\n\nThe ``no_webpage`` parameter from ``.send_message`` `has been\nrenamed <https://github.com/LonamiWebs/Telethon/commit/0119a006585acd1a1a9a8901a21bb2f193142cfe>`__\nto ``link_preview`` for clarity, so now it does the opposite (but has a\nclearer intention).\n\nQuick .send_message() fix (v0.11.3)\n===================================\n\n*Published at 2017/07/05*\n\nA very quick follow-up release to fix a tiny bug with\n``.send_message()``, no new features.\n\nCallable TelegramClient (v0.11.2)\n=================================\n\n*Published at 2017/07/04*\n\n+-----------------------+\n| Scheme layer used: 68 |\n+-----------------------+\n\nThere is a new preferred way to **invoke requests**, which you're\nencouraged to use:\n\n.. code:: python\n\n    # New!\n    result = client(SomeRequest())\n\n    # Old.\n    result = client.invoke(SomeRequest())\n\nExisting code will continue working, since the old ``.invoke()`` has not\nbeen deprecated.\n\nWhen you ``.create_new_connection()``, it will also handle\n``FileMigrateError``\\ 's for you, so you don't need to worry about those\nanymore.\n\n.. bugs-fixed-3:\n\nBugs fixes\n~~~~~~~~~~\n\n-  Fixed some errors when installing Telethon via ``pip`` (for those\n   using either source distributions or a Python version ≤ 3.5).\n-  ``ConnectionResetError`` didn't flag sockets as closed, but now it\n   does.\n\nOn a more technical side, ``msg_id``\\ 's are now more accurate.\n\nImprovements to the updates (v0.11.1)\n=====================================\n\n*Published at 2017/06/24*\n\nReceiving new updates shouldn't miss any anymore, also, periodic pings\nare back again so it should work on the long run.\n\nOn a different order of things, ``.connect()`` also features a timeout.\nNotice that the ``timeout=`` is **not** passed as a **parameter**\nanymore, and is instead specified when creating the ``TelegramClient``.\n\nBug fixes\n~~~~~~~~~\n\n- Fixed some name class when a request had a ``.msg_id`` parameter.\n- The correct amount of random bytes is now used in DH request\n- Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions.\n- Avoid connecting if already connected.\n\nSupport for parallel connections (v0.11)\n========================================\n\n*Published at 2017/06/16*\n\n*This update brings a lot of changes, so it would be nice if you could*\n**read the whole change log**!\n\nBreaking changes\n~~~~~~~~~~~~~~~~\n\n-  Every Telegram error has now its **own class**, so it's easier to\n   fine-tune your ``except``\\ 's.\n-  Markdown parsing is **not part** of Telethon itself anymore, although\n   there are plans to support it again through a some external module.\n-  The ``.list_sessions()`` has been moved to the ``Session`` class\n   instead.\n-  The ``InteractiveTelegramClient`` is **not** shipped with ``pip``\n   anymore.\n\nAdditions\n~~~~~~~~~\n\n-  A new, more **lightweight class** has been added. The\n   ``TelegramBareClient`` is now the base of the normal\n   ``TelegramClient``, and has the most basic features.\n-  New method to ``.create_new_connection()``, which can be ran **in\n   parallel** with the original connection. This will return the\n   previously mentioned ``TelegramBareClient`` already connected.\n-  Any file object can now be used to download a file (for instance, a\n   ``BytesIO()`` instead a file name).\n-  Vales like ``random_id`` are now **automatically inferred**, so you\n   can save yourself from the hassle of writing\n   ``generate_random_long()`` everywhere. Same applies to\n   ``.get_input_peer()``, unless you really need the extra performance\n   provided by skipping one ``if`` if called manually.\n-  Every type now features a new ``.to_dict()`` method.\n\n.. bug-fixes-6:\n\nBug fixes\n~~~~~~~~~\n\n-  Received errors are acknowledged to the server, so they don't happen\n   over and over.\n-  Downloading media on different data centers is now up to **x2\n   faster**, since there used to be an ``InvalidDCError`` for each file\n   part tried to be downloaded.\n-  Lost messages are now properly skipped.\n-  New way to handle the **result of requests**. The old ``ValueError``\n   \"*The previously sent request must be resent. However, no request was\n   previously sent (possibly called from a different thread).*\" *should*\n   not happen anymore.\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n-  Some fixes to the ``JsonSession``.\n-  Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while\n   ``.reconnect()`` was being called on the ``UpdatesThread``.\n-  Some improvements on the ``TcpClient``, such as not switching between\n   blocking and non-blocking sockets.\n-  The code now uses ASCII characters only.\n-  Some enhancements to ``.find_user_or_chat()`` and\n   ``.get_input_peer()``.\n\nJSON session file (v0.10.1)\n===========================\n\n*Published at 2017/06/07*\n\nThis version is primarily for people to **migrate** their ``.session``\nfiles, which are *pickled*, to the new *JSON* format. Although slightly\nslower, and a bit more vulnerable since it's plain text, it's a lot more\nresistant to upgrades.\n\n.. warning::\n\n    You **must** upgrade to this version before any higher one if you've\n    used Telethon ≤ v0.10. If you happen to upgrade to an higher version,\n    that's okay, but you will have to manually delete the ``*.session`` file,\n    and logout from that session from an official client.\n\nAdditions\n~~~~~~~~~\n\n- New ``.get_me()`` function to get the **current** user.\n- ``.is_user_authorized()`` is now more reliable.\n- New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy``\n  on the online documentation.\n- **More error codes** added to the ``errors`` file.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Everything on the documentation is now, theoretically, **sorted\n  alphabetically**.\n- No second thread is spawned unless one or more update handlers are added.\n\nFull support for different DCs and ++stable (v0.10)\n===================================================\n\n*Published at 2017/06/03*\n\nWorking with **different data centers** finally *works*! On a different\norder of things, **reconnection** is now performed automatically every\ntime Telegram decides to kick us off their servers, so now Telethon can\nreally run **forever and ever**! In theory.\n\nEnhancements\n~~~~~~~~~~~~\n\n-  **Documentation** improvements, such as showing the return type.\n-  The ``msg_id too low/high`` error should happen **less often**, if\n   any.\n-  Sleeping on the main thread is **not done anymore**. You will have to\n   ``except FloodWaitError``\\ 's.\n-  You can now specify your *own application version*, device model,\n   system version and language code.\n-  Code is now more *pythonic* (such as making some members private),\n   and other internal improvements (which affect the **updates\n   thread**), such as using ``logger`` instead a bare ``print()`` too.\n\nThis brings Telethon a whole step closer to ``v1.0``, though more things\nshould preferably be changed.\n\nStability improvements (v0.9.1)\n===============================\n\n*Published at 2017/05/23*\n\nTelethon used to crash a lot when logging in for the very first time.\nThe reason for this was that the reconnection (or dead connections) were\nnot handled properly. Now they are, so you should be able to login\ndirectly, without needing to delete the ``*.session`` file anymore.\nNotice that downloading from a different DC is still a WIP.\n\nEnhancements\n~~~~~~~~~~~~\n\n- Updates thread is only started after a successful login.\n- Files meant to be ran by the user now use **shebangs** and\n  proper permissions.\n- In-code documentation now shows the returning type.\n- **Relative import** is now used everywhere, so you can rename\n  ``telethon`` to anything else.\n- **Dead connections** are now **detected** instead entering an infinite loop.\n- **Sockets** can now be **closed** (and re-opened) properly.\n- Telegram decided to update the layer 66 without increasing the number.\n  This has been fixed and now we're up-to-date again.\n\nGeneral improvements (v0.9)\n===========================\n\n*Published at 2017/05/19*\n\n+-----------------------+\n| Scheme layer used: 66 |\n+-----------------------+\n\nAdditions\n~~~~~~~~~\n\n- The **documentation**, available online\n  `here <https://tl.telethon.dev/>`__, has a new search bar.\n- Better **cross-thread safety** by using ``threading.Event``.\n- More improvements for running Telethon during a **long period of time**.\n\nBug fixes\n~~~~~~~~~\n\n- **Avoid a certain crash on login** (occurred if an unexpected object\n  ID was received).\n- Avoid crashing with certain invalid UTF-8 strings.\n- Avoid crashing on certain terminals by using known ASCII characters\n  where possible.\n- The ``UpdatesThread`` is now a daemon, and should cause less issues.\n- Temporary sessions didn't actually work (with ``session=None``).\n\nInternal changes\n~~~~~~~~~~~~~~~~\n\n- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``.\n\nBot login and proxy support (v0.8)\n==================================\n\n*Published at 2017/04/14*\n\nAdditions\n~~~~~~~~~\n\n-  **Bot login**, thanks to @JuanPotato for hinting me about how to do\n   it.\n-  **Proxy support**, thanks to @exzhawk for implementing it.\n-  **Logging support**, used by passing ``--telethon-log=DEBUG`` (or\n   ``INFO``) as a command line argument.\n\nBug fixes\n~~~~~~~~~\n\n- Connection fixes, such as avoiding connection until ``.connect()`` is\n  explicitly invoked.\n- Uploading big files now works correctly.\n- Fix uploading big files.\n- Some fixes on the updates thread, such as correctly sleeping when required.\n\nLong-run bug fix (v0.7.1)\n=========================\n\n*Published at 2017/02/19*\n\nIf you're one of those who runs Telethon for a long time (more than 30\nminutes), this update by @strayge will be great for you. It sends\nperiodic pings to the Telegram servers so you don't get disconnected and\nyou can still send and receive updates!\n\nTwo factor authentication (v0.7)\n================================\n\n*Published at 2017/01/31*\n\n+-----------------------+\n| Scheme layer used: 62 |\n+-----------------------+\n\nIf you're one of those who love security the most, these are good news.\nYou can now use two factor authentication with Telethon too! As internal\nchanges, the coding style has been improved, and you can easily use\ncustom session objects, and various little bugs have been fixed.\n\nUpdated pip version (v0.6)\n==========================\n\n*Published at 2016/11/13*\n\n+-----------------------+\n| Scheme layer used: 57 |\n+-----------------------+\n\nThis release has no new major features. However, it contains some small\nchanges that make using Telethon a little bit easier. Now those who have\ninstalled Telethon via ``pip`` can also take advantage of changes, such\nas less bugs, creating empty instances of ``TLObjects``, specifying a\ntimeout and more!\n\nReady, pip, go! (v0.5)\n======================\n\n*Published at 2016/09/18*\n\nTelethon is now available as a **`Python\npackage <https://pypi.python.org/pypi?name=Telethon>`__**! Those are\nreally exciting news (except, sadly, the project structure had to change\n*a lot* to be able to do that; but hopefully it won't need to change\nmuch more, any more!)\n\nNot only that, but more improvements have also been made: you're now\nable to both **sign up** and **logout**, watch a pretty\n\"Uploading/Downloading… x%\" progress, and other minor changes which make\nusing Telethon **easier**.\n\nMade InteractiveTelegramClient cool (v0.4)\n==========================================\n\n*Published at 2016/09/12*\n\nYes, really cool! I promise. Even though this is meant to be a\n*library*, that doesn't mean it can't have a good *interactive client*\nfor you to try the library out. This is why now you can do many, many\nthings with the ``InteractiveTelegramClient``:\n\n- **List dialogs** (chats) and pick any you wish.\n- **Send any message** you like, text, photos or even documents.\n- **List** the **latest messages** in the chat.\n- **Download** any message's media (photos, documents or even contacts!).\n- **Receive message updates** as you talk (i.e., someone sent you a message).\n\nIt actually is a usable-enough client for your day by day. You could\neven add ``libnotify`` and pop, you're done! A great cli-client with\ndesktop notifications.\n\nAlso, being able to download and upload media implies that you can do\nthe same with the library itself. Did I need to mention that? Oh, and\nnow, with even less bugs! I hope.\n\nMedia revolution and improvements to update handling! (v0.3)\n============================================================\n\n*Published at 2016/09/11*\n\nTelegram is more than an application to send and receive messages. You\ncan also **send and receive media**. Now, this implementation also gives\nyou the power to upload and download media from any message that\ncontains it! Nothing can now stop you from filling up all your disk\nspace with all the photos! If you want to, of course.\n\nHandle updates in their own thread! (v0.2)\n==========================================\n\n*Published at 2016/09/10*\n\nThis version handles **updates in a different thread** (if you wish to\ndo so). This means that both the low level ``TcpClient`` and the\nnot-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can\nuse them with more than a single thread without worrying!\n\nThis also implies that you won't need to send a request to **receive an\nupdate** (is someone typing? did they send me a message? has someone\ngone offline?). They will all be received **instantly**.\n\nSome other cool examples of things that you can do: when someone tells\nyou \"*Hello*\", you can automatically reply with another \"*Hello*\"\nwithout even needing to type it by yourself :)\n\nHowever, be careful with spamming!! Do **not** use the program for that!\n\nFirst working alpha version! (v0.1)\n===================================\n\n*Published at 2016/09/06*\n\n+-----------------------+\n| Scheme layer used: 55 |\n+-----------------------+\n\nThere probably are some bugs left, which haven't yet been found.\nHowever, the majority of code works and the application is already\nusable! Not only that, but also uses the latest scheme as of now *and*\nhandles way better the errors. This tag is being used to mark this\nrelease as stable enough.\n"
  },
  {
    "path": "readthedocs/misc/compatibility-and-convenience.rst",
    "content": ".. _compatibility-and-convenience:\n\n=============================\nCompatibility and Convenience\n=============================\n\nTelethon is an `asyncio` library. Compatibility is an important concern,\nand while it can't always be kept and mistakes happens, the :ref:`changelog`\nis there to tell you when these important changes happen.\n\n.. contents::\n\n\nCompatibility\n=============\n\nSome decisions when developing will inevitable be proven wrong in the future.\nOne of these decisions was using threads. Now that Python 3.4 is reaching EOL\nand using `asyncio` is usable as of Python 3.5 it makes sense for a library\nlike Telethon to make a good use of it.\n\nIf you have old code, **just use old versions** of the library! There is\nnothing wrong with that other than not getting new updates or fixes, but\nusing a fixed version with ``pip install telethon==0.19.1.6`` is easy\nenough to do.\n\nYou might want to consider using `Virtual Environments\n<https://docs.python.org/3/tutorial/venv.html>`_ in your projects.\n\nThere's no point in maintaining a synchronous version because the whole point\nis that people don't have time to upgrade, and there has been several changes\nand clean-ups. Using an older version is the right way to go.\n\nSometimes, other small decisions are made. These all will be reflected in the\n:ref:`changelog` which you should read when upgrading.\n\nIf you want to jump the `asyncio` boat, here are some of the things you will\nneed to start migrating really old code:\n\n.. code-block:: python\n\n    # 1. Import the client from telethon.sync\n    from telethon.sync import TelegramClient\n\n    # 2. Change this monster...\n    try:\n        assert client.connect()\n        if not client.is_user_authorized():\n            client.send_code_request(phone_number)\n            me = client.sign_in(phone_number, input('Enter code: '))\n\n        ...  # REST OF YOUR CODE\n    finally:\n        client.disconnect()\n\n    # ...for this:\n    with client:\n        ...  # REST OF YOUR CODE\n\n    # 3. client.idle() no longer exists.\n    # Change this...\n    client.idle()\n    # ...to this:\n    client.run_until_disconnected()\n\n    # 4. client.add_update_handler no longer exists.\n    # Change this...\n    client.add_update_handler(handler)\n    # ...to this:\n    client.add_event_handler(handler)\n\n\nIn addition, all the update handlers must be ``async def``, and you need\nto ``await`` method calls that rely on network requests, such as getting\nthe chat or sender. If you don't use updates, you're done!\n\n\nConvenience\n===========\n\n.. note::\n\n    The entire documentation assumes you have done one of the following:\n\n    .. code-block:: python\n\n        from telethon import TelegramClient, sync\n        # or\n        from telethon.sync import TelegramClient\n\n    This makes the examples shorter and easier to think about.\n\nFor quick scripts that don't need updates, it's a lot more convenient to\nforget about `asyncio` and just work with sequential code. This can prove\nto be a powerful hybrid for running under the Python REPL too.\n\n.. code-block:: python\n\n    from telethon.sync import TelegramClient\n    #            ^~~~~ note this part; it will manage the asyncio loop for you\n\n    with TelegramClient(...) as client:\n        print(client.get_me().username)\n        #     ^ notice the lack of await, or loop.run_until_complete().\n        #       Since there is no loop running, this is done behind the scenes.\n        #\n        message = client.send_message('me', 'Hi!')\n        import time\n        time.sleep(5)\n        message.delete()\n\n        # You can also have an hybrid between a synchronous\n        # part and asynchronous event handlers.\n        #\n        from telethon import events\n        @client.on(events.NewMessage(pattern='(?i)hi|hello'))\n        async def handler(event):\n            await event.reply('hey')\n\n        client.run_until_disconnected()\n\n\nSome methods, such as ``with``, ``start``, ``disconnect`` and\n``run_until_disconnected`` work both in synchronous and asynchronous\ncontexts by default for convenience, and to avoid the little overhead\nit has when using methods like sending a message, getting messages, etc.\nThis keeps the best of both worlds as a sane default.\n\n.. note::\n\n    As a rule of thumb, if you're inside an ``async def`` and you need\n    the client, you need to ``await`` calls to the API. If you call other\n    functions that also need API calls, make them ``async def`` and ``await``\n    them too. Otherwise, there is no need to do so with this mode.\n\nSpeed\n=====\n\nWhen you're ready to micro-optimize your application, or if you simply\ndon't need to call any non-basic methods from a synchronous context,\njust get rid of ``telethon.sync`` and work inside an ``async def``:\n\n.. code-block:: python\n\n    import asyncio\n    from telethon import TelegramClient, events\n\n    async def main():\n        async with TelegramClient(...) as client:\n            print((await client.get_me()).username)\n            #     ^_____________________^ notice these parenthesis\n            #     You want to ``await`` the call, not the username.\n            #\n            message = await client.send_message('me', 'Hi!')\n            await asyncio.sleep(5)\n            await message.delete()\n\n            @client.on(events.NewMessage(pattern='(?i)hi|hello'))\n            async def handler(event):\n                await event.reply('hey')\n\n            await client.run_until_disconnected()\n\n    asyncio.run(main())\n\n\nThe ``telethon.sync`` magic module essentially wraps every method behind:\n\n.. code-block:: python\n\n    asyncio.run(main())\n\nWith some other tricks, so that you don't have to write it yourself every time.\nThat's the overhead you pay if you import it, and what you save if you don't.\n\nLearning\n========\n\nYou know the library uses `asyncio` everywhere, and you want to learn\nhow to do things right. Even though `asyncio` is its own topic, the\ndocumentation wants you to learn how to use Telethon correctly, and for\nthat, you need to use `asyncio` correctly too. For this reason, there\nis a section called :ref:`mastering-asyncio` that will introduce you to\nthe `asyncio` world, with links to more resources for learning how to\nuse it. Feel free to check that section out once you have read the rest.\n"
  },
  {
    "path": "readthedocs/modules/client.rst",
    "content": ".. _telethon-client:\n\n==============\nTelegramClient\n==============\n\n.. currentmodule:: telethon.client\n\nThe `TelegramClient <telegramclient.TelegramClient>` aggregates several mixin\nclasses to provide all the common functionality in a nice, Pythonic interface.\nEach mixin has its own methods, which you all can use.\n\n**In short, to create a client you must run:**\n\n.. code-block:: python\n\n    from telethon import TelegramClient\n\n    client = TelegramClient(name, api_id, api_hash)\n\n    async def main():\n        # Now you can use all client methods listed below, like for example...\n        await client.send_message('me', 'Hello to myself!')\n\n    with client:\n        client.loop.run_until_complete(main())\n\n\nYou **don't** need to import these `AuthMethods`, `MessageMethods`, etc.\nTogether they are the `TelegramClient <telegramclient.TelegramClient>` and\nyou can access all of their methods.\n\nSee :ref:`client-ref` for a short summary.\n\n.. automodule:: telethon.client.telegramclient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.telegrambaseclient\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.account\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.auth\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.bots\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.buttons\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.chats\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.dialogs\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.downloads\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.messageparse\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.messages\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.updates\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.uploads\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.client.users\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/custom.rst",
    "content": "==============\nCustom package\n==============\n\nThe `telethon.tl.custom` package contains custom classes that the library\nuses in order to make working with Telegram easier. Only those that you\nare supposed to use will be documented here. You can use undocumented ones\nat your own risk.\n\nMore often than not, you don't need to import these (unless you want\ntype hinting), nor do you need to manually create instances of these\nclasses. They are returned by client methods.\n\n.. contents::\n\n.. automodule:: telethon.tl.custom\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nAdminLogEvent\n=============\n\n.. automodule:: telethon.tl.custom.adminlogevent\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nButton\n======\n\n.. automodule:: telethon.tl.custom.button\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nChatGetter\n==========\n\n.. automodule:: telethon.tl.custom.chatgetter\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nConversation\n============\n\n.. automodule:: telethon.tl.custom.conversation\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nDialog\n======\n\n.. automodule:: telethon.tl.custom.dialog\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nDraft\n=====\n\n.. automodule:: telethon.tl.custom.draft\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nFile\n====\n\n.. automodule:: telethon.tl.custom.file\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nForward\n=======\n\n.. automodule:: telethon.tl.custom.forward\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nInlineBuilder\n=============\n\n.. automodule:: telethon.tl.custom.inlinebuilder\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nInlineResult\n============\n\n.. automodule:: telethon.tl.custom.inlineresult\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nInlineResults\n=============\n\n.. automodule:: telethon.tl.custom.inlineresults\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nMessage\n=======\n\n.. automodule:: telethon.tl.custom.message\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nMessageButton\n=============\n\n.. automodule:: telethon.tl.custom.messagebutton\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nParticipantPermissions\n======================\n\n.. automodule:: telethon.tl.custom.participantpermissions\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nQRLogin\n=======\n\n.. automodule:: telethon.tl.custom.qrlogin\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\nSenderGetter\n============\n\n.. automodule:: telethon.tl.custom.sendergetter\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/errors.rst",
    "content": ".. _telethon-errors:\n\n==========\nAPI Errors\n==========\n\nThese are the base errors that Telegram's API may raise.\n\nSee :ref:`rpc-errors` for a more in-depth explanation on how to handle all\nknown possible errors and learning to determine what a method may raise.\n\n.. automodule:: telethon.errors.common\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.errors.rpcbaseerrors\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/events.rst",
    "content": ".. _telethon-events:\n\n=============\nUpdate Events\n=============\n\n.. currentmodule:: telethon.events\n\nEvery event (builder) subclasses `common.EventBuilder`,\nso all the methods in it can be used from any event builder/event instance.\n\n.. automodule:: telethon.events.common\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.newmessage\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.chataction\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.userupdate\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.messageedited\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.messagedeleted\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.messageread\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.callbackquery\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.inlinequery\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.album\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events.raw\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.events\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/helpers.rst",
    "content": "=======\nHelpers\n=======\n\n.. automodule:: telethon.helpers\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/network.rst",
    "content": ".. _telethon-network:\n\n================\nConnection Modes\n================\n\nThe only part about network that you should worry about are\nthe different connection modes, which are the following:\n\n.. automodule:: telethon.network.connection.tcpfull\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.network.connection.tcpabridged\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.network.connection.tcpintermediate\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.network.connection.tcpobfuscated\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.network.connection.http\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/sessions.rst",
    "content": ".. _telethon-sessions:\n\n========\nSessions\n========\n\nThese are the different built-in session storage that you may subclass.\n\n.. automodule:: telethon.sessions.abstract\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.sessions.memory\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.sessions.sqlite\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. automodule:: telethon.sessions.string\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/modules/utils.rst",
    "content": ".. _telethon-utils:\n\n=========\nUtilities\n=========\n\nThese are the utilities that the library has to offer.\n\n.. automodule:: telethon.utils\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "readthedocs/quick-references/client-reference.rst",
    "content": ".. _client-ref:\n\n================\nClient Reference\n================\n\nThis page contains a summary of all the important methods and properties that\nyou may need when using Telethon. They are sorted by relevance and are not in\nalphabetical order.\n\nYou should use this page to learn about which methods are available, and\nif you need a usage example or further description of the arguments, be\nsure to follow the links.\n\n.. contents::\n\nTelegramClient\n==============\n\nThis is a summary of the methods and\nproperties you will find at :ref:`telethon-client`.\n\nAuth\n----\n\n.. currentmodule:: telethon.client.auth.AuthMethods\n\n.. autosummary::\n    :nosignatures:\n\n    start\n    send_code_request\n    sign_in\n    qr_login\n    log_out\n    edit_2fa\n\nBase\n----\n\n.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient\n\n.. autosummary::\n    :nosignatures:\n\n    connect\n    disconnect\n    is_connected\n    disconnected\n    loop\n    set_proxy\n\nMessages\n--------\n\n.. py:currentmodule:: telethon.client.messages.MessageMethods\n\n.. autosummary::\n    :nosignatures:\n\n    send_message\n    edit_message\n    delete_messages\n    forward_messages\n    iter_messages\n    get_messages\n    pin_message\n    unpin_message\n    send_read_acknowledge\n\nUploads\n-------\n\n.. py:currentmodule:: telethon.client.uploads.UploadMethods\n\n.. autosummary::\n    :nosignatures:\n\n    send_file\n    upload_file\n\nDownloads\n---------\n\n.. currentmodule:: telethon.client.downloads.DownloadMethods\n\n.. autosummary::\n    :nosignatures:\n\n    download_media\n    download_profile_photo\n    download_file\n    iter_download\n\nDialogs\n-------\n\n.. py:currentmodule:: telethon.client.dialogs.DialogMethods\n\n.. autosummary::\n    :nosignatures:\n\n    iter_dialogs\n    get_dialogs\n    edit_folder\n    iter_drafts\n    get_drafts\n    delete_dialog\n    conversation\n\nUsers\n-----\n\n.. py:currentmodule:: telethon.client.users.UserMethods\n\n.. autosummary::\n    :nosignatures:\n\n    get_me\n    is_bot\n    is_user_authorized\n    get_entity\n    get_input_entity\n    get_peer_id\n\nChats\n-----\n\n.. currentmodule:: telethon.client.chats.ChatMethods\n\n.. autosummary::\n    :nosignatures:\n\n    iter_participants\n    get_participants\n    kick_participant\n    iter_admin_log\n    get_admin_log\n    iter_profile_photos\n    get_profile_photos\n    edit_admin\n    edit_permissions\n    get_permissions\n    get_stats\n    action\n\nParse Mode\n----------\n\n.. py:currentmodule:: telethon.client.messageparse.MessageParseMethods\n\n.. autosummary::\n    :nosignatures:\n\n    parse_mode\n\nUpdates\n-------\n\n.. py:currentmodule:: telethon.client.updates.UpdateMethods\n\n.. autosummary::\n    :nosignatures:\n\n    on\n    run_until_disconnected\n    add_event_handler\n    remove_event_handler\n    list_event_handlers\n    catch_up\n    set_receive_updates\n\nBots\n----\n\n.. currentmodule:: telethon.client.bots.BotMethods\n\n.. autosummary::\n    :nosignatures:\n\n    inline_query\n\nButtons\n-------\n\n.. currentmodule:: telethon.client.buttons.ButtonMethods\n\n.. autosummary::\n    :nosignatures:\n\n    build_reply_markup\n\nAccount\n-------\n\n.. currentmodule:: telethon.client.account.AccountMethods\n\n.. autosummary::\n    :nosignatures:\n\n    takeout\n    end_takeout\n"
  },
  {
    "path": "readthedocs/quick-references/events-reference.rst",
    "content": "================\nEvents Reference\n================\n\nHere you will find a quick summary of all the methods\nand properties that you can access when working with events.\n\nYou can access the client that creates this event by doing\n``event.client``, and you should view the description of the\nevents to find out what arguments it allows on creation and\nits **attributes** (the properties will be shown here).\n\n.. important::\n\n    Remember that **all events base** `ChatGetter\n    <telethon.tl.custom.chatgetter.ChatGetter>`! Please see :ref:`faq`\n    if you don't know what this means or the implications of it.\n\n.. contents::\n\n\nNewMessage\n==========\n\nOccurs whenever a new text message or a message with media arrives.\n\n.. note::\n\n    The new message event **should be treated as** a\n    normal `Message <telethon.tl.custom.message.Message>`, with\n    the following exceptions:\n\n    * ``pattern_match`` is the match object returned by ``pattern=``.\n    * ``message`` is **not** the message string. It's the `Message\n      <telethon.tl.custom.message.Message>` object.\n\n    Remember, this event is just a proxy over the message, so while\n    you won't see its attributes and properties, you can still access\n    them. Please see the full documentation for examples.\n\nFull documentation for the `NewMessage\n<telethon.events.newmessage.NewMessage>`.\n\n\nMessageEdited\n=============\n\nOccurs whenever a message is edited. Just like `NewMessage\n<telethon.events.newmessage.NewMessage>`, you should treat\nthis event as a `Message <telethon.tl.custom.message.Message>`.\n\nFull documentation for the `MessageEdited\n<telethon.events.messageedited.MessageEdited>`.\n\n\nMessageDeleted\n==============\n\nOccurs whenever a message is deleted. Note that this event isn't 100%\nreliable, since Telegram doesn't always notify the clients that a message\nwas deleted.\n\nIt only has the ``deleted_id`` and ``deleted_ids`` attributes\n(in addition to the chat if the deletion happened in a channel).\n\nFull documentation for the `MessageDeleted\n<telethon.events.messagedeleted.MessageDeleted>`.\n\n\nMessageRead\n===========\n\nOccurs whenever one or more messages are read in a chat.\n\nFull documentation for the `MessageRead\n<telethon.events.messageread.MessageRead>`.\n\n.. currentmodule:: telethon.events.messageread.MessageRead.Event\n\n.. autosummary::\n    :nosignatures:\n\n        inbox\n        message_ids\n\n        get_messages\n        is_read\n\n\nChatAction\n==========\n\nOccurs on certain chat actions, such as chat title changes,\nuser join or leaves, pinned messages, photo changes, etc.\n\nFull documentation for the `ChatAction\n<telethon.events.chataction.ChatAction>`.\n\n.. currentmodule:: telethon.events.chataction.ChatAction.Event\n\n.. autosummary::\n    :nosignatures:\n\n        added_by\n        kicked_by\n        user\n        input_user\n        user_id\n        users\n        input_users\n        user_ids\n\n        respond\n        reply\n        delete\n        get_pinned_message\n        get_added_by\n        get_kicked_by\n        get_user\n        get_input_user\n        get_users\n        get_input_users\n\n\nUserUpdate\n==========\n\nOccurs whenever a user goes online, starts typing, etc.\n\nFull documentation for the `UserUpdate\n<telethon.events.userupdate.UserUpdate>`.\n\n.. currentmodule:: telethon.events.userupdate.UserUpdate.Event\n\n.. autosummary::\n    :nosignatures:\n\n        user\n        input_user\n        user_id\n\n        get_user\n        get_input_user\n\n        typing\n        uploading\n        recording\n        playing\n        cancel\n        geo\n        audio\n        round\n        video\n        contact\n        document\n        photo\n        last_seen\n        until\n        online\n        recently\n        within_weeks\n        within_months\n\n\nCallbackQuery\n=============\n\nOccurs whenever you sign in as a bot and a user\nclicks one of the inline buttons on your messages.\n\nFull documentation for the `CallbackQuery\n<telethon.events.callbackquery.CallbackQuery>`.\n\n.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event\n\n.. autosummary::\n    :nosignatures:\n\n        id\n        message_id\n        data\n        chat_instance\n        via_inline\n\n        respond\n        reply\n        edit\n        delete\n        answer\n        get_message\n\nInlineQuery\n===========\n\nOccurs whenever you sign in as a bot and a user\nsends an inline query such as ``@bot query``.\n\nFull documentation for the `InlineQuery\n<telethon.events.inlinequery.InlineQuery>`.\n\n.. currentmodule:: telethon.events.inlinequery.InlineQuery.Event\n\n.. autosummary::\n    :nosignatures:\n\n        id\n        text\n        offset\n        geo\n        builder\n\n        answer\n\nAlbum\n=====\n\nOccurs whenever you receive an entire album.\n\nFull documentation for the `Album\n<telethon.events.album.Album>`.\n\n.. currentmodule:: telethon.events.album.Album.Event\n\n.. autosummary::\n    :nosignatures:\n\n        grouped_id\n        text\n        raw_text\n        is_reply\n        forward\n\n        get_reply_message\n        respond\n        reply\n        forward_to\n        edit\n        delete\n        mark_read\n        pin\n\nRaw\n===\n\nRaw events are not actual events. Instead, they are the raw\n:tl:`Update` object that Telegram sends. You normally shouldn't\nneed these.\n"
  },
  {
    "path": "readthedocs/quick-references/faq.rst",
    "content": ".. _faq:\n\n===\nFAQ\n===\n\nLet's start the quick references section with some useful tips to keep in\nmind, with the hope that you will understand why certain things work the\nway that they do.\n\n.. contents::\n\n\nCode without errors doesn't work\n================================\n\nThen it probably has errors, but you haven't enabled logging yet.\nTo enable logging, at the following code to the top of your main file:\n\n.. code-block:: python\n\n    import logging\n    logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',\n                        level=logging.WARNING)\n\nYou can change the logging level to be something different, from less to more information:\n\n.. code-block:: python\n\n    level=logging.CRITICAL  # won't show errors (same as disabled)\n    level=logging.ERROR     # will only show errors that you didn't handle\n    level=logging.WARNING   # will also show messages with medium severity, such as internal Telegram issues\n    level=logging.INFO      # will also show informational messages, such as connection or disconnections\n    level=logging.DEBUG     # will show a lot of output to help debugging issues in the library\n\nSee the official Python documentation for more information on logging_.\n\n\nHow can I except FloodWaitError?\n================================\n\nYou can use all errors from the API by importing:\n\n.. code-block:: python\n\n    from telethon import errors\n\nAnd except them as such:\n\n.. code-block:: python\n\n    try:\n        await client.send_message(chat, 'Hi')\n    except errors.FloodWaitError as e:\n        # e.seconds is how many seconds you have\n        # to wait before making the request again.\n        print('Flood for', e.seconds)\n\n\nMy account was deleted/limited when using the library\n=====================================================\n\nFirst and foremost, **this is not a problem exclusive to Telethon.\nAny third-party library is prone to cause the accounts to appear banned.**\nEven official applications can make Telegram ban an account under certain\ncircumstances. Third-party libraries such as Telethon are a lot easier to\nuse, and as such, they are misused to spam, which causes Telegram to learn\ncertain patterns and ban suspicious activity.\n\nThere is no point in Telethon trying to circumvent this. Even if it succeeded,\nspammers would then abuse the library again, and the cycle would repeat.\n\nThe library will only do things that you tell it to do. If you use\nthe library with bad intentions, Telegram will hopefully ban you.\n\nHowever, you may also be part of a limited country, such as Iran or Russia.\nIn that case, we have bad news for you. Telegram is much more likely to ban\nthese numbers, as they are often used to spam other accounts, likely through\nthe use of libraries like this one. The best advice we can give you is to not\nabuse the API, like calling many requests really quickly.\n\nWe have also had reports from Kazakhstan and China, where connecting\nwould fail. To solve these connection problems, you should use a proxy.\n\nTelegram may also ban virtual (VoIP) phone numbers,\nas again, they're likely to be used for spam.\n\nMore recently (year 2023 onwards), Telegram has started putting a lot more\nmeasures to prevent spam (with even additions such as anonymous participants\nin groups or the inability to fetch group members at all). This means some\nof the anti-spam measures have gotten more aggressive.\n\nThe recommendation has usually been to use the library only on well-established\naccounts (and not an account you just created), and to not perform actions that\ncould be seen as abuse. Telegram decides what those actions are, and they're\nfree to change how they operate at any time.\n\nIf you want to check if your account has been limited,\nsimply send a private message to `@SpamBot`_ through Telegram itself.\nYou should notice this by getting errors like ``PeerFloodError``,\nwhich means you're limited, for instance,\nwhen sending a message to some accounts but not others.\n\nFor more discussion, please see `issue 297`_.\n\n\nHow can I use a proxy?\n======================\n\nThis was one of the first things described in :ref:`signing-in`.\n\n\nHow do I access a field?\n========================\n\nThis is basic Python knowledge. You should use the dot operator:\n\n.. code-block:: python\n\n    me = await client.get_me()\n    print(me.username)\n    #       ^ we used the dot operator to access the username attribute\n\n    result = await client(functions.photos.GetUserPhotosRequest(\n        user_id='me',\n        offset=0,\n        max_id=0,\n        limit=100\n    ))\n\n    # Working with list is also pretty basic\n    print(result.photos[0].sizes[-1].type)\n    #           ^       ^ ^       ^ ^\n    #           |       | |       | \\ type\n    #           |       | |       \\ last size\n    #           |       | \\ list of sizes\n    #  access   |       \\ first photo from the list\n    #  the...   \\ list of photos\n    #\n    # To print all, you could do (or mix-and-match):\n    for photo in result.photos:\n        for size in photo.sizes:\n            print(size.type)\n\n\nAttributeError: 'coroutine' object has no attribute 'id'\n========================================================\n\nYou either forgot to:\n\n.. code-block:: python\n\n    import telethon.sync\n    #              ^^^^^ import sync\n\nOr:\n\n.. code-block:: python\n\n    async def handler(event):\n        me = await client.get_me()\n        #    ^^^^^ note the await\n        print(me.username)\n\n\nsqlite3.OperationalError: database is locked\n============================================\n\nAn older process is still running and is using the same ``'session'`` file.\n\nThis error occurs when **two or more clients use the same session**,\nthat is, when you write the same session name to be used in the client:\n\n* You have an older process using the same session file.\n* You have two different scripts running (interactive sessions count too).\n* You have two clients in the same script running at the same time.\n\nThe solution is, if you need two clients, use two sessions. If the\nproblem persists and you're on Linux, you can use ``fuser my.session``\nto find out the process locking the file. As a last resort, you can\nreboot your system.\n\nIf you really dislike SQLite, use a different session storage. There\nis an entire section covering that at :ref:`sessions`.\n\n\nevent.chat or event.sender is None\n==================================\n\nTelegram doesn't always send this information in order to save bandwidth.\nIf you need the information, you should fetch it yourself, since the library\nwon't do unnecessary work unless you need to:\n\n.. code-block:: python\n\n    async def handler(event):\n        chat = await event.get_chat()\n        sender = await event.get_sender()\n\n\nFile download is slow or sending files takes too long\n=====================================================\n\nThe communication with Telegram is encrypted. Encryption requires a lot of\nmath, and doing it in pure Python is very slow. ``cryptg`` is a library which\ncontainns the encryption functions used by Telethon. If it is installed (via\n``pip install cryptg``), it will automatically be used and should provide\na considerable speed boost. You can know whether it's used by configuring\n``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``.\n\nNote that the library does *not* download or upload files in parallel, which\ncan also help with the speed of downloading or uploading a single file. There\nare snippets online implementing that. The reason why this is not built-in\nis because the limiting factor in the long run are ``FloodWaitError``, and\nusing parallel download or uploads only makes them occur sooner.\n\n\nWhat does \"Server sent a very new message with ID\" mean?\n========================================================\n\nYou may also see this error as \"Server sent a very old message with ID\".\n\nThis is a security feature from Telethon that cannot be disabled and is\nmeant to protect you against replay attacks.\n\nWhen this message is incorrectly reported as a \"bug\",\nthe most common patterns seem to be:\n\n* Your system time is incorrect.\n* The proxy you're using may be interfering somehow.\n* The Telethon session is being used or has been used from somewhere else.\n  Make sure that you created the session from Telethon, and are not using the\n  same session anywhere else. If you need to use the same account from\n  multiple places, login and use a different session for each place you need.\n\n\nWhat does \"Server replied with a wrong session ID\" mean?\n========================================================\n\nThis is a security feature from Telethon that cannot be disabled and is\nmeant to protect you against unwanted session reuse.\n\nWhen this message is reported as a \"bug\", the most common patterns seem to be:\n\n* The proxy you're using may be interfering somehow.\n* The Telethon session is being used or has been used from somewhere else.\n  Make sure that you created the session from Telethon, and are not using the\n  same session anywhere else. If you need to use the same account from\n  multiple places, login and use a different session for each place you need.\n* You may be using multiple connections to the Telegram server, which seems\n  to confuse Telegram.\n\nMost of the time it should be safe to ignore this warning. If the library\nstill doesn't behave correctly, make sure to check if any of the above bullet\npoints applies in your case and try to work around it.\n\nIf the issue persists and there is a way to reliably reproduce this error,\nplease add a comment with any additional details you can provide to\n`issue 3759`_, and perhaps some additional investigation can be done\n(but it's unlikely, as Telegram *is* sending unexpected data).\n\n\nWhat does \"Could not find a matching Constructor ID for the TLObject\" mean?\n===========================================================================\n\nTelegram uses \"layers\", which you can think of as \"versions\" of the API they\noffer. When Telethon reads responses that the Telegram servers send, these\nneed to be deserialized (into what Telethon calls \"TLObjects\").\n\nEvery Telethon version understands a single Telegram layer. When Telethon\nconnects to Telegram, both agree on the layer to use. If the layers don't\nmatch, Telegram may send certain objects which Telethon no longer understands.\n\nWhen this message is reported as a \"bug\", the most common patterns seem to be\nthat the Telethon session is being used or has been used from somewhere else.\nMake sure that you created the session from Telethon, and are not using the\nsame session anywhere else. If you need to use the same account from\nmultiple places, login and use a different session for each place you need.\n\n\nWhat does \"Task was destroyed but it is pending\" mean?\n======================================================\n\nYour script likely finished abruptly, the ``asyncio`` event loop got\ndestroyed, and the library did not get a chance to properly close the\nconnection and close the session.\n\nMake sure you're either using the context manager for the client or always\ncall ``await client.disconnect()`` (by e.g. using a ``try/finally``).\n\n\nWhat does \"The asyncio event loop must not change after connection\" mean?\n=========================================================================\n\nTelethon uses ``asyncio``, and makes use of things like tasks and queues\ninternally to manage the connection to the server and match responses to the\nrequests you make. Most of them are initialized after the client is connected.\n\nFor example, if the library expects a result to a request made in loop A, but\nyou attempt to get that result in loop B, you will very likely find a deadlock.\nTo avoid a deadlock, the library checks to make sure the loop in use is the\nsame as the one used to initialize everything, and if not, it throws an error.\n\nThe most common cause is ``asyncio.run``, since it creates a new event loop.\nIf you ``asyncio.run`` a function to create the client and set it up, and then\nyou ``asyncio.run`` another function to do work, things won't work, so the\nlibrary throws an error early to let you know something is wrong.\n\nInstead, it's often a good idea to have a single ``async def main`` and simply\n``asyncio.run()`` it and do all the work there. From it, you're also able to\ncall other ``async def`` without having to touch ``asyncio.run`` again:\n\n.. code-block:: python\n\n    # It's fine to create the client outside as long as you don't connect\n    client = TelegramClient(...)\n\n    async def main():\n        # Now the client will connect, so the loop must not change from now on.\n        # But as long as you do all the work inside main, including calling\n        # other async functions, things will work.\n        async with client:\n            ....\n\n    if __name__ == '__main__':\n        asyncio.run(main())\n\nBe sure to read the ``asyncio`` documentation if you want a better\nunderstanding of event loop, tasks, and what functions you can use.\n\n\nWhat does \"bases ChatGetter\" mean?\n==================================\n\nIn Python, classes can base others. This is called `inheritance\n<https://ddg.gg/python%20inheritance>`_. What it means is that\n\"if a class bases another, you can use the other's methods too\".\n\nFor example, `Message <telethon.tl.custom.message.Message>` *bases*\n`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`. In turn,\n`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` defines\nthings like `obj.chat_id <telethon.tl.custom.chatgetter.ChatGetter>`.\n\nSo if you have a message, you can access that too:\n\n.. code-block:: python\n\n    # ChatGetter has a chat_id property, and Message bases ChatGetter.\n    # Thus you can use ChatGetter properties and methods from Message\n    print(message.chat_id)\n\n\nTelegram has a lot to offer, and inheritance helps the library reduce\nboilerplate, so it's important to know this concept. For newcomers,\nthis may be a problem, so we explain what it means here in the FAQ.\n\nCan I send files by ID?\n=======================\n\nWhen people talk about IDs, they often refer to one of two things:\nthe integer ID inside media, and a random-looking long string.\n\nYou cannot use the integer ID to send media. Generally speaking, sending media\nrequires a combination of ID, ``access_hash`` and ``file_reference``.\nThe first two are integers, while the last one is a random ``bytes`` sequence.\n\n* The integer ``id`` will always be the same for every account, so every user\n  or bot looking at a particular media file, will see a consistent ID.\n* The ``access_hash`` will always be the same for a given account, but\n  different accounts will each see their own, different ``access_hash``.\n  This makes it impossible to get media object from one account and use it in\n  another. The other account must fetch the media object itself.\n* The ``file_reference`` is random for everyone and will only work for a few\n  hours before it expires. It must be refetched before the media can be used\n  (to either resend the media or download it).\n\nThe second type of \"`file ID <https://core.telegram.org/bots/api#inputfile>`_\"\npeople refer to is a concept from the HTTP Bot API. It's a custom format which\nencodes enough information to use the media.\n\nTelethon provides an old version of these HTTP Bot API-style file IDs via\n``message.file.id``, however, this feature is no longer maintained, so it may\nnot work. It will be removed in future versions. Nonetheless, it is possible\nto find a different Python package (or write your own) to parse these file IDs\nand construct the necessary input file objects to send or download the media.\n\n\nCan I use Flask with the library?\n=================================\n\nYes, if you know what you are doing. However, you will probably have a\nlot of headaches to get threads and asyncio to work together. Instead,\nconsider using `Quart <https://pgjones.gitlab.io/quart/>`_, an asyncio-based\nalternative to `Flask <flask.pocoo.org/>`_.\n\nCheck out `quart_login.py`_ for an example web-application based on Quart.\n\nCan I use Anaconda/Spyder/IPython with the library?\n===================================================\n\nYes, but these interpreters run the asyncio event loop implicitly,\nwhich interferes with the ``telethon.sync`` magic module.\n\nIf you use them, you should **not** import ``sync``:\n\n.. code-block:: python\n\n    # Change any of these...:\n    from telethon import TelegramClient, sync, ...\n    from telethon.sync import TelegramClient, ...\n\n    # ...with this:\n    from telethon import TelegramClient, ...\n\nYou are also more likely to get \"sqlite3.OperationalError: database is locked\"\nwith them. If they cause too much trouble, just write your code in a ``.py``\nfile and run that, or use the normal ``python`` interpreter.\n\n.. _logging: https://docs.python.org/3/library/logging.html\n.. _@SpamBot: https://t.me/SpamBot\n.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297\n.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759\n.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy\n"
  },
  {
    "path": "readthedocs/quick-references/objects-reference.rst",
    "content": "=================\nObjects Reference\n=================\n\nThis is the quick reference for those objects returned by client methods\nor other useful modules that the library has to offer. They are kept in\na separate page to help finding and discovering them.\n\nRemember that this page only shows properties and methods,\n**not attributes**. Make sure to open the full documentation\nto find out about the attributes.\n\n.. contents::\n\n\nChatGetter\n==========\n\nAll events base `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`,\nand some of the objects below do too, so it's important to know its methods.\n\n.. currentmodule:: telethon.tl.custom.chatgetter.ChatGetter\n\n.. autosummary::\n    :nosignatures:\n\n    chat\n    input_chat\n    chat_id\n    is_private\n    is_group\n    is_channel\n\n    get_chat\n    get_input_chat\n\n\nSenderGetter\n============\n\nSimilar to `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`, a\n`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is the same,\nbut it works for senders instead.\n\n.. currentmodule:: telethon.tl.custom.sendergetter.SenderGetter\n\n.. autosummary::\n    :nosignatures:\n\n    sender\n    input_sender\n    sender_id\n\n    get_sender\n    get_input_sender\n\n\nMessage\n=======\n\n.. currentmodule:: telethon.tl.custom.message\n\nThe `Message` type is very important, mostly because we are working\nwith a library for a *messaging* platform, so messages are widely used:\nin events, when fetching history, replies, etc.\n\nIt bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` and\n`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`.\n\nProperties\n----------\n\n.. note::\n\n    We document *custom properties* here, not all the attributes of the\n    `Message` (which is the information Telegram actually returns).\n\n.. currentmodule:: telethon.tl.custom.message.Message\n\n.. autosummary::\n    :nosignatures:\n\n    text\n    raw_text\n    is_reply\n    forward\n    buttons\n    button_count\n    file\n    photo\n    document\n    web_preview\n    audio\n    voice\n    video\n    video_note\n    gif\n    sticker\n    contact\n    game\n    geo\n    invoice\n    poll\n    venue\n    action_entities\n    via_bot\n    via_input_bot\n    client\n\n\nMethods\n-------\n\n.. autosummary::\n    :nosignatures:\n\n    respond\n    reply\n    forward_to\n    edit\n    delete\n    get_reply_message\n    click\n    mark_read\n    pin\n    download_media\n    get_entities_text\n    get_buttons\n\n\nFile\n====\n\nThe `File <telethon.tl.custom.file.File>` type is a wrapper object\nreturned by `Message.file <telethon.tl.custom.message.Message.file>`,\nand you can use it to easily access a document's attributes, such as\nits name, bot-API style file ID, etc.\n\n.. currentmodule:: telethon.tl.custom.file.File\n\n.. autosummary::\n    :nosignatures:\n\n    id\n    name\n    ext\n    mime_type\n    width\n    height\n    size\n    duration\n    title\n    performer\n    emoji\n    sticker_set\n\n\nConversation\n============\n\nThe `Conversation <telethon.tl.custom.conversation.Conversation>` object\nis returned by the `client.conversation()\n<telethon.client.dialogs.DialogMethods.conversation>` method to easily\nsend and receive responses like a normal conversation.\n\nIt bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.\n\n.. currentmodule:: telethon.tl.custom.conversation.Conversation\n\n.. autosummary::\n    :nosignatures:\n\n    send_message\n    send_file\n    mark_read\n    get_response\n    get_reply\n    get_edit\n    wait_read\n    wait_event\n    cancel\n    cancel_all\n\n\nAdminLogEvent\n=============\n\nThe `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>` object\nis returned by the `client.iter_admin_log()\n<telethon.client.chats.ChatMethods.iter_admin_log>` method to easily iterate\nover past \"events\" (deleted messages, edits, title changes, leaving members…)\n\nThese are all the properties you can find in it:\n\n.. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent\n\n.. autosummary::\n    :nosignatures:\n\n    id\n    date\n    user_id\n    action\n    old\n    new\n    changed_about\n    changed_title\n    changed_username\n    changed_photo\n    changed_sticker_set\n    changed_message\n    deleted_message\n    changed_admin\n    changed_restrictions\n    changed_invites\n    joined\n    joined_invite\n    left\n    changed_hide_history\n    changed_signatures\n    changed_pin\n    changed_default_banned_rights\n    stopped_poll\n\n\nButton\n======\n\nThe `Button <telethon.tl.custom.button.Button>` class is used when you login\nas a bot account to send messages with reply markup, such as inline buttons\nor custom keyboards.\n\nThese are the static methods you can use to create instances of the markup:\n\n.. currentmodule:: telethon.tl.custom.button.Button\n\n.. autosummary::\n    :nosignatures:\n\n    inline\n    switch_inline\n    url\n    auth\n    text\n    request_location\n    request_phone\n    request_poll\n    clear\n    force_reply\n\n\nInlineResult\n============\n\nThe `InlineResult <telethon.tl.custom.inlineresult.InlineResult>` object\nis returned inside a list by the `client.inline_query()\n<telethon.client.bots.BotMethods.inline_query>` method to make an inline\nquery to a bot that supports being used in inline mode, such as\n`@like <https://t.me/like>`_.\n\nNote that the list returned is in fact a *subclass* of a list called\n`InlineResults <telethon.tl.custom.inlineresults.InlineResults>`, which,\nin addition of being a list (iterator, indexed access, etc.), has extra\nattributes and methods.\n\nThese are the constants for the types, properties and methods you\ncan find the individual results:\n\n.. currentmodule:: telethon.tl.custom.inlineresult.InlineResult\n\n.. autosummary::\n    :nosignatures:\n\n    ARTICLE\n    PHOTO\n    GIF\n    VIDEO\n    VIDEO_GIF\n    AUDIO\n    DOCUMENT\n    LOCATION\n    VENUE\n    CONTACT\n    GAME\n    type\n    message\n    title\n    description\n    url\n    photo\n    document\n    click\n    download_media\n\n\nDialog\n======\n\nThe `Dialog <telethon.tl.custom.dialog.Dialog>` object is returned when\nyou call `client.iter_dialogs() <telethon.client.dialogs.DialogMethods.iter_dialogs>`.\n\n.. currentmodule:: telethon.tl.custom.dialog.Dialog\n\n.. autosummary::\n    :nosignatures:\n\n    send_message\n    archive\n    delete\n\n\nDraft\n======\n\nThe `Draft <telethon.tl.custom.draft.Draft>` object is returned when\nyou call `client.iter_drafts() <telethon.client.dialogs.DialogMethods.iter_drafts>`.\n\n.. currentmodule:: telethon.tl.custom.draft.Draft\n\n.. autosummary::\n    :nosignatures:\n\n    entity\n    input_entity\n    get_entity\n    get_input_entity\n    text\n    raw_text\n    is_empty\n    set_message\n    send\n    delete\n\n\nUtils\n=====\n\nThe `telethon.utils` module has plenty of methods that make using the\nlibrary a lot easier. Only the interesting ones will be listed here.\n\n.. currentmodule:: telethon.utils\n\n.. autosummary::\n    :nosignatures:\n\n    get_display_name\n    get_extension\n    get_inner_text\n    get_peer_id\n    resolve_id\n    pack_bot_file_id\n    resolve_bot_file_id\n    resolve_invite_link\n"
  },
  {
    "path": "readthedocs/requirements.txt",
    "content": "./\nsphinx-rtd-theme~=1.3.0\n"
  },
  {
    "path": "requirements.txt",
    "content": "pyaes\nrsa\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n\"\"\"A setuptools based setup module.\n\nSee:\nhttps://packaging.python.org/en/latest/distributing.html\nhttps://github.com/pypa/sampleproject\n\nExtra supported commands are:\n* gen, to generate the classes required for Telethon to run or docs\n* pypi, to generate sdist, bdist_wheel, and push to PyPi\n\"\"\"\n\nimport itertools\nimport json\nimport os\nimport re\nimport shutil\nimport sys\nimport urllib.request\nfrom pathlib import Path\nfrom subprocess import run\n\nfrom setuptools import find_packages, setup\n\n# Needed since we're importing local files\nsys.path.insert(0, os.path.dirname(__file__))\n\nclass TempWorkDir:\n    \"\"\"Switches the working directory to be the one on which this file lives,\n       while within the 'with' block.\n    \"\"\"\n    def __init__(self, new=None):\n        self.original = None\n        self.new = new or str(Path(__file__).parent.resolve())\n\n    def __enter__(self):\n        # os.chdir does not work with Path in Python 3.5.x\n        self.original = str(Path('.').resolve())\n        os.makedirs(self.new, exist_ok=True)\n        os.chdir(self.new)\n        return self\n\n    def __exit__(self, *args):\n        os.chdir(self.original)\n\n\nAPI_REF_URL = 'https://tl.telethon.dev/'\n\nGENERATOR_DIR = Path('telethon_generator')\nLIBRARY_DIR = Path('telethon')\n\nERRORS_IN = GENERATOR_DIR / 'data/errors.csv'\nERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'\n\nMETHODS_IN = GENERATOR_DIR / 'data/methods.csv'\n\n# Which raw API methods are covered by *friendly* methods in the client?\nFRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv'\n\nTLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]\nTLOBJECT_OUT = LIBRARY_DIR / 'tl'\nIMPORT_DEPTH = 2\n\nDOCS_IN_RES = GENERATOR_DIR / 'data/html'\nDOCS_OUT = Path('docs')\n\n\ndef generate(which, action='gen'):\n    from telethon_generator.parsers import\\\n        parse_errors, parse_methods, parse_tl, find_layer\n\n    from telethon_generator.generators import\\\n        generate_errors, generate_tlobjects, generate_docs, clean_tlobjects\n\n    layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))\n    errors = list(parse_errors(ERRORS_IN))\n    methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors}))\n\n    tlobjects = list(itertools.chain(*(\n        parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS)))\n\n    if not which:\n        which.extend(('tl', 'errors'))\n\n    clean = action == 'clean'\n    action = 'Cleaning' if clean else 'Generating'\n\n    if 'all' in which:\n        which.remove('all')\n        for x in ('tl', 'errors', 'docs'):\n            if x not in which:\n                which.append(x)\n\n    if 'tl' in which:\n        which.remove('tl')\n        print(action, 'TLObjects...')\n        if clean:\n            clean_tlobjects(TLOBJECT_OUT)\n        else:\n            generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT)\n\n    if 'errors' in which:\n        which.remove('errors')\n        print(action, 'RPCErrors...')\n        if clean:\n            if ERRORS_OUT.is_file():\n                ERRORS_OUT.unlink()\n        else:\n            with ERRORS_OUT.open('w') as file:\n                generate_errors(errors, file)\n\n    if 'docs' in which:\n        which.remove('docs')\n        print(action, 'documentation...')\n        if clean:\n            if DOCS_OUT.is_dir():\n                shutil.rmtree(str(DOCS_OUT))\n        else:\n            in_path = DOCS_IN_RES.resolve()\n            with TempWorkDir(DOCS_OUT):\n                generate_docs(tlobjects, methods, layer, in_path)\n\n    if 'json' in which:\n        which.remove('json')\n        print(action, 'JSON schema...')\n        json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS]\n        if clean:\n            for file in json_files:\n                if file.is_file():\n                    file.unlink()\n        else:\n            def gen_json(fin, fout):\n                meths = []\n                constructors = []\n                for tl in parse_tl(fin, layer):\n                    if tl.is_function:\n                        meths.append(tl.to_dict())\n                    else:\n                        constructors.append(tl.to_dict())\n                what = {'constructors': constructors, 'methods': meths}\n                with open(fout, 'w') as f:\n                    json.dump(what, f, indent=2)\n\n            for fs in zip(TLOBJECT_IN_TLS, json_files):\n                gen_json(*fs)\n\n    if which:\n        print(\n            'The following items were not understood:', which,\n            '\\n  Consider using only \"tl\", \"errors\" and/or \"docs\".'\n            '\\n  Using only \"clean\" will clean them. \"all\" to act on all.'\n            '\\n  For instance \"gen tl errors\".'\n        )\n\n\ndef main(argv):\n    if len(argv) >= 2 and argv[1] in ('gen', 'clean'):\n        generate(argv[2:], argv[1])\n\n    elif len(argv) >= 2 and argv[1] == 'pypi':\n        # Make sure tl.telethon.dev is up-to-date first\n        with urllib.request.urlopen(API_REF_URL) as resp:\n            html = resp.read()\n            m = re.search(br'layer\\s+(\\d+)', html)\n            if not m:\n                print('Failed to check that the API reference is up to date:', API_REF_URL)\n                return\n\n            from telethon_generator.parsers import find_layer\n            layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))\n            published_layer = int(m[1])\n            if published_layer != layer:\n                print('Published layer', published_layer, 'does not match current layer', layer, '.')\n                print('Make sure to update the API reference site first:', API_REF_URL)\n                return\n\n        # (Re)generate the code to make sure we don't push without it\n        generate(['tl', 'errors'])\n\n        # Try importing the telethon module to assert it has no errors\n        try:\n            import telethon\n        except Exception as e:\n            print('Packaging for PyPi aborted, importing the module failed.')\n            print(e)\n            return\n\n        remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']\n        for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False):\n            # setuptools is including __pycache__ for some reason (#1605)\n            if root.endswith('/__pycache__'):\n                remove_dirs.append(root)\n        for x in remove_dirs:\n            shutil.rmtree(x, ignore_errors=True)\n\n        run('python3 setup.py sdist', shell=True)\n        run('python3 setup.py bdist_wheel', shell=True)\n        run('twine upload dist/*', shell=True)\n        for x in ('build', 'dist', 'Telethon.egg-info'):\n            shutil.rmtree(x, ignore_errors=True)\n\n    else:\n        # e.g. install from GitHub\n        if GENERATOR_DIR.is_dir():\n            generate(['tl', 'errors'])\n\n        # Get the long description from the README file\n        with open('README.rst', 'r', encoding='utf-8') as f:\n            long_description = f.read()\n\n        with open('telethon/version.py', 'r', encoding='utf-8') as f:\n            version = re.search(r\"^__version__\\s*=\\s*'(.*)'.*$\",\n                                f.read(), flags=re.MULTILINE).group(1)\n        setup(\n            name='Telethon',\n            version=version,\n            description=\"Full-featured Telegram client library for Python 3\",\n            long_description=long_description,\n\n            url='https://github.com/LonamiWebs/Telethon',\n            download_url='https://github.com/LonamiWebs/Telethon/releases',\n\n            author='Lonami Exo',\n            author_email='totufals@hotmail.com',\n\n            license='MIT',\n\n            # See https://stackoverflow.com/a/40300957/4759433\n            # -> https://www.python.org/dev/peps/pep-0345/#requires-python\n            # -> http://setuptools.readthedocs.io/en/latest/setuptools.html\n            python_requires='>=3.5',\n\n            # See https://pypi.python.org/pypi?%3Aaction=list_classifiers\n            classifiers=[\n                #   3 - Alpha\n                #   4 - Beta\n                #   5 - Production/Stable\n                'Development Status :: 5 - Production/Stable',\n\n                'Intended Audience :: Developers',\n                'Topic :: Communications :: Chat',\n\n                'License :: OSI Approved :: MIT License',\n\n                'Programming Language :: Python :: 3',\n                'Programming Language :: Python :: 3.5',\n                'Programming Language :: Python :: 3.6',\n                'Programming Language :: Python :: 3.7',\n                'Programming Language :: Python :: 3.8',\n            ],\n            keywords='telegram api chat client library messaging mtproto',\n            packages=find_packages(exclude=[\n                'telethon_*', 'tests*'\n            ]),\n            install_requires=['pyaes', 'rsa'],\n            extras_require={\n                'cryptg': ['cryptg']\n            }\n        )\n\n\nif __name__ == '__main__':\n    with TempWorkDir():\n        main(sys.argv)\n"
  },
  {
    "path": "telethon/__init__.py",
    "content": "from .client.telegramclient import TelegramClient\nfrom .network import connection\nfrom .tl.custom import Button\nfrom .tl import patched as _  # import for its side-effects\nfrom . import version, events, utils, errors, types, functions, custom\n\n__version__ = version.__version__\n\n__all__ = [\n    'TelegramClient', 'Button',\n    'types', 'functions', 'custom', 'errors',\n    'events', 'utils', 'connection'\n]\n"
  },
  {
    "path": "telethon/_updates/__init__.py",
    "content": "from .entitycache import EntityCache\nfrom .messagebox import MessageBox, GapError, PrematureEndReason\nfrom .session import SessionState, ChannelState, Entity, EntityType\n"
  },
  {
    "path": "telethon/_updates/entitycache.py",
    "content": "from .session import EntityType, Entity\n\n\n_sentinel = object()\n\n\nclass EntityCache:\n    def __init__(\n        self,\n        hash_map: dict = _sentinel,\n        self_id: int = None,\n        self_bot: bool = None\n    ):\n        self.hash_map = {} if hash_map is _sentinel else hash_map\n        self.self_id = self_id\n        self.self_bot = self_bot\n\n    def set_self_user(self, id, bot, hash):\n        self.self_id = id\n        self.self_bot = bot\n        if hash:\n            self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER)\n\n    def get(self, id):\n        try:\n            hash, ty = self.hash_map[id]\n            return Entity(ty, id, hash)\n        except KeyError:\n            return None\n\n    def extend(self, users, chats):\n        # See https://core.telegram.org/api/min for \"issues\" with \"min constructors\".\n        self.hash_map.update(\n            (u.id, (\n                u.access_hash,\n                EntityType.BOT if u.bot else EntityType.USER,\n            ))\n            for u in users\n            if getattr(u, 'access_hash', None) and not u.min\n        )\n        self.hash_map.update(\n            (c.id, (\n                c.access_hash,\n                EntityType.MEGAGROUP if c.megagroup else (\n                    EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL\n                ),\n            ))\n            for c in chats\n            if getattr(c, 'access_hash', None) and not getattr(c, 'min', None)\n        )\n\n    def put(self, entity):\n        self.hash_map[entity.id] = (entity.hash, entity.ty)\n\n    def retain(self, filter):\n        self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)}\n\n    def __len__(self):\n        return len(self.hash_map)\n"
  },
  {
    "path": "telethon/_updates/messagebox.py",
    "content": "\"\"\"\nThis module deals with correct handling of updates, including gaps, and knowing when the code\nshould \"get difference\" (the set of updates that the client should know by now minus the set\nof updates that it actually knows).\n\nEach chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the \"entry point\").\nAt any given time, the message box may be either getting difference for them (entry is in\n[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be\nfound for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is\non its happy path.\n\nGaps are cleared when they are either resolved on their own (by waiting for a short time)\nor because we got the difference for the corresponding entry.\n\nWhile there are entries for which their difference must be fetched,\n[`MessageBox::check_deadlines`] will always return [`Instant::now`], since \"now\" is the time\nto get the difference.\n\"\"\"\nimport asyncio\nimport datetime\nimport time\nimport logging\nfrom enum import Enum\nfrom .session import SessionState, ChannelState\nfrom ..tl import types as tl, functions as fn\nfrom ..helpers import get_running_loop\n\n\n# Telegram sends `seq` equal to `0` when \"it doesn't matter\", so we use that value too.\nNO_SEQ = 0\n\n# See https://core.telegram.org/method/updates.getChannelDifference.\nBOT_CHANNEL_DIFF_LIMIT = 100000\nUSER_CHANNEL_DIFF_LIMIT = 100\n\n# > It may be useful to wait up to 0.5 seconds\nPOSSIBLE_GAP_TIMEOUT = 0.5\n\n# After how long without updates the client will \"timeout\".\n#\n# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the\n# updates that arrive in the meantime. After all updates are fetched when this happens, the\n# client will resume normal operation, and the timeout will reset.\n#\n# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates).\nNO_UPDATES_TIMEOUT = 15 * 60\n\n# object() but with a tag to make it easier to debug\nclass Sentinel:\n    __slots__ = ('tag',)\n\n    def __init__(self, tag=None):\n        self.tag = tag or '_'\n\n    def __repr__(self):\n        return self.tag\n\n# Entry \"enum\".\n# Account-wide `pts` includes private conversations (one-to-one) and small group chats.\nENTRY_ACCOUNT = Sentinel('ACCOUNT')\n# Account-wide `qts` includes only \"secret\" one-to-one chats.\nENTRY_SECRET = Sentinel('SECRET')\n# Integers will be Channel-specific `pts`, and includes \"megagroup\", \"broadcast\" and \"supergroup\" channels.\n\n# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET.\n# We don't define a name for this as libraries shouldn't do that though.\nLOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2\n\n_sentinel = Sentinel()\n\ndef next_updates_deadline():\n    return get_running_loop().time() + NO_UPDATES_TIMEOUT\n\ndef epoch():\n    return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)\n\nclass GapError(ValueError):\n    def __repr__(self):\n        return 'GapError()'\n\n\nclass PrematureEndReason(Enum):\n    TEMPORARY_SERVER_ISSUES = 'tmp'\n    BANNED = 'ban'\n\n\n# Represents the information needed to correctly handle a specific `tl::enums::Update`.\nclass PtsInfo:\n    __slots__ = ('pts', 'pts_count', 'entry')\n\n    def __init__(\n        self,\n        pts: int,\n        pts_count: int,\n        entry: object\n    ):\n        self.pts = pts\n        self.pts_count = pts_count\n        self.entry = entry\n\n    @classmethod\n    def from_update(cls, update):\n        pts = getattr(update, 'pts', None)\n        if pts:\n            pts_count = getattr(update, 'pts_count', None) or 0\n            try:\n                entry = update.message.peer_id.channel_id\n            except AttributeError:\n                entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT\n            return cls(pts=pts, pts_count=pts_count, entry=entry)\n\n        qts = getattr(update, 'qts', None)\n        if qts:\n            return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET)\n\n        return None\n\n    def __repr__(self):\n        return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.entry})'\n\n\n# The state of a particular entry in the message box.\nclass State:\n    __slots__ = ('pts', 'deadline')\n\n    def __init__(\n        self,\n        # Current local persistent timestamp.\n        pts: int,\n        # Next instant when we would get the update difference if no updates arrived before then.\n        deadline: float\n    ):\n        self.pts = pts\n        self.deadline = deadline\n\n    def __repr__(self):\n        return f'State(pts={self.pts}, deadline={self.deadline})'\n\n\n# > ### Recovering gaps\n# > […] Manually obtaining updates is also required in the following situations:\n# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above).\n# >   It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update\n# >   arrives, that fills the gap.\n#\n# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because\n# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone).\nclass PossibleGap:\n    __slots__ = ('deadline', 'updates')\n\n    def __init__(\n        self,\n        deadline: float,\n        # Pending updates (those with a larger PTS, producing the gap which may later be filled).\n        updates: list  # of updates\n    ):\n        self.deadline = deadline\n        self.updates = updates\n\n    def __repr__(self):\n        return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})'\n\n\n# Represents a \"message box\" (event `pts` for a specific entry).\n#\n# See https://core.telegram.org/api/updates#message-related-event-sequences.\nclass MessageBox:\n    __slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for')\n\n    def __init__(\n        self,\n        log,\n        # Map each entry to their current state.\n        map: dict = _sentinel,  # entry -> state\n\n        # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`.\n        date: datetime.datetime = epoch() + datetime.timedelta(seconds=1),\n        seq: int = NO_SEQ,\n\n        # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline).\n        next_deadline: object = None,  # entry\n\n        # Which entries have a gap and may soon trigger a need to get difference.\n        #\n        # If a gap is found, stores the required information to resolve it (when should it timeout and what updates\n        # should be held in case the gap is resolved on its own).\n        #\n        # Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have\n        # a gap in them).\n        possible_gaps: dict = _sentinel,  # entry -> possiblegap\n\n        # For which entries are we currently getting difference.\n        getting_diff_for: set = _sentinel,  # entry\n    ):\n        self._log = log\n        self.map = {} if map is _sentinel else map\n        self.date = date\n        self.seq = seq\n        self.next_deadline = next_deadline\n        self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps\n        self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for\n\n        if __debug__:\n            self._trace('MessageBox initialized')\n\n    def _trace(self, msg, *args, **kwargs):\n        # Calls to trace can't really be removed beforehand without some dark magic.\n        # So every call to trace is prefixed with `if __debug__`` instead, to remove\n        # it when using `python -O`. Probably unnecessary, but it's nice to avoid\n        # paying the cost for something that is not used.\n        self._log.log(LOG_LEVEL_TRACE, 'Current MessageBox state: seq = %r, date = %s, map = %r',\n                      self.seq, self.date.isoformat(), self.map)\n        self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs)\n\n    # region Creation, querying, and setting base state.\n\n    def load(self, session_state, channel_states):\n        \"\"\"\n        Create a [`MessageBox`] from a previously known update state.\n        \"\"\"\n        if __debug__:\n            self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states)\n\n        deadline = next_updates_deadline()\n\n        self.map.clear()\n        if session_state.pts != NO_SEQ:\n            self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline)\n        if session_state.qts != NO_SEQ:\n            self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline)\n        self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states)\n\n        self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc)\n        self.seq = session_state.seq\n        self.next_deadline = ENTRY_ACCOUNT\n\n    def session_state(self):\n        \"\"\"\n        Return the current state.\n\n        This should be used for persisting the state.\n        \"\"\"\n        return dict(\n            pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ,\n            qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,\n            date=self.date,\n            seq=self.seq,\n        ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)}\n\n    def is_empty(self) -> bool:\n        \"\"\"\n        Return true if the message box is empty and has no state yet.\n        \"\"\"\n        return ENTRY_ACCOUNT not in self.map\n\n    def check_deadlines(self):\n        \"\"\"\n        Return the next deadline when receiving updates should timeout.\n\n        If a deadline expired, the corresponding entries will be marked as needing to get its difference.\n        While there are entries pending of getting their difference, this method returns the current instant.\n        \"\"\"\n        now = get_running_loop().time()\n\n        if self.getting_diff_for:\n            return now\n\n        deadline = next_updates_deadline()\n\n        # Most of the time there will be zero or one gap in flight so finding the minimum is cheap.\n        if self.possible_gaps:\n            deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values()))\n        elif self.next_deadline in self.map:\n            deadline = min(deadline, self.map[self.next_deadline].deadline)\n\n        # asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that\n        # we find the same number again on repeated calls. Without the \"or equal\" part we would log the\n        # timeout for updates several times (it also makes sense to get difference if now is the deadline).\n        if now >= deadline:\n            # Check all expired entries and add them to the list that needs getting difference.\n            self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now >= gap.deadline)\n            self.getting_diff_for.update(entry for entry, state in self.map.items() if now >= state.deadline)\n\n            if __debug__:\n                self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for)\n\n            # When extending `getting_diff_for`, it's important to have the moral equivalent of\n            # `begin_get_diff` (that is, clear possible gaps if we're now getting difference).\n            for entry in self.getting_diff_for:\n                self.possible_gaps.pop(entry, None)\n\n        return deadline\n\n    # Reset the deadline for the periods without updates for the given entries.\n    #\n    # It also updates the next deadline time to reflect the new closest deadline.\n    def reset_deadlines(self, entries, deadline):\n        if not entries:\n            return\n        for entry in entries:\n            if entry not in self.map:\n                raise RuntimeError('Called reset_deadline on an entry for which we do not have state')\n            self.map[entry].deadline = deadline\n\n        if self.next_deadline in entries:\n            # If the updated deadline was the closest one, recalculate the new minimum.\n            self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0]\n        elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline:\n            # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one.\n            # Any entry will do, so the one from the last iteration is fine.\n            self.next_deadline = entry\n        # else an unrelated deadline was updated, so the closest one remains unchanged.\n\n    # Convenience to reset a channel's deadline, with optional timeout.\n    def reset_channel_deadline(self, channel_id, timeout):\n        self.reset_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT))\n\n    # Sets the update state.\n    #\n    # Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable\n    # updates will be fetched.\n    def set_state(self, state, reset=True):\n        if __debug__:\n            self._trace('Setting state %s', state)\n\n        deadline = next_updates_deadline()\n\n        if state.pts != NO_SEQ or not reset:\n            self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline)\n        else:\n            self.map.pop(ENTRY_ACCOUNT, None)\n\n        # Telegram seems to use the `qts` for bot accounts, but while applying difference,\n        # it might be reset back to 0. See issue #3873 for more details.\n        #\n        # During login, a value of zero would mean the `pts` is unknown,\n        # so the map shouldn't contain that entry.\n        # But while applying difference, if the value is zero, it (probably)\n        # truly means that's what should be used (hence the `reset` flag).\n        if state.qts != NO_SEQ or not reset:\n            self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline)\n        else:\n            self.map.pop(ENTRY_SECRET, None)\n\n        self.date = state.date\n        self.seq = state.seq\n\n    # Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs.\n    #\n    # The update state will only be updated if no entry was known previously.\n    def try_set_channel_state(self, id, pts):\n        if __debug__:\n            self._trace('Trying to set channel state for %r: %r', id, pts)\n\n        if id not in self.map:\n            self.map[id] = State(pts=pts, deadline=next_updates_deadline())\n\n    # Try to begin getting difference for the given entry.\n    # Fails if the entry does not have a previously-known state that can be used to get its difference.\n    #\n    # Clears any previous gaps.\n    def try_begin_get_diff(self, entry, reason):\n        if entry not in self.map:\n            # Won't actually be able to get difference for this entry if we don't have a pts to start off from.\n            if entry in self.possible_gaps:\n                raise RuntimeError('Should not have a possible_gap for an entry not in the state map')\n\n            if __debug__:\n                self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason)\n            return\n\n        if __debug__:\n            self._trace('Marking %r as needing difference because %s', entry, reason)\n        self.getting_diff_for.add(entry)\n        self.possible_gaps.pop(entry, None)\n\n    # Finish getting difference for the given entry.\n    #\n    # It also resets the deadline.\n    def end_get_diff(self, entry):\n        try:\n            self.getting_diff_for.remove(entry)\n        except KeyError:\n            raise RuntimeError('Called end_get_diff on an entry which was not getting diff for')\n\n        self.reset_deadlines({entry}, next_updates_deadline())\n        assert entry not in self.possible_gaps, \"gaps shouldn't be created while getting difference\"\n\n    # endregion Creation, querying, and setting base state.\n\n    # region \"Normal\" updates flow (processing and detection of gaps).\n\n    # Process an update and return what should be done with it.\n    #\n    # Updates corresponding to entries for which their difference is currently being fetched\n    # will be ignored. While according to the [updates' documentation]:\n    #\n    # > Implementations [have] to postpone updates received via the socket while\n    # > filling gaps in the event and `Update` sequences, as well as avoid filling\n    # > gaps in the same sequence.\n    #\n    # In practice, these updates should have also been retrieved through getting difference.\n    #\n    # [updates documentation] https://core.telegram.org/api/updates\n    def process_updates(\n        self,\n        updates,\n        chat_hashes,\n        result,  # out list of updates; returns list of user, chat, or raise if gap\n    ):\n\n        # v1 has never sent updates produced by the client itself to the handlers.\n        # However proper update handling requires those to be processed.\n        # This is an ugly workaround for that.\n        self_outgoing = getattr(updates, '_self_outgoing', False)\n        real_result = result\n        result = []\n\n        date = getattr(updates, 'date', None)\n        seq = getattr(updates, 'seq', None)\n        seq_start = getattr(updates, 'seq_start', None)\n        users = getattr(updates, 'users', None) or []\n        chats = getattr(updates, 'chats', None) or []\n\n        if __debug__:\n            self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s',\n                        seq, seq_start, date.isoformat() if date else None, updates)\n\n        if date is None:\n            # updatesTooLong is the only one with no date (we treat it as a gap)\n            self.try_begin_get_diff(ENTRY_ACCOUNT, 'received updatesTooLong')\n            raise GapError\n        if seq is None:\n            seq = NO_SEQ\n        if seq_start is None:\n            seq_start = seq\n\n        # updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field\n        updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates]\n\n        for u in updates:\n            u._self_outgoing = self_outgoing\n\n        # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors\n        # > there is no need to check `seq` or change a local state.\n        if seq_start != NO_SEQ:\n            if self.seq + 1 > seq_start:\n                # Skipping updates that were already handled\n                if __debug__:\n                    self._trace('Skipping updates as they should have already been handled')\n                return (users, chats)\n            elif self.seq + 1 < seq_start:\n                # Gap detected\n                self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap')\n                raise GapError\n            # else apply\n\n        def _sort_gaps(update):\n            pts = PtsInfo.from_update(update)\n            return pts.pts - pts.pts_count if pts else 0\n\n        reset_deadlines = set()  # temporary buffer\n\n        result.extend(filter(None, (\n            self.apply_pts_info(u, reset_deadlines=reset_deadlines)\n            # Telegram can send updates out of order (e.g. ReadChannelInbox first\n            # and then NewChannelMessage, both with the same pts, but the count is\n            # 0 and 1 respectively), so we sort them first.\n            for u in sorted(updates, key=_sort_gaps))))\n\n        self.reset_deadlines(reset_deadlines, next_updates_deadline())\n\n        if self.possible_gaps:\n            if __debug__:\n                self._trace('Trying to re-apply %r possible gaps', len(self.possible_gaps))\n\n            # For each update in possible gaps, see if the gap has been resolved already.\n            for key in list(self.possible_gaps.keys()):\n                self.possible_gaps[key].updates.sort(key=_sort_gaps)\n\n                for _ in range(len(self.possible_gaps[key].updates)):\n                    update = self.possible_gaps[key].updates.pop(0)\n\n                    # If this fails to apply, it will get re-inserted at the end.\n                    # All should fail, so the order will be preserved (it would've cycled once).\n                    update = self.apply_pts_info(update, reset_deadlines=None)\n                    if update:\n                        result.append(update)\n                        if __debug__:\n                            self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), update)\n\n            # Clear now-empty gaps.\n            self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates}\n\n        real_result.extend(u for u in result if not u._self_outgoing)\n\n        if result and not self.possible_gaps:\n            # > If the updates were applied, local *Updates* state must be updated\n            # > with `seq` (unless it's 0) and `date` from the constructor.\n            if __debug__:\n                self._trace('Updating seq as all updates were applied')\n            if date != epoch():\n                self.date = date\n            if seq != NO_SEQ:\n                self.seq = seq\n\n        return (users, chats)\n\n    # Tries to apply the input update if its `PtsInfo` follows the correct order.\n    #\n    # If the update can be applied, it is returned; otherwise, the update is stored in a\n    # possible gap (unless it was already handled or would be handled through getting\n    # difference) and `None` is returned.\n    def apply_pts_info(\n        self,\n        update,\n        *,\n        reset_deadlines,\n    ):\n        # This update means we need to call getChannelDifference to get the updates from the channel\n        if isinstance(update, tl.UpdateChannelTooLong):\n            self.try_begin_get_diff(update.channel_id, 'received updateChannelTooLong')\n            return None\n\n        pts = PtsInfo.from_update(update)\n        if not pts:\n            # No pts means that the update can be applied in any order.\n            if __debug__:\n                self._trace('No pts in update, so it can be applied in any order: %s', update)\n            return update\n\n        # As soon as we receive an update of any form related to messages (has `PtsInfo`),\n        # the \"no updates\" period for that entry is reset.\n        #\n        # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry.\n        #\n        # By the time this method returns, self.map will have an entry for which we can reset its deadline.\n        if reset_deadlines:\n            reset_deadlines.add(pts.entry)\n\n        if pts.entry in self.getting_diff_for:\n            # Note: early returning here also prevents gap from being inserted (which they should\n            # not be while getting difference).\n            if __debug__:\n                self._trace('Skipping update with %r as its difference is being fetched', pts)\n            return None\n\n        if pts.entry in self.map:\n            local_pts = self.map[pts.entry].pts\n            if local_pts + pts.pts_count > pts.pts:\n                # Ignore\n                if __debug__:\n                    self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update)\n                return None\n            elif local_pts + pts.pts_count < pts.pts:\n                # Possible gap\n                # TODO store chats too?\n                if __debug__:\n                    self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update)\n                if pts.entry not in self.possible_gaps:\n                    self.possible_gaps[pts.entry] = PossibleGap(\n                        deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT,\n                        updates=[]\n                    )\n\n                self.possible_gaps[pts.entry].updates.append(update)\n                return None\n            else:\n                # Apply\n                if __debug__:\n                    self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update)\n\n        # In a channel, we may immediately receive:\n        # * ReadChannelInbox (pts = X, pts_count = 0)\n        # * NewChannelMessage (pts = X, pts_count = 1)\n        #\n        # Notice how both `pts` are the same. If they were to be applied out of order, the first\n        # one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be\n        # less than `remote_pts`. So there is no risk by setting the `local_pts` to match the\n        # `remote_pts` here of missing the new message.\n        #\n        # The message would however be lost if we initialized the pts with the first one, since\n        # the second one would appear \"already handled\". To prevent this we set the pts to be\n        # one less when the count is 0 (which might be wrong and trigger a gap later on, but is\n        # unlikely). This will prevent us from losing updates in the unlikely scenario where these\n        # two updates arrive in different packets (and therefore couldn't be sorted beforehand).\n        if pts.entry in self.map:\n            self.map[pts.entry].pts = pts.pts\n        else:\n            # When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox`\n            # with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`.\n            # Note how the `pts` for the message is 2 and not 1 unlike the case described before!\n            # This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY),\n            # which forces the first update to be 1. But if we got difference with 1 and the second update\n            # also used 1, we would miss it, so Telegram probably uses 2 to work around that.\n            self.map[pts.entry] = State(\n                pts=(pts.pts - (0 if pts.pts_count else 1)) or 1,\n                deadline=next_updates_deadline()\n            )\n\n        return update\n\n    # endregion \"Normal\" updates flow (processing and detection of gaps).\n\n    # region Getting and applying account difference.\n\n    # Return the request that needs to be made to get the difference, if any.\n    def get_difference(self):\n        for entry in (ENTRY_ACCOUNT, ENTRY_SECRET):\n            if entry in self.getting_diff_for:\n                if entry not in self.map:\n                    raise RuntimeError('Should not try to get difference for an entry without known state')\n\n                gd = fn.updates.GetDifferenceRequest(\n                    pts=self.map[ENTRY_ACCOUNT].pts,\n                    pts_total_limit=None,\n                    date=self.date,\n                    qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,\n                )\n                if __debug__:\n                    self._trace('Requesting account difference %s', gd)\n                return gd\n\n        return None\n\n    # Similar to [`MessageBox::process_updates`], but using the result from getting difference.\n    def apply_difference(\n        self,\n        diff,\n        chat_hashes,\n    ):\n        if __debug__:\n            self._trace('Applying account difference %s', diff)\n\n        finish = None\n        result = None\n\n        if isinstance(diff, tl.updates.DifferenceEmpty):\n            finish = True\n            self.date = diff.date\n            self.seq = diff.seq\n            result = [], [], []\n        elif isinstance(diff, tl.updates.Difference):\n            finish = True\n            chat_hashes.extend(diff.users, diff.chats)\n            result = self.apply_difference_type(diff, chat_hashes)\n        elif isinstance(diff, tl.updates.DifferenceSlice):\n            finish = False\n            chat_hashes.extend(diff.users, diff.chats)\n            result = self.apply_difference_type(diff, chat_hashes)\n        elif isinstance(diff, tl.updates.DifferenceTooLong):\n            finish = True\n            self.map[ENTRY_ACCOUNT].pts = diff.pts  # the deadline will be reset once the diff ends\n            result = [], [], []\n\n        if finish:\n            account = ENTRY_ACCOUNT in self.getting_diff_for\n            secret = ENTRY_SECRET in self.getting_diff_for\n\n            if not account and not secret:\n                raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active')\n\n            # Both may be active if both expired at the same time.\n            if account:\n                self.end_get_diff(ENTRY_ACCOUNT)\n            if secret:\n                self.end_get_diff(ENTRY_SECRET)\n\n        return result\n\n    def apply_difference_type(\n        self,\n        diff,\n        chat_hashes,\n    ):\n        state = getattr(diff, 'intermediate_state', None) or diff.state\n        self.set_state(state, reset=False)\n\n        # diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage.\n        # We need to process those as if they were socket updates to discard any we have already handled.\n        updates = []\n        self.process_updates(tl.Updates(\n            updates=diff.other_updates,\n            users=diff.users,\n            chats=diff.chats,\n            date=epoch(),\n            seq=NO_SEQ,  # this way date is not used\n        ), chat_hashes, updates)\n\n        updates.extend(tl.UpdateNewMessage(\n            message=m,\n            pts=NO_SEQ,\n            pts_count=NO_SEQ,\n        ) for m in diff.new_messages)\n        updates.extend(tl.UpdateNewEncryptedMessage(\n            message=m,\n            qts=NO_SEQ,\n        ) for m in diff.new_encrypted_messages)\n\n        return updates, diff.users, diff.chats\n\n    def end_difference(self):\n        if __debug__:\n            self._trace('Ending account difference')\n\n        account = ENTRY_ACCOUNT in self.getting_diff_for\n        secret = ENTRY_SECRET in self.getting_diff_for\n\n        if not account and not secret:\n            raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active')\n\n        # Both may be active if both expired at the same time.\n        if account:\n            self.end_get_diff(ENTRY_ACCOUNT)\n        if secret:\n            self.end_get_diff(ENTRY_SECRET)\n\n    # endregion Getting and applying account difference.\n\n    # region Getting and applying channel difference.\n\n    # Return the request that needs to be made to get a channel's difference, if any.\n    def get_channel_difference(\n        self,\n        chat_hashes,\n    ):\n        entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None)\n        if not entry:\n            return None\n\n        packed = chat_hashes.get(entry)\n        if not packed:\n            # Cannot get channel difference as we're missing its hash\n            # TODO we should probably log this\n            self.end_get_diff(entry)\n            # Remove the outdated `pts` entry from the map so that the next update can correct\n            # it. Otherwise, it will spam that the access hash is missing.\n            self.map.pop(entry, None)\n            return None\n\n        state = self.map.get(entry)\n        if not state:\n            raise RuntimeError('Should not try to get difference for an entry without known state')\n\n        gd = fn.updates.GetChannelDifferenceRequest(\n            force=False,\n            channel=tl.InputChannel(packed.id, packed.hash),\n            filter=tl.ChannelMessagesFilterEmpty(),\n            pts=state.pts,\n            limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT\n        )\n        if __debug__:\n            self._trace('Requesting channel difference %s', gd)\n        return gd\n\n    # Similar to [`MessageBox::process_updates`], but using the result from getting difference.\n    def apply_channel_difference(\n        self,\n        request,\n        diff,\n        chat_hashes,\n    ):\n        entry = request.channel.channel_id\n        if __debug__:\n            self._trace('Applying channel difference for %r: %s', entry, diff)\n\n        self.possible_gaps.pop(entry, None)\n\n        if isinstance(diff, tl.updates.ChannelDifferenceEmpty):\n            assert diff.final\n            self.end_get_diff(entry)\n            self.map[entry].pts = diff.pts\n            return [], [], []\n        elif isinstance(diff, tl.updates.ChannelDifferenceTooLong):\n            assert diff.final\n            self.map[entry].pts = diff.dialog.pts\n            chat_hashes.extend(diff.users, diff.chats)\n            self.reset_channel_deadline(entry, diff.timeout)\n            # This `diff` has the \"latest messages and corresponding chats\", but it would\n            # be strange to give the user only partial changes of these when they would\n            # expect all updates to be fetched. Instead, nothing is returned.\n            return [], [], []\n        elif isinstance(diff, tl.updates.ChannelDifference):\n            if diff.final:\n                self.end_get_diff(entry)\n\n            self.map[entry].pts = diff.pts\n            chat_hashes.extend(diff.users, diff.chats)\n\n            updates = []\n            self.process_updates(tl.Updates(\n                updates=diff.other_updates,\n                users=diff.users,\n                chats=diff.chats,\n                date=epoch(),\n                seq=NO_SEQ,  # this way date is not used\n            ), chat_hashes, updates)\n\n            updates.extend(tl.UpdateNewChannelMessage(\n                message=m,\n                pts=NO_SEQ,\n                pts_count=NO_SEQ,\n            ) for m in diff.new_messages)\n            self.reset_channel_deadline(entry, None)\n\n            return updates, diff.users, diff.chats\n\n    def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes):\n        entry = request.channel.channel_id\n        if __debug__:\n            self._trace('Ending channel difference for %r because %s', entry, reason)\n\n        if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES:\n            # Temporary issues. End getting difference without updating the pts so we can retry later.\n            self.possible_gaps.pop(entry, None)\n            self.end_get_diff(entry)\n        elif reason == PrematureEndReason.BANNED:\n            # Banned in the channel. Forget its state since we can no longer fetch updates from it.\n            self.possible_gaps.pop(entry, None)\n            self.end_get_diff(entry)\n            del self.map[entry]\n        else:\n            raise RuntimeError('Unknown reason to end channel difference')\n\n    # endregion Getting and applying channel difference.\n"
  },
  {
    "path": "telethon/_updates/session.py",
    "content": "from typing import Optional, Tuple\nfrom enum import IntEnum\nfrom ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel\nimport struct\n\nclass SessionState:\n    \"\"\"\n    Stores the information needed to fetch updates and about the current user.\n\n    * user_id: 64-bit number representing the user identifier.\n    * dc_id: 32-bit number relating to the datacenter identifier where the user is.\n    * bot: is the logged-in user a bot?\n    * pts: 64-bit number holding the state needed to fetch updates.\n    * qts: alternative 64-bit number holding the state needed to fetch updates.\n    * date: 64-bit number holding the date needed to fetch updates.\n    * seq: 64-bit-number holding the sequence number needed to fetch updates.\n    * takeout_id: 64-bit-number holding the identifier of the current takeout session.\n\n    Note that some of the numbers will only use 32 out of the 64 available bits.\n    However, for future-proofing reasons, we recommend you pretend they are 64-bit long.\n    \"\"\"\n    __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id')\n\n    def __init__(\n        self,\n        user_id: int,\n        dc_id: int,\n        bot: bool,\n        pts: int,\n        qts: int,\n        date: int,\n        seq: int,\n        takeout_id: Optional[int]\n    ):\n        self.user_id = user_id\n        self.dc_id = dc_id\n        self.bot = bot\n        self.pts = pts\n        self.qts = qts\n        self.date = date\n        self.seq = seq\n        self.takeout_id = takeout_id\n\n    def __repr__(self):\n        return repr({k: getattr(self, k) for k in self.__slots__})\n\n\nclass ChannelState:\n    \"\"\"\n    Stores the information needed to fetch updates from a channel.\n\n    * channel_id: 64-bit number representing the channel identifier.\n    * pts: 64-bit number holding the state needed to fetch updates.\n    \"\"\"\n    __slots__ = ('channel_id', 'pts')\n\n    def __init__(\n        self,\n        channel_id: int,\n        pts: int,\n    ):\n        self.channel_id = channel_id\n        self.pts = pts\n\n    def __repr__(self):\n        return repr({k: getattr(self, k) for k in self.__slots__})\n\n\nclass EntityType(IntEnum):\n    \"\"\"\n    You can rely on the type value to be equal to the ASCII character one of:\n\n    * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``.\n    * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``.\n    * 'G' (71): this entity belongs to a small group :tl:`Chat`.\n    * 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`.\n    * 'M' (77): this entity belongs to a megagroup :tl:`Channel`.\n    * 'E' (69): this entity belongs to an \"enormous\" \"gigagroup\" :tl:`Channel`.\n    \"\"\"\n    USER = ord('U')\n    BOT = ord('B')\n    GROUP = ord('G')\n    CHANNEL = ord('C')\n    MEGAGROUP = ord('M')\n    GIGAGROUP = ord('E')\n\n    def canonical(self):\n        \"\"\"\n        Return the canonical version of this type.\n        \"\"\"\n        return _canon_entity_types[self]\n\n\n_canon_entity_types = {\n    EntityType.USER: EntityType.USER,\n    EntityType.BOT: EntityType.USER,\n    EntityType.GROUP: EntityType.GROUP,\n    EntityType.CHANNEL: EntityType.CHANNEL,\n    EntityType.MEGAGROUP: EntityType.CHANNEL,\n    EntityType.GIGAGROUP: EntityType.CHANNEL,\n}\n\n\nclass Entity:\n    \"\"\"\n    Stores the information needed to use a certain user, chat or channel with the API.\n\n    * ty: 8-bit number indicating the type of the entity (of type `EntityType`).\n    * id: 64-bit number uniquely identifying the entity among those of the same type.\n    * hash: 64-bit signed number needed to use this entity with the API.\n\n    The string representation of this class is considered to be stable, for as long as\n    Telegram doesn't need to add more fields to the entities. It can also be converted\n    to bytes with ``bytes(entity)``, for a more compact representation.\n    \"\"\"\n    __slots__ = ('ty', 'id', 'hash')\n\n    def __init__(\n        self,\n        ty: EntityType,\n        id: int,\n        hash: int\n    ):\n        self.ty = ty\n        self.id = id\n        self.hash = hash\n\n    @property\n    def is_user(self):\n        \"\"\"\n        ``True`` if the entity is either a user or a bot.\n        \"\"\"\n        return self.ty in (EntityType.USER, EntityType.BOT)\n\n    @property\n    def is_group(self):\n        \"\"\"\n        ``True`` if the entity is a small group chat or `megagroup`_.\n\n        .. _megagroup: https://telegram.org/blog/supergroups5k\n        \"\"\"\n        return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP)\n\n    @property\n    def is_broadcast(self):\n        \"\"\"\n        ``True`` if the entity is a broadcast channel or `broadcast group`_.\n\n        .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members\n        \"\"\"\n        return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP)\n\n    @classmethod\n    def from_str(cls, string: str):\n        \"\"\"\n        Convert the string into an `Entity`.\n        \"\"\"\n        try:\n            ty, id, hash = string.split('.')\n            ty, id, hash = ord(ty), int(id), int(hash)\n        except AttributeError:\n            raise TypeError(f'expected str, got {string!r}') from None\n        except (TypeError, ValueError):\n            raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None\n\n        return cls(EntityType(ty), id, hash)\n\n    @classmethod\n    def from_bytes(cls, blob):\n        \"\"\"\n        Convert the bytes into an `Entity`.\n        \"\"\"\n        try:\n            ty, id, hash = struct.unpack('<Bqq', blob)\n        except struct.error:\n            raise ValueError(f'malformed entity data, got {blob!r}') from None\n\n        return cls(EntityType(ty), id, hash)\n\n    def __str__(self):\n        return f'{chr(self.ty)}.{self.id}.{self.hash}'\n\n    def __bytes__(self):\n        return struct.pack('<Bqq', self.ty, self.id, self.hash)\n\n    def _as_input_peer(self):\n        if self.is_user:\n            return InputPeerUser(self.id, self.hash)\n        elif self.ty == EntityType.GROUP:\n            return InputPeerChat(self.id)\n        else:\n            return InputPeerChannel(self.id, self.hash)\n\n    def __repr__(self):\n        return repr({k: getattr(self, k) for k in self.__slots__})\n"
  },
  {
    "path": "telethon/client/__init__.py",
    "content": "\"\"\"\nThis package defines clients as subclasses of others, and then a single\n`telethon.client.telegramclient.TelegramClient` which is subclass of them\nall to provide the final unified interface while the methods can live in\ndifferent subclasses to be more maintainable.\n\nThe ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the\nfirst implementor is `telethon.client.users.UserMethods`, since calling\nrequests require them to be resolved first, and that requires accessing\nentities (users).\n\"\"\"\nfrom .telegrambaseclient import TelegramBaseClient\nfrom .users import UserMethods  # Required for everything\nfrom .messageparse import MessageParseMethods  # Required for messages\nfrom .uploads import UploadMethods  # Required for messages to send files\nfrom .updates import UpdateMethods  # Required for buttons (register callbacks)\nfrom .buttons import ButtonMethods  # Required for messages to use buttons\nfrom .messages import MessageMethods\nfrom .chats import ChatMethods\nfrom .dialogs import DialogMethods\nfrom .downloads import DownloadMethods\nfrom .account import AccountMethods\nfrom .auth import AuthMethods\nfrom .bots import BotMethods\nfrom .telegramclient import TelegramClient\n"
  },
  {
    "path": "telethon/client/account.py",
    "content": "import functools\nimport inspect\nimport typing\n\nfrom .users import _NOT_A_REQUEST\nfrom .. import helpers, utils\nfrom ..tl import functions, TLRequest\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\n# TODO Make use of :tl:`InvokeWithMessagesRange` somehow\n#      For that, we need to use :tl:`GetSplitRanges` first.\nclass _TakeoutClient:\n    \"\"\"\n    Proxy object over the client.\n    \"\"\"\n    __PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__')\n\n    def __init__(self, finalize, client, request):\n        # We use the name mangling for attributes to make them inaccessible\n        # from within the shadowed client object and to distinguish them from\n        # its own attributes where needed.\n        self.__finalize = finalize\n        self.__client = client\n        self.__request = request\n        self.__success = None\n\n    @property\n    def success(self):\n        return self.__success\n\n    @success.setter\n    def success(self, value):\n        self.__success = value\n\n    async def __aenter__(self):\n        # Enter/Exit behaviour is \"overrode\", we don't want to call start.\n        client = self.__client\n        if client.session.takeout_id is None:\n            client.session.takeout_id = (await client(self.__request)).id\n        elif self.__request is not None:\n            raise ValueError(\"Can't send a takeout request while another \"\n                \"takeout for the current session still not been finished yet.\")\n        return self\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        if self.__success is None and self.__finalize:\n            self.__success = exc_type is None\n\n        if self.__success is not None:\n            result = await self(functions.account.FinishTakeoutSessionRequest(\n                self.__success))\n            if not result:\n                raise ValueError(\"Failed to finish the takeout.\")\n            self.session.takeout_id = None\n\n    __enter__ = helpers._sync_enter\n    __exit__ = helpers._sync_exit\n\n    async def __call__(self, request, ordered=False):\n        takeout_id = self.__client.session.takeout_id\n        if takeout_id is None:\n            raise ValueError('Takeout mode has not been initialized '\n                '(are you calling outside of \"with\"?)')\n\n        single = not utils.is_list_like(request)\n        requests = ((request,) if single else request)\n        wrapped = []\n        for r in requests:\n            if not isinstance(r, TLRequest):\n                raise _NOT_A_REQUEST()\n            await r.resolve(self, utils)\n            wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r))\n\n        return await self.__client(\n            wrapped[0] if single else wrapped, ordered=ordered)\n\n    def __getattribute__(self, name):\n        # We access class via type() because __class__ will recurse infinitely.\n        # Also note that since we've name-mangled our own class attributes,\n        # they'll be passed to __getattribute__() as already decorated. For\n        # example, 'self.__client' will be passed as '_TakeoutClient__client'.\n        # https://docs.python.org/3/tutorial/classes.html#private-variables\n        if name.startswith('__') and name not in type(self).__PROXY_INTERFACE:\n            raise AttributeError  # force call of __getattr__\n\n        # Try to access attribute in the proxy object and check for the same\n        # attribute in the shadowed object (through our __getattr__) if failed.\n        return super().__getattribute__(name)\n\n    def __getattr__(self, name):\n        value = getattr(self.__client, name)\n        if inspect.ismethod(value):\n            # Emulate bound methods behavior by partially applying our proxy\n            # class as the self parameter instead of the client.\n            return functools.partial(\n                getattr(self.__client.__class__, name), self)\n\n        return value\n\n    def __setattr__(self, name, value):\n        if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))):\n            # This is our own name-mangled attribute, keep calm.\n            return super().__setattr__(name, value)\n        return setattr(self.__client, name, value)\n\n\nclass AccountMethods:\n    def takeout(\n            self: 'TelegramClient',\n            finalize: bool = True,\n            *,\n            contacts: bool = None,\n            users: bool = None,\n            chats: bool = None,\n            megagroups: bool = None,\n            channels: bool = None,\n            files: bool = None,\n            max_file_size: bool = None) -> 'TelegramClient':\n        \"\"\"\n        Returns a :ref:`telethon-client` which calls methods behind a takeout session.\n\n        It does so by creating a proxy object over the current client through\n        which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap\n        them. In other words, returns the current client modified so that\n        requests are done as a takeout:\n\n        Some of the calls made through the takeout session will have lower\n        flood limits. This is useful if you want to export the data from\n        conversations or mass-download media, since the rate limits will\n        be lower. Only some requests will be affected, and you will need\n        to adjust the `wait_time` of methods like `client.iter_messages\n        <telethon.client.messages.MessageMethods.iter_messages>`.\n\n        By default, all parameters are `None`, and you need to enable those\n        you plan to use by setting them to either `True` or `False`.\n\n        You should ``except errors.TakeoutInitDelayError as e``, since this\n        exception will raise depending on the condition of the session. You\n        can then access ``e.seconds`` to know how long you should wait for\n        before calling the method again.\n\n        There's also a `success` property available in the takeout proxy\n        object, so from the `with` body you can set the boolean result that\n        will be sent back to Telegram. But if it's left `None` as by\n        default, then the action is based on the `finalize` parameter. If\n        it's `True` then the takeout will be finished, and if no exception\n        occurred during it, then `True` will be considered as a result.\n        Otherwise, the takeout will not be finished and its ID will be\n        preserved for future usage as `client.session.takeout_id\n        <telethon.sessions.abstract.Session.takeout_id>`.\n\n        Arguments\n            finalize (`bool`):\n                Whether the takeout session should be finalized upon\n                exit or not.\n\n            contacts (`bool`):\n                Set to `True` if you plan on downloading contacts.\n\n            users (`bool`):\n                Set to `True` if you plan on downloading information\n                from users and their private conversations with you.\n\n            chats (`bool`):\n                Set to `True` if you plan on downloading information\n                from small group chats, such as messages and media.\n\n            megagroups (`bool`):\n                Set to `True` if you plan on downloading information\n                from megagroups (channels), such as messages and media.\n\n            channels (`bool`):\n                Set to `True` if you plan on downloading information\n                from broadcast channels, such as messages and media.\n\n            files (`bool`):\n                Set to `True` if you plan on downloading media and\n                you don't only wish to export messages.\n\n            max_file_size (`int`):\n                The maximum file size, in bytes, that you plan\n                to download for each message with media.\n\n        Example\n            .. code-block:: python\n\n                from telethon import errors\n\n                try:\n                    async with client.takeout() as takeout:\n                        await client.get_messages('me')  # normal call\n                        await takeout.get_messages('me')  # wrapped through takeout (less limits)\n\n                        async for message in takeout.iter_messages(chat, wait_time=0):\n                            ...  # Do something with the message\n\n                except errors.TakeoutInitDelayError as e:\n                    print('Must wait', e.seconds, 'before takeout')\n        \"\"\"\n        request_kwargs = dict(\n            contacts=contacts,\n            message_users=users,\n            message_chats=chats,\n            message_megagroups=megagroups,\n            message_channels=channels,\n            files=files,\n            file_max_size=max_file_size\n        )\n        arg_specified = (arg is not None for arg in request_kwargs.values())\n\n        if self.session.takeout_id is None or any(arg_specified):\n            request = functions.account.InitTakeoutSessionRequest(\n                **request_kwargs)\n        else:\n            request = None\n\n        return _TakeoutClient(finalize, self, request)\n\n    async def end_takeout(self: 'TelegramClient', success: bool) -> bool:\n        \"\"\"\n        Finishes the current takeout session.\n\n        Arguments\n            success (`bool`):\n                Whether the takeout completed successfully or not.\n\n        Returns\n            `True` if the operation was successful, `False` otherwise.\n\n        Example\n            .. code-block:: python\n\n                await client.end_takeout(success=False)\n        \"\"\"\n        try:\n            async with _TakeoutClient(True, self, None) as takeout:\n                takeout.success = success\n        except ValueError:\n            return False\n        return True\n"
  },
  {
    "path": "telethon/client/auth.py",
    "content": "import getpass\nimport inspect\nimport os\nimport sys\nimport typing\nimport warnings\n\nfrom .. import utils, helpers, errors, password as pwd_mod\nfrom ..tl import types, functions, custom\nfrom .._updates import SessionState\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nclass AuthMethods:\n\n    # region Public methods\n\n    def start(\n            self: 'TelegramClient',\n            phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '),\n            password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '),\n            *,\n            bot_token: str = None,\n            force_sms: bool = False,\n            code_callback: typing.Callable[[], typing.Union[str, int]] = None,\n            first_name: str = 'New User',\n            last_name: str = '',\n            max_attempts: int = 3) -> 'TelegramClient':\n        \"\"\"\n        Starts the client (connects and logs in if necessary).\n\n        By default, this method will be interactive (asking for\n        user input if needed), and will handle 2FA if enabled too.\n\n        If the event loop is already running, this method returns a\n        coroutine that you should await on your own code; otherwise\n        the loop is ran until said coroutine completes.\n\n        Arguments\n            phone (`str` | `int` | `callable`):\n                The phone (or callable without arguments to get it)\n                to which the code will be sent. If a bot-token-like\n                string is given, it will be used as such instead.\n                The argument may be a coroutine.\n\n            password (`str`, `callable`, optional):\n                The password for 2 Factor Authentication (2FA).\n                This is only required if it is enabled in your account.\n                The argument may be a coroutine.\n\n            bot_token (`str`):\n                Bot Token obtained by `@BotFather <https://t.me/BotFather>`_\n                to log in as a bot. Cannot be specified with ``phone`` (only\n                one of either allowed).\n\n            force_sms (`bool`, optional):\n                Whether to force sending the code request as SMS.\n                This only makes sense when signing in with a `phone`.\n\n            code_callback (`callable`, optional):\n                A callable that will be used to retrieve the Telegram\n                login code. Defaults to `input()`.\n                The argument may be a coroutine.\n\n            first_name (`str`, optional):\n                The first name to be used if signing up. This has no\n                effect if the account already exists and you sign in.\n\n            last_name (`str`, optional):\n                Similar to the first name, but for the last. Optional.\n\n            max_attempts (`int`, optional):\n                How many times the code/password callback should be\n                retried or switching between signing in and signing up.\n\n        Returns\n            This `TelegramClient`, so initialization\n            can be chained with ``.start()``.\n\n        Example\n            .. code-block:: python\n\n                client = TelegramClient('anon', api_id, api_hash)\n\n                # Starting as a bot account\n                await client.start(bot_token=bot_token)\n\n                # Starting as a user account\n                await client.start(phone)\n                # Please enter the code you received: 12345\n                # Please enter your password: *******\n                # (You are now logged in)\n\n                # Starting using a context manager (this calls start()):\n                with client:\n                    pass\n        \"\"\"\n        if code_callback is None:\n            def code_callback():\n                return input('Please enter the code you received: ')\n        elif not callable(code_callback):\n            raise ValueError(\n                'The code_callback parameter needs to be a callable '\n                'function that returns the code you received by Telegram.'\n            )\n\n        if not phone and not bot_token:\n            raise ValueError('No phone number or bot token provided.')\n\n        if phone and bot_token and not callable(phone):\n            raise ValueError('Both a phone and a bot token provided, '\n                             'must only provide one of either')\n\n        coro = self._start(\n            phone=phone,\n            password=password,\n            bot_token=bot_token,\n            force_sms=force_sms,\n            code_callback=code_callback,\n            first_name=first_name,\n            last_name=last_name,\n            max_attempts=max_attempts\n        )\n        return (\n            coro if self.loop.is_running()\n            else self.loop.run_until_complete(coro)\n        )\n\n    async def _start(\n            self: 'TelegramClient', phone, password, bot_token, force_sms,\n            code_callback, first_name, last_name, max_attempts):\n        if not self.is_connected():\n            await self.connect()\n\n        # Rather than using `is_user_authorized`, use `get_me`. While this is\n        # more expensive and needs to retrieve more data from the server, it\n        # enables the library to warn users trying to login to a different\n        # account. See #1172.\n        me = await self.get_me()\n        if me is not None:\n            # The warnings here are on a best-effort and may fail.\n            if bot_token:\n                # bot_token's first part has the bot ID, but it may be invalid\n                # so don't try to parse as int (instead cast our ID to string).\n                if bot_token[:bot_token.find(':')] != str(me.id):\n                    warnings.warn(\n                        'the session already had an authorized user so it did '\n                        'not login to the bot account using the provided bot_token; '\n                        'if you were expecting a different user, check whether '\n                        'you are accidentally reusing an existing session'\n                    )\n            elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone:\n                warnings.warn(\n                    'the session already had an authorized user so it did '\n                    'not login to the user account using the provided phone; '\n                    'if you were expecting a different user, check whether '\n                    'you are accidentally reusing an existing session'\n                )\n\n            return self\n\n        if not bot_token:\n            # Turn the callable into a valid phone number (or bot token)\n            while callable(phone):\n                value = phone()\n                if inspect.isawaitable(value):\n                    value = await value\n\n                if ':' in value:\n                    # Bot tokens have 'user_id:access_hash' format\n                    bot_token = value\n                    break\n\n                phone = utils.parse_phone(value) or phone\n\n        if bot_token:\n            await self.sign_in(bot_token=bot_token)\n            return self\n\n        me = None\n        attempts = 0\n        two_step_detected = False\n\n        await self.send_code_request(phone, force_sms=force_sms)\n        while attempts < max_attempts:\n            try:\n                value = code_callback()\n                if inspect.isawaitable(value):\n                    value = await value\n\n                # Since sign-in with no code works (it sends the code)\n                # we must double-check that here. Else we'll assume we\n                # logged in, and it will return None as the User.\n                if not value:\n                    raise errors.PhoneCodeEmptyError(request=None)\n\n                # Raises SessionPasswordNeededError if 2FA enabled\n                me = await self.sign_in(phone, code=value)\n                break\n            except errors.SessionPasswordNeededError:\n                two_step_detected = True\n                break\n            except (errors.PhoneCodeEmptyError,\n                    errors.PhoneCodeExpiredError,\n                    errors.PhoneCodeHashEmptyError,\n                    errors.PhoneCodeInvalidError):\n                print('Invalid code. Please try again.', file=sys.stderr)\n\n            attempts += 1\n        else:\n            raise RuntimeError(\n                '{} consecutive sign-in attempts failed. Aborting'\n                .format(max_attempts)\n            )\n\n        if two_step_detected:\n            if not password:\n                raise ValueError(\n                    \"Two-step verification is enabled for this account. \"\n                    \"Please provide the 'password' argument to 'start()'.\"\n                )\n\n            if callable(password):\n                for _ in range(max_attempts):\n                    try:\n                        value = password()\n                        if inspect.isawaitable(value):\n                            value = await value\n\n                        me = await self.sign_in(phone=phone, password=value)\n                        break\n                    except errors.PasswordHashInvalidError:\n                        print('Invalid password. Please try again',\n                              file=sys.stderr)\n                else:\n                    raise errors.PasswordHashInvalidError(request=None)\n            else:\n                me = await self.sign_in(phone=phone, password=password)\n\n        # We won't reach here if any step failed (exit by exception)\n        signed, name = 'Signed in successfully as ', utils.get_display_name(me)\n        tos = '; remember to not break the ToS or you will risk an account ban!'\n        try:\n            print(signed, name, tos, sep='')\n        except UnicodeEncodeError:\n            # Some terminals don't support certain characters\n            print(signed, name.encode('utf-8', errors='ignore')\n                              .decode('ascii', errors='ignore'), tos, sep='')\n\n        return self\n\n    def _parse_phone_and_hash(self, phone, phone_hash):\n        \"\"\"\n        Helper method to both parse and validate phone and its hash.\n        \"\"\"\n        phone = utils.parse_phone(phone) or self._phone\n        if not phone:\n            raise ValueError(\n                'Please make sure to call send_code_request first.'\n            )\n\n        phone_hash = phone_hash or self._phone_code_hash.get(phone, None)\n        if not phone_hash:\n            raise ValueError('You also need to provide a phone_code_hash.')\n\n        return phone, phone_hash\n\n    async def sign_in(\n            self: 'TelegramClient',\n            phone: str = None,\n            code: typing.Union[str, int] = None,\n            *,\n            password: str = None,\n            bot_token: str = None,\n            phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]':\n        \"\"\"\n        Logs in to Telegram to an existing user or bot account.\n\n        You should only use this if you are not authorized yet.\n\n        This method will send the code if it's not provided.\n\n        .. note::\n\n            In most cases, you should simply use `start()` and not this method.\n\n        Arguments\n            phone (`str` | `int`):\n                The phone to send the code to if no code was provided,\n                or to override the phone that was previously used with\n                these requests.\n\n            code (`str` | `int`):\n                The code that Telegram sent. Note that if you have sent this\n                code through the application itself it will immediately\n                expire. If you want to send the code, obfuscate it somehow.\n                If you're not doing any of this you can ignore this note.\n\n            password (`str`):\n                2FA password, should be used if a previous call raised\n                ``SessionPasswordNeededError``.\n\n            bot_token (`str`):\n                Used to sign in as a bot. Not all requests will be available.\n                This should be the hash the `@BotFather <https://t.me/BotFather>`_\n                gave you.\n\n            phone_code_hash (`str`, optional):\n                The hash returned by `send_code_request`. This can be left as\n                `None` to use the last hash known for the phone to be used.\n\n        Returns\n            The signed in user, or the information about\n            :meth:`send_code_request`.\n\n        Example\n            .. code-block:: python\n\n                phone = '+34 123 123 123'\n                await client.sign_in(phone)  # send code\n\n                code = input('enter code: ')\n                await client.sign_in(phone, code)\n        \"\"\"\n        me = await self.get_me()\n        if me:\n            return me\n\n        if phone and not code and not password:\n            return await self.send_code_request(phone)\n        elif code:\n            phone, phone_code_hash = \\\n                self._parse_phone_and_hash(phone, phone_code_hash)\n\n            # May raise PhoneCodeEmptyError, PhoneCodeExpiredError,\n            # PhoneCodeHashEmptyError or PhoneCodeInvalidError.\n            request = functions.auth.SignInRequest(\n                phone, phone_code_hash, str(code)\n            )\n        elif password:\n            pwd = await self(functions.account.GetPasswordRequest())\n            request = functions.auth.CheckPasswordRequest(\n                pwd_mod.compute_check(pwd, password)\n            )\n        elif bot_token:\n            request = functions.auth.ImportBotAuthorizationRequest(\n                flags=0, bot_auth_token=bot_token,\n                api_id=self.api_id, api_hash=self.api_hash\n            )\n        else:\n            raise ValueError(\n                'You must provide a phone and a code the first time, '\n                'and a password only if an RPCError was raised before.'\n            )\n\n        try:\n            result = await self(request)\n        except errors.PhoneCodeExpiredError:\n            self._phone_code_hash.pop(phone, None)\n            raise\n\n        if isinstance(result, types.auth.AuthorizationSignUpRequired):\n            # Emulate pre-layer 104 behaviour\n            self._tos = result.terms_of_service\n            raise errors.PhoneNumberUnoccupiedError(request=request)\n\n        return await self._on_login(result.user)\n\n    async def sign_up(\n            self: 'TelegramClient',\n            code: typing.Union[str, int],\n            first_name: str,\n            last_name: str = '',\n            *,\n            phone: str = None,\n            phone_code_hash: str = None) -> 'types.User':\n        \"\"\"\n        This method can no longer be used, and will immediately raise a ``ValueError``.\n        See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.\n        \"\"\"\n        raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')\n\n    async def _on_login(self, user):\n        \"\"\"\n        Callback called whenever the login or sign up process completes.\n\n        Returns the input user parameter.\n        \"\"\"\n        self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)\n        self._authorized = True\n\n        state = await self(functions.updates.GetStateRequest())\n        # the server may send an old qts in getState\n        difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts))\n\n        if isinstance(difference, types.updates.Difference):\n            state = difference.state\n        elif isinstance(difference, types.updates.DifferenceSlice):\n            state = difference.intermediate_state\n        elif isinstance(difference, types.updates.DifferenceTooLong):\n            state.pts = difference.pts\n\n        self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), [])\n\n        return user\n\n    async def send_code_request(\n            self: 'TelegramClient',\n            phone: str,\n            *,\n            force_sms: bool = False,\n            _retry_count: int = 0) -> 'types.auth.SentCode':\n        \"\"\"\n        Sends the Telegram code needed to login to the given phone number.\n\n        Arguments\n            phone (`str` | `int`):\n                The phone to which the code will be sent.\n\n            force_sms (`bool`, optional):\n                Whether to force sending as SMS. This has been deprecated.\n                See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.\n\n        Returns\n            An instance of :tl:`SentCode`.\n\n        Example\n            .. code-block:: python\n\n                phone = '+34 123 123 123'\n                sent = await client.send_code_request(phone)\n                print(sent)\n        \"\"\"\n        if force_sms:\n            warnings.warn('force_sms has been deprecated and no longer works')\n            force_sms = False\n\n        result = None\n        phone = utils.parse_phone(phone) or self._phone\n        phone_hash = self._phone_code_hash.get(phone)\n\n        if not phone_hash:\n            try:\n                result = await self(functions.auth.SendCodeRequest(\n                    phone, self.api_id, self.api_hash, types.CodeSettings()))\n            except errors.AuthRestartError:\n                if _retry_count > 2:\n                    raise\n                return await self.send_code_request(\n                    phone, force_sms=force_sms, _retry_count=_retry_count+1)\n\n            # TODO figure out when/if/how this can happen\n            if isinstance(result, types.auth.SentCodeSuccess):\n                raise RuntimeError('logged in right after sending the code')\n\n            # If we already sent a SMS, do not resend the code (hash may be empty)\n            if isinstance(result.type, types.auth.SentCodeTypeSms):\n                force_sms = False\n\n            # phone_code_hash may be empty, if it is, do not save it (#1283)\n            if result.phone_code_hash:\n                self._phone_code_hash[phone] = phone_hash = result.phone_code_hash\n        else:\n            force_sms = True\n\n        self._phone = phone\n\n        if force_sms:\n            try:\n                result = await self(\n                    functions.auth.ResendCodeRequest(phone, phone_hash))\n            except errors.PhoneCodeExpiredError:\n                if _retry_count > 2:\n                    raise\n                self._phone_code_hash.pop(phone, None)\n                self._log[__name__].info(\n                    \"Phone code expired in ResendCodeRequest, requesting a new code\"\n                )\n                return await self.send_code_request(\n                    phone, force_sms=False, _retry_count=_retry_count+1)\n\n            if isinstance(result, types.auth.SentCodeSuccess):\n                raise RuntimeError('logged in right after resending the code')\n\n            self._phone_code_hash[phone] = result.phone_code_hash\n\n        return result\n\n    async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin:\n        \"\"\"\n        Initiates the QR login procedure.\n\n        Note that you must be connected before invoking this, as with any\n        other request.\n\n        It is up to the caller to decide how to present the code to the user,\n        whether it's the URL, using the token bytes directly, or generating\n        a QR code and displaying it by other means.\n\n        See the documentation for `QRLogin` to see how to proceed after this.\n\n        Arguments\n            ignored_ids (List[`int`]):\n                List of already logged-in user IDs, to prevent logging in\n                twice with the same user.\n\n        Returns\n            An instance of `QRLogin`.\n\n        Example\n            .. code-block:: python\n\n                def display_url_as_qr(url):\n                    pass  # do whatever to show url as a qr to the user\n\n                qr_login = await client.qr_login()\n                display_url_as_qr(qr_login.url)\n\n                # Important! You need to wait for the login to complete!\n                await qr_login.wait()\n\n                # If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`.\n                # You should except that error and call `sign_in` with the password if this happens.\n        \"\"\"\n        qr_login = custom.QRLogin(self, ignored_ids or [])\n        await qr_login.recreate()\n        return qr_login\n\n    async def log_out(self: 'TelegramClient') -> bool:\n        \"\"\"\n        Logs out Telegram and deletes the current ``*.session`` file.\n\n        The client is unusable after logging out and a new instance should be created.\n\n        Returns\n            `True` if the operation was successful.\n\n        Example\n            .. code-block:: python\n\n                # Note: you will need to login again!\n                await client.log_out()\n        \"\"\"\n        try:\n            await self(functions.auth.LogOutRequest())\n        except errors.RPCError:\n            return False\n\n        self._mb_entity_cache.set_self_user(None, None, None)\n        self._authorized = False\n\n        await self.disconnect()\n        await utils.maybe_async(self.session.delete())\n        self.session = None\n        return True\n\n    async def edit_2fa(\n            self: 'TelegramClient',\n            current_password: str = None,\n            new_password: str = None,\n            *,\n            hint: str = '',\n            email: str = None,\n            email_code_callback: typing.Callable[[int], str] = None) -> bool:\n        \"\"\"\n        Changes the 2FA settings of the logged in user.\n\n        Review carefully the parameter explanations before using this method.\n\n        Note that this method may be *incredibly* slow depending on the\n        prime numbers that must be used during the process to make sure\n        that everything is safe.\n\n        Has no effect if both current and new password are omitted.\n\n        Arguments\n            current_password (`str`, optional):\n                The current password, to authorize changing to ``new_password``.\n                Must be set if changing existing 2FA settings.\n                Must **not** be set if 2FA is currently disabled.\n                Passing this by itself will remove 2FA (if correct).\n\n            new_password (`str`, optional):\n                The password to set as 2FA.\n                If 2FA was already enabled, ``current_password`` **must** be set.\n                Leaving this blank or `None` will remove the password.\n\n            hint (`str`, optional):\n                Hint to be displayed by Telegram when it asks for 2FA.\n                Leaving unspecified is highly discouraged.\n                Has no effect if ``new_password`` is not set.\n\n            email (`str`, optional):\n                Recovery and verification email. If present, you must also\n                set `email_code_callback`, else it raises ``ValueError``.\n\n            email_code_callback (`callable`, optional):\n                If an email is provided, a callback that returns the code sent\n                to it must also be set. This callback may be asynchronous.\n                It should return a string with the code. The length of the\n                code will be passed to the callback as an input parameter.\n\n                If the callback returns an invalid code, it will raise\n                ``CodeInvalidError``.\n\n        Returns\n            `True` if successful, `False` otherwise.\n\n        Example\n            .. code-block:: python\n\n                # Setting a password for your account which didn't have\n                await client.edit_2fa(new_password='I_<3_Telethon')\n\n                # Removing the password\n                await client.edit_2fa(current_password='I_<3_Telethon')\n        \"\"\"\n        if new_password is None and current_password is None:\n            return False\n\n        if email and not callable(email_code_callback):\n            raise ValueError('email present without email_code_callback')\n\n        pwd = await self(functions.account.GetPasswordRequest())\n        pwd.new_algo.salt1 += os.urandom(32)\n        assert isinstance(pwd, types.account.Password)\n        if not pwd.has_password and current_password:\n            current_password = None\n\n        if current_password:\n            password = pwd_mod.compute_check(pwd, current_password)\n        else:\n            password = types.InputCheckPasswordEmpty()\n\n        if new_password:\n            new_password_hash = pwd_mod.compute_digest(\n                pwd.new_algo, new_password)\n        else:\n            new_password_hash = b''\n\n        try:\n            await self(functions.account.UpdatePasswordSettingsRequest(\n                password=password,\n                new_settings=types.account.PasswordInputSettings(\n                    new_algo=pwd.new_algo,\n                    new_password_hash=new_password_hash,\n                    hint=hint,\n                    email=email,\n                    new_secure_settings=None\n                )\n            ))\n        except errors.EmailUnconfirmedError as e:\n            code = email_code_callback(e.code_length)\n            if inspect.isawaitable(code):\n                code = await code\n\n            code = str(code)\n            await self(functions.account.ConfirmPasswordEmailRequest(code))\n\n        return True\n\n    # endregion\n\n    # region with blocks\n\n    async def __aenter__(self):\n        return await self.start()\n\n    async def __aexit__(self, *args):\n        await self.disconnect()\n\n    __enter__ = helpers._sync_enter\n    __exit__ = helpers._sync_exit\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/bots.py",
    "content": "import typing\n\nfrom .. import hints\nfrom ..tl import types, functions, custom\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nclass BotMethods:\n    async def inline_query(\n            self: 'TelegramClient',\n            bot: 'hints.EntityLike',\n            query: str,\n            *,\n            entity: 'hints.EntityLike' = None,\n            offset: str = None,\n            geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:\n        \"\"\"\n        Makes an inline query to the specified bot (``@vote New Poll``).\n\n        Arguments\n            bot (`entity`):\n                The bot entity to which the inline query should be made.\n\n            query (`str`):\n                The query that should be made to the bot.\n\n            entity (`entity`, optional):\n                The entity where the inline query is being made from. Certain\n                bots use this to display different results depending on where\n                it's used, such as private chats, groups or channels.\n\n                If specified, it will also be the default entity where the\n                message will be sent after clicked. Otherwise, the \"empty\n                peer\" will be used, which some bots may not handle correctly.\n\n            offset (`str`, optional):\n                The string offset to use for the bot.\n\n            geo_point (:tl:`GeoPoint`, optional)\n                The geo point location information to send to the bot\n                for localised results. Available under some bots.\n\n        Returns\n            A list of `custom.InlineResult\n            <telethon.tl.custom.inlineresult.InlineResult>`.\n\n        Example\n            .. code-block:: python\n\n                # Make an inline query to @like\n                results = await client.inline_query('like', 'Do you like Telethon?')\n\n                # Send the first result to some chat\n                message = await results[0].click('TelethonOffTopic')\n        \"\"\"\n        bot = await self.get_input_entity(bot)\n        if entity:\n            peer = await self.get_input_entity(entity)\n        else:\n            peer = types.InputPeerEmpty()\n\n        result = await self(functions.messages.GetInlineBotResultsRequest(\n            bot=bot,\n            peer=peer,\n            query=query,\n            offset=offset or '',\n            geo_point=geo_point\n        ))\n\n        return custom.InlineResults(self, result, entity=peer if entity else None)\n"
  },
  {
    "path": "telethon/client/buttons.py",
    "content": "import typing\n\nfrom .. import utils, hints\nfrom ..tl import types, custom\n\n\nclass ButtonMethods:\n    @staticmethod\n    def build_reply_markup(\n            buttons: 'typing.Optional[hints.MarkupLike]'\n    ) -> 'typing.Optional[types.TypeReplyMarkup]':\n        \"\"\"\n        Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for\n        the given buttons.\n\n        Does nothing if either no buttons are provided or the provided\n        argument is already a reply markup.\n\n        You should consider using this method if you are going to reuse\n        the markup very often. Otherwise, it is not necessary.\n\n        This method is **not** asynchronous (don't use ``await`` on it).\n\n        Arguments\n            buttons (`hints.MarkupLike`):\n                The button, list of buttons, array of buttons or markup\n                to convert into a markup.\n\n        Example\n            .. code-block:: python\n\n                from telethon import Button\n\n                markup = client.build_reply_markup(Button.inline('hi'))\n                # later\n                await client.send_message(chat, 'click me', buttons=markup)\n        \"\"\"\n        if buttons is None:\n            return None\n\n        try:\n            if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:  # crc32(b'ReplyMarkup'):\n                return buttons\n        except AttributeError:\n            pass\n\n        if not utils.is_list_like(buttons):\n            buttons = [[buttons]]\n        elif not buttons or not utils.is_list_like(buttons[0]):\n            buttons = [buttons]\n\n        is_inline = False\n        is_normal = False\n        resize = None\n        single_use = None\n        selective = None\n        persistent = None\n        placeholder = None\n\n        rows = []\n        for row in buttons:\n            current = []\n            for button in row:\n                if isinstance(button, custom.Button):\n                    if button.resize is not None:\n                        resize = button.resize\n                    if button.single_use is not None:\n                        single_use = button.single_use\n                    if button.selective is not None:\n                        selective = button.selective\n                    if button.persistent is not None:\n                        persistent = button.persistent\n                    if button.placeholder is not None:\n                        placeholder = button.placeholder\n\n                    button = button.button\n                elif isinstance(button, custom.MessageButton):\n                    button = button.button\n\n                inline = custom.Button._is_inline(button)\n                is_inline |= inline\n                is_normal |= not inline\n\n                if button.SUBCLASS_OF_ID == 0xbad74a3:  # crc32(b'KeyboardButton')\n                    current.append(button)\n\n            if current:\n                rows.append(types.KeyboardButtonRow(current))\n\n        if is_inline and is_normal:\n            raise ValueError('You cannot mix inline with normal buttons')\n        elif is_inline:\n            return types.ReplyInlineMarkup(rows)\n        return types.ReplyKeyboardMarkup(\n            rows=rows,\n            resize=resize,\n            single_use=single_use,\n            selective=selective,\n            persistent=persistent,\n            placeholder=placeholder\n        )\n"
  },
  {
    "path": "telethon/client/chats.py",
    "content": "import asyncio\nimport inspect\nimport itertools\nimport string\nimport typing\n\nfrom .. import helpers, utils, hints, errors\nfrom ..requestiter import RequestIter\nfrom ..tl import types, functions, custom\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n_MAX_PARTICIPANTS_CHUNK_SIZE = 200\n_MAX_ADMIN_LOG_CHUNK_SIZE = 100\n_MAX_PROFILE_PHOTO_CHUNK_SIZE = 100\n\n\nclass _ChatAction:\n    _str_mapping = {\n        'typing': types.SendMessageTypingAction(),\n        'contact': types.SendMessageChooseContactAction(),\n        'game': types.SendMessageGamePlayAction(),\n        'location': types.SendMessageGeoLocationAction(),\n        'sticker': types.SendMessageChooseStickerAction(),\n\n        'record-audio': types.SendMessageRecordAudioAction(),\n        'record-voice': types.SendMessageRecordAudioAction(),  # alias\n        'record-round': types.SendMessageRecordRoundAction(),\n        'record-video': types.SendMessageRecordVideoAction(),\n\n        'audio': types.SendMessageUploadAudioAction(1),\n        'voice': types.SendMessageUploadAudioAction(1),  # alias\n        'song': types.SendMessageUploadAudioAction(1),  # alias\n        'round': types.SendMessageUploadRoundAction(1),\n        'video': types.SendMessageUploadVideoAction(1),\n\n        'photo': types.SendMessageUploadPhotoAction(1),\n        'document': types.SendMessageUploadDocumentAction(1),\n        'file': types.SendMessageUploadDocumentAction(1),  # alias\n\n        'cancel': types.SendMessageCancelAction()\n    }\n\n    def __init__(self, client, chat, action, *, delay, auto_cancel):\n        self._client = client\n        self._chat = chat\n        self._action = action\n        self._delay = delay\n        self._auto_cancel = auto_cancel\n        self._request = None\n        self._task = None\n        self._running = False\n\n    async def __aenter__(self):\n        self._chat = await self._client.get_input_entity(self._chat)\n\n        # Since `self._action` is passed by reference we can avoid\n        # recreating the request all the time and still modify\n        # `self._action.progress` directly in `progress`.\n        self._request = functions.messages.SetTypingRequest(\n            self._chat, self._action)\n\n        self._running = True\n        self._task = self._client.loop.create_task(self._update())\n        return self\n\n    async def __aexit__(self, *args):\n        self._running = False\n        if self._task:\n            self._task.cancel()\n            try:\n                await self._task\n            except asyncio.CancelledError:\n                pass\n\n            self._task = None\n\n    __enter__ = helpers._sync_enter\n    __exit__ = helpers._sync_exit\n\n    async def _update(self):\n        try:\n            while self._running:\n                await self._client(self._request)\n                await asyncio.sleep(self._delay)\n        except ConnectionError:\n            pass\n        except asyncio.CancelledError:\n            if self._auto_cancel:\n                await self._client(functions.messages.SetTypingRequest(\n                    self._chat, types.SendMessageCancelAction()))\n\n    def progress(self, current, total):\n        if hasattr(self._action, 'progress'):\n            self._action.progress = 100 * round(current / total)\n\n\nclass _ParticipantsIter(RequestIter):\n    async def _init(self, entity, filter, search):\n        if isinstance(filter, type):\n            if filter in (types.ChannelParticipantsBanned,\n                          types.ChannelParticipantsKicked,\n                          types.ChannelParticipantsSearch,\n                          types.ChannelParticipantsContacts):\n                # These require a `q` parameter (support types for convenience)\n                filter = filter('')\n            else:\n                filter = filter()\n\n        entity = await self.client.get_input_entity(entity)\n        ty = helpers._entity_type(entity)\n        if search and (filter or ty != helpers._EntityType.CHANNEL):\n            # We need to 'search' ourselves unless we have a PeerChannel\n            search = search.casefold()\n\n            self.filter_entity = lambda ent: (\n                search in utils.get_display_name(ent).casefold() or\n                search in (getattr(ent, 'username', None) or '').casefold()\n            )\n        else:\n            self.filter_entity = lambda ent: True\n\n        # Only used for channels, but we should always set the attribute\n        # Called `requests` even though it's just one for legacy purposes.\n        self.requests = None\n\n        if ty == helpers._EntityType.CHANNEL:\n            if self.limit <= 0:\n                # May not have access to the channel, but getFull can get the .total.\n                self.total = (await self.client(\n                    functions.channels.GetFullChannelRequest(entity)\n                )).full_chat.participants_count\n                raise StopAsyncIteration\n\n            self.seen = set()\n            self.requests = functions.channels.GetParticipantsRequest(\n                channel=entity,\n                filter=filter or types.ChannelParticipantsSearch(search),\n                offset=0,\n                limit=_MAX_PARTICIPANTS_CHUNK_SIZE,\n                hash=0\n            )\n\n        elif ty == helpers._EntityType.CHAT:\n            full = await self.client(\n                functions.messages.GetFullChatRequest(entity.chat_id))\n            if not isinstance(\n                    full.full_chat.participants, types.ChatParticipants):\n                # ChatParticipantsForbidden won't have ``.participants``\n                self.total = 0\n                raise StopAsyncIteration\n\n            self.total = len(full.full_chat.participants.participants)\n\n            users = {user.id: user for user in full.users}\n            for participant in full.full_chat.participants.participants:\n                if isinstance(participant, types.ChannelParticipantLeft):\n                    # See issue #3231 to learn why this is ignored.\n                    continue\n                elif isinstance(participant, types.ChannelParticipantBanned):\n                    user_id = participant.peer.user_id\n                else:\n                    user_id = participant.user_id\n                user = users[user_id]\n                if not self.filter_entity(user):\n                    continue\n\n                user = users[user_id]\n                user.participant = participant\n                self.buffer.append(user)\n\n            return True\n        else:\n            self.total = 1\n            if self.limit != 0:\n                user = await self.client.get_entity(entity)\n                if self.filter_entity(user):\n                    user.participant = None\n                    self.buffer.append(user)\n\n            return True\n\n    async def _load_next_chunk(self):\n        if not self.requests:\n            return True\n\n        self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE)\n\n        if self.requests.offset > self.limit:\n            return True\n\n        if self.total is None:\n            f = self.requests.filter\n            if (\n                not isinstance(f, types.ChannelParticipantsRecent)\n                and (not isinstance(f, types.ChannelParticipantsSearch) or f.q)\n            ):\n                # Only do an additional getParticipants here to get the total\n                # if there's a filter which would reduce the real total number.\n                # getParticipants is cheaper than getFull.\n                self.total = (await self.client(functions.channels.GetParticipantsRequest(\n                    channel=self.requests.channel,\n                    filter=types.ChannelParticipantsRecent(),\n                    offset=0,\n                    limit=1,\n                    hash=0\n                ))).count\n\n        participants = await self.client(self.requests)\n        if self.total is None:\n            # Will only get here if there was one request with a filter that matched all users.\n            self.total = participants.count\n        if not participants.users:\n            self.requests = None\n            return\n\n        self.requests.offset += len(participants.participants)\n        users = {user.id: user for user in participants.users}\n        for participant in participants.participants:\n            if isinstance(participant, types.ChannelParticipantLeft):\n                # See issue #3231 to learn why this is ignored.\n                continue\n            elif isinstance(participant, types.ChannelParticipantBanned):\n                if not isinstance(participant.peer, types.PeerUser):\n                    # May have the entire channel banned. See #3105.\n                    continue\n                user_id = participant.peer.user_id\n            else:\n                user_id = participant.user_id\n\n            user = users[user_id]\n            if not self.filter_entity(user) or user.id in self.seen:\n                continue\n            self.seen.add(user_id)\n            user = users[user_id]\n            user.participant = participant\n            self.buffer.append(user)\n\n\nclass _AdminLogIter(RequestIter):\n    async def _init(\n            self, entity, admins, search, min_id, max_id,\n            join, leave, invite, restrict, unrestrict, ban, unban,\n            promote, demote, info, settings, pinned, edit, delete,\n            group_call\n    ):\n        if any((join, leave, invite, restrict, unrestrict, ban, unban,\n                promote, demote, info, settings, pinned, edit, delete,\n                group_call)):\n            events_filter = types.ChannelAdminLogEventsFilter(\n                join=join, leave=leave, invite=invite, ban=restrict,\n                unban=unrestrict, kick=ban, unkick=unban, promote=promote,\n                demote=demote, info=info, settings=settings, pinned=pinned,\n                edit=edit, delete=delete, group_call=group_call\n            )\n        else:\n            events_filter = None\n\n        self.entity = await self.client.get_input_entity(entity)\n\n        admin_list = []\n        if admins:\n            if not utils.is_list_like(admins):\n                admins = (admins,)\n\n            for admin in admins:\n                admin_list.append(await self.client.get_input_entity(admin))\n\n        self.request = functions.channels.GetAdminLogRequest(\n            self.entity, q=search or '', min_id=min_id, max_id=max_id,\n            limit=0, events_filter=events_filter, admins=admin_list or None\n        )\n\n    async def _load_next_chunk(self):\n        self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE)\n        r = await self.client(self.request)\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(r.users, r.chats)}\n\n        self.request.max_id = min((e.id for e in r.events), default=0)\n        for ev in r.events:\n            if isinstance(ev.action,\n                          types.ChannelAdminLogEventActionEditMessage):\n                ev.action.prev_message._finish_init(\n                    self.client, entities, self.entity)\n\n                ev.action.new_message._finish_init(\n                    self.client, entities, self.entity)\n\n            elif isinstance(ev.action,\n                            types.ChannelAdminLogEventActionDeleteMessage):\n                ev.action.message._finish_init(\n                    self.client, entities, self.entity)\n\n            self.buffer.append(custom.AdminLogEvent(ev, entities))\n\n        if len(r.events) < self.request.limit:\n            return True\n\n\nclass _ProfilePhotoIter(RequestIter):\n    async def _init(\n            self, entity, offset, max_id\n    ):\n        entity = await self.client.get_input_entity(entity)\n        ty = helpers._entity_type(entity)\n        if ty == helpers._EntityType.USER:\n            self.request = functions.photos.GetUserPhotosRequest(\n                entity,\n                offset=offset,\n                max_id=max_id,\n                limit=1\n            )\n        else:\n            self.request = functions.messages.SearchRequest(\n                peer=entity,\n                q='',\n                filter=types.InputMessagesFilterChatPhotos(),\n                min_date=None,\n                max_date=None,\n                offset_id=0,\n                add_offset=offset,\n                limit=1,\n                max_id=max_id,\n                min_id=0,\n                hash=0\n            )\n\n        if self.limit == 0:\n            self.request.limit = 1\n            result = await self.client(self.request)\n            if isinstance(result, types.photos.Photos):\n                self.total = len(result.photos)\n            elif isinstance(result, types.messages.Messages):\n                self.total = len(result.messages)\n            else:\n                # Luckily both photosSlice and messages have a count for total\n                self.total = getattr(result, 'count', None)\n\n    async def _load_next_chunk(self):\n        self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE)\n        result = await self.client(self.request)\n\n        if isinstance(result, types.photos.Photos):\n            self.buffer = result.photos\n            self.left = len(self.buffer)\n            self.total = len(self.buffer)\n        elif isinstance(result, types.messages.Messages):\n            self.buffer = [x.action.photo for x in result.messages\n                           if isinstance(x.action, types.MessageActionChatEditPhoto)]\n\n            self.left = len(self.buffer)\n            self.total = len(self.buffer)\n        elif isinstance(result, types.photos.PhotosSlice):\n            self.buffer = result.photos\n            self.total = result.count\n            if len(self.buffer) < self.request.limit:\n                self.left = len(self.buffer)\n            else:\n                self.request.offset += len(result.photos)\n        else:\n            # Some broadcast channels have a photo that this request doesn't\n            # retrieve for whatever random reason the Telegram server feels.\n            #\n            # This means the `total` count may be wrong but there's not much\n            # that can be done around it (perhaps there are too many photos\n            # and this is only a partial result so it's not possible to just\n            # use the len of the result).\n            self.total = getattr(result, 'count', None)\n\n            # Unconditionally fetch the full channel to obtain this photo and\n            # yield it with the rest (unless it's a duplicate).\n            seen_id = None\n            if isinstance(result, types.messages.ChannelMessages):\n                channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer))\n                photo = channel.full_chat.chat_photo\n                if isinstance(photo, types.Photo):\n                    self.buffer.append(photo)\n                    seen_id = photo.id\n\n            self.buffer.extend(\n                x.action.photo for x in result.messages\n                if isinstance(x.action, types.MessageActionChatEditPhoto)\n                and x.action.photo.id != seen_id\n            )\n\n            if len(result.messages) < self.request.limit:\n                self.left = len(self.buffer)\n            elif result.messages:\n                self.request.add_offset = 0\n                self.request.offset_id = result.messages[-1].id\n\n\nclass ChatMethods:\n\n    # region Public methods\n\n    def iter_participants(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            limit: float = None,\n            *,\n            search: str = '',\n            filter: 'types.TypeChannelParticipantsFilter' = None,\n            aggressive: bool = False) -> _ParticipantsIter:\n        \"\"\"\n        Iterator over the participants belonging to the specified chat.\n\n        The order is unspecified.\n\n        Arguments\n            entity (`entity`):\n                The entity from which to retrieve the participants list.\n\n            limit (`int`):\n                Limits amount of participants fetched.\n\n            search (`str`, optional):\n                Look for participants with this string in name/username.\n\n            filter (:tl:`ChannelParticipantsFilter`, optional):\n                The filter to be used, if you want e.g. only admins\n                Note that you might not have permissions for some filter.\n                This has no effect for normal chats or users.\n\n                .. note::\n\n                    The filter :tl:`ChannelParticipantsBanned` will return\n                    *restricted* users. If you want *banned* users you should\n                    use :tl:`ChannelParticipantsKicked` instead.\n\n            aggressive (`bool`, optional):\n                Does nothing. This is kept for backwards-compatibility.\n\n                There have been several changes to Telegram's API that limits\n                the amount of members that can be retrieved, and this was a\n                hack that no longer works.\n\n        Yields\n            The :tl:`User` objects returned by :tl:`GetParticipantsRequest`\n            with an additional ``.participant`` attribute which is the\n            matched :tl:`ChannelParticipant` type for channels/megagroups\n            or :tl:`ChatParticipants` for normal chats.\n\n        Example\n            .. code-block:: python\n\n                # Show all user IDs in a chat\n                async for user in client.iter_participants(chat):\n                    print(user.id)\n\n                # Search by name\n                async for user in client.iter_participants(chat, search='name'):\n                    print(user.username)\n\n                # Filter by admins\n                from telethon.tl.types import ChannelParticipantsAdmins\n                async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins):\n                    print(user.first_name)\n        \"\"\"\n        return _ParticipantsIter(\n            self,\n            limit,\n            entity=entity,\n            filter=filter,\n            search=search\n        )\n\n    async def get_participants(\n            self: 'TelegramClient',\n            *args,\n            **kwargs) -> 'hints.TotalList':\n        \"\"\"\n        Same as `iter_participants()`, but returns a\n        `TotalList <telethon.helpers.TotalList>` instead.\n\n        Example\n            .. code-block:: python\n\n                users = await client.get_participants(chat)\n                print(users[0].first_name)\n\n                for user in users:\n                    if user.username is not None:\n                        print(user.username)\n        \"\"\"\n        return await self.iter_participants(*args, **kwargs).collect()\n\n    get_participants.__signature__ = inspect.signature(iter_participants)\n\n\n    def iter_admin_log(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            limit: float = None,\n            *,\n            max_id: int = 0,\n            min_id: int = 0,\n            search: str = None,\n            admins: 'hints.EntitiesLike' = None,\n            join: bool = None,\n            leave: bool = None,\n            invite: bool = None,\n            restrict: bool = None,\n            unrestrict: bool = None,\n            ban: bool = None,\n            unban: bool = None,\n            promote: bool = None,\n            demote: bool = None,\n            info: bool = None,\n            settings: bool = None,\n            pinned: bool = None,\n            edit: bool = None,\n            delete: bool = None,\n            group_call: bool = None) -> _AdminLogIter:\n        \"\"\"\n        Iterator over the admin log for the specified channel.\n\n        The default order is from the most recent event to to the oldest.\n\n        Note that you must be an administrator of it to use this method.\n\n        If none of the filters are present (i.e. they all are `None`),\n        *all* event types will be returned. If at least one of them is\n        `True`, only those that are true will be returned.\n\n        Arguments\n            entity (`entity`):\n                The channel entity from which to get its admin log.\n\n            limit (`int` | `None`, optional):\n                Number of events to be retrieved.\n\n                The limit may also be `None`, which would eventually return\n                the whole history.\n\n            max_id (`int`):\n                All the events with a higher (newer) ID or equal to this will\n                be excluded.\n\n            min_id (`int`):\n                All the events with a lower (older) ID or equal to this will\n                be excluded.\n\n            search (`str`):\n                The string to be used as a search query.\n\n            admins (`entity` | `list`):\n                If present, the events will be filtered by these admins\n                (or single admin) and only those caused by them will be\n                returned.\n\n            join (`bool`):\n                If `True`, events for when a user joined will be returned.\n\n            leave (`bool`):\n                If `True`, events for when a user leaves will be returned.\n\n            invite (`bool`):\n                If `True`, events for when a user joins through an invite\n                link will be returned.\n\n            restrict (`bool`):\n                If `True`, events with partial restrictions will be\n                returned. This is what the API calls \"ban\".\n\n            unrestrict (`bool`):\n                If `True`, events removing restrictions will be returned.\n                This is what the API calls \"unban\".\n\n            ban (`bool`):\n                If `True`, events applying or removing all restrictions will\n                be returned. This is what the API calls \"kick\" (restricting\n                all permissions removed is a ban, which kicks the user).\n\n            unban (`bool`):\n                If `True`, events removing all restrictions will be\n                returned. This is what the API calls \"unkick\".\n\n            promote (`bool`):\n                If `True`, events with admin promotions will be returned.\n\n            demote (`bool`):\n                If `True`, events with admin demotions will be returned.\n\n            info (`bool`):\n                If `True`, events changing the group info will be returned.\n\n            settings (`bool`):\n                If `True`, events changing the group settings will be\n                returned.\n\n            pinned (`bool`):\n                If `True`, events of new pinned messages will be returned.\n\n            edit (`bool`):\n                If `True`, events of message edits will be returned.\n\n            delete (`bool`):\n                If `True`, events of message deletions will be returned.\n\n            group_call (`bool`):\n                If `True`, events related to group calls will be returned.\n\n        Yields\n            Instances of `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>`.\n\n        Example\n            .. code-block:: python\n\n                async for event in client.iter_admin_log(channel):\n                    if event.changed_title:\n                        print('The title changed from', event.old, 'to', event.new)\n        \"\"\"\n        return _AdminLogIter(\n            self,\n            limit,\n            entity=entity,\n            admins=admins,\n            search=search,\n            min_id=min_id,\n            max_id=max_id,\n            join=join,\n            leave=leave,\n            invite=invite,\n            restrict=restrict,\n            unrestrict=unrestrict,\n            ban=ban,\n            unban=unban,\n            promote=promote,\n            demote=demote,\n            info=info,\n            settings=settings,\n            pinned=pinned,\n            edit=edit,\n            delete=delete,\n            group_call=group_call\n        )\n\n    async def get_admin_log(\n            self: 'TelegramClient',\n            *args,\n            **kwargs) -> 'hints.TotalList':\n        \"\"\"\n        Same as `iter_admin_log()`, but returns a ``list`` instead.\n\n        Example\n            .. code-block:: python\n\n                # Get a list of deleted message events which said \"heck\"\n                events = await client.get_admin_log(channel, search='heck', delete=True)\n\n                # Print the old message before it was deleted\n                print(events[0].old)\n        \"\"\"\n        return await self.iter_admin_log(*args, **kwargs).collect()\n\n    get_admin_log.__signature__ = inspect.signature(iter_admin_log)\n\n    def iter_profile_photos(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            limit: int = None,\n            *,\n            offset: int = 0,\n            max_id: int = 0) -> _ProfilePhotoIter:\n        \"\"\"\n        Iterator over a user's profile photos or a chat's photos.\n\n        The order is from the most recent photo to the oldest.\n\n        Arguments\n            entity (`entity`):\n                The entity from which to get the profile or chat photos.\n\n            limit (`int` | `None`, optional):\n                Number of photos to be retrieved.\n\n                The limit may also be `None`, which would eventually all\n                the photos that are still available.\n\n            offset (`int`):\n                How many photos should be skipped before returning the first one.\n\n            max_id (`int`):\n                The maximum ID allowed when fetching photos.\n\n        Yields\n            Instances of :tl:`Photo`.\n\n        Example\n            .. code-block:: python\n\n                # Download all the profile photos of some user\n                async for photo in client.iter_profile_photos(user):\n                    await client.download_media(photo)\n        \"\"\"\n        return _ProfilePhotoIter(\n            self,\n            limit,\n            entity=entity,\n            offset=offset,\n            max_id=max_id\n        )\n\n    async def get_profile_photos(\n            self: 'TelegramClient',\n            *args,\n            **kwargs) -> 'hints.TotalList':\n        \"\"\"\n        Same as `iter_profile_photos()`, but returns a\n        `TotalList <telethon.helpers.TotalList>` instead.\n\n        Example\n            .. code-block:: python\n\n                # Get the photos of a channel\n                photos = await client.get_profile_photos(channel)\n\n                # Download the oldest photo\n                await client.download_media(photos[-1])\n        \"\"\"\n        return await self.iter_profile_photos(*args, **kwargs).collect()\n\n    get_profile_photos.__signature__ = inspect.signature(iter_profile_photos)\n\n    def action(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            action: 'typing.Union[str, types.TypeSendMessageAction]',\n            *,\n            delay: float = 4,\n            auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]':\n        \"\"\"\n        Returns a context-manager object to represent a \"chat action\".\n\n        Chat actions indicate things like \"user is typing\", \"user is\n        uploading a photo\", etc.\n\n        If the action is ``'cancel'``, you should just ``await`` the result,\n        since it makes no sense to use a context-manager for it.\n\n        See the example below for intended usage.\n\n        Arguments\n            entity (`entity`):\n                The entity where the action should be showed in.\n\n            action (`str` | :tl:`SendMessageAction`):\n                The action to show. You can either pass a instance of\n                :tl:`SendMessageAction` or better, a string used while:\n\n                * ``'typing'``: typing a text message.\n                * ``'contact'``: choosing a contact.\n                * ``'game'``: playing a game.\n                * ``'location'``: choosing a geo location.\n                * ``'sticker'``: choosing a sticker.\n                * ``'record-audio'``: recording a voice note.\n                  You may use ``'record-voice'`` as alias.\n                * ``'record-round'``: recording a round video.\n                * ``'record-video'``: recording a normal video.\n                * ``'audio'``: sending an audio file (voice note or song).\n                  You may use ``'voice'`` and ``'song'`` as aliases.\n                * ``'round'``: uploading a round video.\n                * ``'video'``: uploading a video file.\n                * ``'photo'``: uploading a photo.\n                * ``'document'``: uploading a document file.\n                  You may use ``'file'`` as alias.\n                * ``'cancel'``: cancel any pending action in this chat.\n\n                Invalid strings will raise a ``ValueError``.\n\n            delay (`int` | `float`):\n                The delay, in seconds, to wait between sending actions.\n                For example, if the delay is 5 and it takes 7 seconds to\n                do something, three requests will be made at 0s, 5s, and\n                7s to cancel the action.\n\n            auto_cancel (`bool`):\n                Whether the action should be cancelled once the context\n                manager exists or not. The default is `True`, since\n                you don't want progress to be shown when it has already\n                completed.\n\n        Returns\n            Either a context-manager object or a coroutine.\n\n        Example\n            .. code-block:: python\n\n                # Type for 2 seconds, then send a message\n                async with client.action(chat, 'typing'):\n                    await asyncio.sleep(2)\n                    await client.send_message(chat, 'Hello world! I type slow ^^')\n\n                # Cancel any previous action\n                await client.action(chat, 'cancel')\n\n                # Upload a document, showing its progress (most clients ignore this)\n                async with client.action(chat, 'document') as action:\n                    await client.send_file(chat, zip_file, progress_callback=action.progress)\n        \"\"\"\n        if isinstance(action, str):\n            try:\n                action = _ChatAction._str_mapping[action.lower()]\n            except KeyError:\n                raise ValueError(\n                    'No such action \"{}\"'.format(action)) from None\n        elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21:\n            # 0x20b2cc21 = crc32(b'SendMessageAction')\n            if isinstance(action, type):\n                raise ValueError('You must pass an instance, not the class')\n            else:\n                raise ValueError('Cannot use {} as action'.format(action))\n\n        if isinstance(action, types.SendMessageCancelAction):\n            # ``SetTypingRequest.resolve`` will get input peer of ``entity``.\n            return self(functions.messages.SetTypingRequest(\n                entity, types.SendMessageCancelAction()))\n\n        return _ChatAction(\n            self, entity, action, delay=delay, auto_cancel=auto_cancel)\n\n    async def edit_admin(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            user: 'hints.EntityLike',\n            *,\n            change_info: bool = None,\n            post_messages: bool = None,\n            edit_messages: bool = None,\n            delete_messages: bool = None,\n            ban_users: bool = None,\n            invite_users: bool = None,\n            pin_messages: bool = None,\n            add_admins: bool = None,\n            manage_call: bool = None,\n            anonymous: bool = None,\n            is_admin: bool = None,\n            title: str = None) -> types.Updates:\n        \"\"\"\n        Edits admin permissions for someone in a chat.\n\n        Raises an error if a wrong combination of rights are given\n        (e.g. you don't have enough permissions to grant one).\n\n        Unless otherwise stated, permissions will work in channels and megagroups.\n\n        Arguments\n            entity (`entity`):\n                The channel, megagroup or chat where the promotion should happen.\n\n            user (`entity`):\n                The user to be promoted.\n\n            change_info (`bool`, optional):\n                Whether the user will be able to change info.\n\n            post_messages (`bool`, optional):\n                Whether the user will be able to post in the channel.\n                This will only work in broadcast channels.\n\n            edit_messages (`bool`, optional):\n                Whether the user will be able to edit messages in the channel.\n                This will only work in broadcast channels.\n\n            delete_messages (`bool`, optional):\n                Whether the user will be able to delete messages.\n\n            ban_users (`bool`, optional):\n                Whether the user will be able to ban users.\n\n            invite_users (`bool`, optional):\n                Whether the user will be able to invite users. Needs some testing.\n\n            pin_messages (`bool`, optional):\n                Whether the user will be able to pin messages.\n\n            add_admins (`bool`, optional):\n                Whether the user will be able to add admins.\n\n            manage_call (`bool`, optional):\n                Whether the user will be able to manage group calls.\n\n            anonymous (`bool`, optional):\n                Whether the user will remain anonymous when sending messages.\n                The sender of the anonymous messages becomes the group itself.\n\n                .. note::\n\n                    Users may be able to identify the anonymous admin by its\n                    custom title, so additional care is needed when using both\n                    ``anonymous`` and custom titles. For example, if multiple\n                    anonymous admins share the same title, users won't be able\n                    to distinguish them.\n\n            is_admin (`bool`, optional):\n                Whether the user will be an admin in the chat.\n                This will only work in small group chats.\n                Whether the user will be an admin in the chat. This is the\n                only permission available in small group chats, and when\n                used in megagroups, all non-explicitly set permissions will\n                have this value.\n\n                Essentially, only passing ``is_admin=True`` will grant all\n                permissions, but you can still disable those you need.\n\n            title (`str`, optional):\n                The custom title (also known as \"rank\") to show for this admin.\n                This text will be shown instead of the \"admin\" badge.\n                This will only work in channels and megagroups.\n\n                When left unspecified or empty, the default localized \"admin\"\n                badge will be shown.\n\n        Returns\n            The resulting :tl:`Updates` object.\n\n        Example\n            .. code-block:: python\n\n                # Allowing `user` to pin messages in `chat`\n                await client.edit_admin(chat, user, pin_messages=True)\n\n                # Granting all permissions except for `add_admins`\n                await client.edit_admin(chat, user, is_admin=True, add_admins=False)\n        \"\"\"\n        entity = await self.get_input_entity(entity)\n        user = await self.get_input_entity(user)\n\n        perm_names = (\n            'change_info', 'post_messages', 'edit_messages', 'delete_messages',\n            'ban_users', 'invite_users', 'pin_messages', 'add_admins',\n            'anonymous', 'manage_call',\n        )\n\n        ty = helpers._entity_type(entity)\n        if ty == helpers._EntityType.CHANNEL:\n            # If we try to set these permissions in a megagroup, we\n            # would get a RIGHT_FORBIDDEN. However, it makes sense\n            # that an admin can post messages, so we want to avoid the error\n            if post_messages or edit_messages:\n                # TODO get rid of this once sessions cache this information\n                if entity.channel_id not in self._megagroup_cache:\n                    full_entity = await self.get_entity(entity)\n                    self._megagroup_cache[entity.channel_id] = full_entity.megagroup\n\n                if self._megagroup_cache[entity.channel_id]:\n                    post_messages = None\n                    edit_messages = None\n\n            perms = locals()\n            return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{\n                # A permission is its explicit (not-None) value or `is_admin`.\n                # This essentially makes `is_admin` be the default value.\n                name: perms[name] if perms[name] is not None else is_admin\n                for name in perm_names\n            }), rank=title or ''))\n\n        elif ty == helpers._EntityType.CHAT:\n            # If the user passed any permission in a small\n            # group chat, they must be a full admin to have it.\n            if is_admin is None:\n                is_admin = any(locals()[x] for x in perm_names)\n\n            return await self(functions.messages.EditChatAdminRequest(\n                entity.chat_id, user, is_admin=is_admin))\n\n        else:\n            raise ValueError(\n                'You can only edit permissions in groups and channels')\n\n    async def edit_permissions(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            user: 'typing.Optional[hints.EntityLike]' = None,\n            until_date: 'hints.DateLike' = None,\n            *,\n            view_messages: bool = True,\n            send_messages: bool = True,\n            send_media: bool = True,\n            send_stickers: bool = True,\n            send_gifs: bool = True,\n            send_games: bool = True,\n            send_inline: bool = True,\n            embed_link_previews: bool = True,\n            send_polls: bool = True,\n            change_info: bool = True,\n            invite_users: bool = True,\n            pin_messages: bool = True) -> types.Updates:\n        \"\"\"\n        Edits user restrictions in a chat.\n\n        Set an argument to `False` to apply a restriction (i.e. remove\n        the permission), or omit them to use the default `True` (i.e.\n        don't apply a restriction).\n\n        Raises an error if a wrong combination of rights are given\n        (e.g. you don't have enough permissions to revoke one).\n\n        By default, each boolean argument is `True`, meaning that it\n        is true that the user has access to the default permission\n        and may be able to make use of it.\n\n        If you set an argument to `False`, then a restriction is applied\n        regardless of the default permissions.\n\n        It is important to note that `True` does *not* mean grant, only\n        \"don't restrict\", and this is where the default permissions come\n        in. A user may have not been revoked the ``pin_messages`` permission\n        (it is `True`) but they won't be able to use it if the default\n        permissions don't allow it either.\n\n        Arguments\n            entity (`entity`):\n                The channel or megagroup where the restriction should happen.\n\n            user (`entity`, optional):\n                If specified, the permission will be changed for the specific user.\n                If left as `None`, the default chat permissions will be updated.\n\n            until_date (`DateLike`, optional):\n                When the user will be unbanned.\n\n                If the due date or duration is longer than 366 days or shorter than\n                30 seconds, the ban will be forever. Defaults to ``0`` (ban forever).\n\n            view_messages (`bool`, optional):\n                Whether the user is able to view messages or not.\n                Forbidding someone from viewing messages equals to banning them.\n                This will only work if ``user`` is set.\n\n            send_messages (`bool`, optional):\n                Whether the user is able to send messages or not.\n\n            send_media (`bool`, optional):\n                Whether the user is able to send media or not.\n\n            send_stickers (`bool`, optional):\n                Whether the user is able to send stickers or not.\n\n            send_gifs (`bool`, optional):\n                Whether the user is able to send animated gifs or not.\n\n            send_games (`bool`, optional):\n                Whether the user is able to send games or not.\n\n            send_inline (`bool`, optional):\n                Whether the user is able to use inline bots or not.\n\n            embed_link_previews (`bool`, optional):\n                Whether the user is able to enable the link preview in the\n                messages they send. Note that the user will still be able to\n                send messages with links if this permission is removed, but\n                these links won't display a link preview.\n\n            send_polls (`bool`, optional):\n                Whether the user is able to send polls or not.\n\n            change_info (`bool`, optional):\n                Whether the user is able to change info or not.\n\n            invite_users (`bool`, optional):\n                Whether the user is able to invite other users or not.\n\n            pin_messages (`bool`, optional):\n                Whether the user is able to pin messages or not.\n\n        Returns\n            The resulting :tl:`Updates` object.\n\n        Example\n            .. code-block:: python\n\n                from datetime import timedelta\n\n                # Banning `user` from `chat` for 1 minute\n                await client.edit_permissions(chat, user, timedelta(minutes=1),\n                                              view_messages=False)\n\n                # Banning `user` from `chat` forever\n                await client.edit_permissions(chat, user, view_messages=False)\n\n                # Kicking someone (ban + un-ban)\n                await client.edit_permissions(chat, user, view_messages=False)\n                await client.edit_permissions(chat, user)\n        \"\"\"\n        entity = await self.get_input_entity(entity)\n        ty = helpers._entity_type(entity)\n        if ty != helpers._EntityType.CHANNEL:\n            raise ValueError('You must pass either a channel or a supergroup')\n\n        rights = types.ChatBannedRights(\n            until_date=until_date,\n            view_messages=not view_messages,\n            send_messages=not send_messages,\n            send_media=not send_media,\n            send_stickers=not send_stickers,\n            send_gifs=not send_gifs,\n            send_games=not send_games,\n            send_inline=not send_inline,\n            embed_links=not embed_link_previews,\n            send_polls=not send_polls,\n            change_info=not change_info,\n            invite_users=not invite_users,\n            pin_messages=not pin_messages\n        )\n\n        if user is None:\n            return await self(functions.messages.EditChatDefaultBannedRightsRequest(\n                peer=entity,\n                banned_rights=rights\n            ))\n\n        user = await self.get_input_entity(user)\n\n        return await self(functions.channels.EditBannedRequest(\n            channel=entity,\n            participant=user,\n            banned_rights=rights\n        ))\n\n    async def kick_participant(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            user: 'typing.Optional[hints.EntityLike]'\n    ):\n        \"\"\"\n        Kicks a user from a chat.\n\n        Kicking yourself (``'me'``) will result in leaving the chat.\n\n        .. note::\n\n            Attempting to kick someone who was banned will remove their\n            restrictions (and thus unbanning them), since kicking is just\n            ban + unban.\n\n        Arguments\n            entity (`entity`):\n                The channel or chat where the user should be kicked from.\n\n            user (`entity`, optional):\n                The user to kick.\n\n        Returns\n            Returns the service `Message <telethon.tl.custom.message.Message>`\n            produced about a user being kicked, if any.\n\n        Example\n            .. code-block:: python\n\n                # Kick some user from some chat, and deleting the service message\n                msg = await client.kick_participant(chat, user)\n                await msg.delete()\n\n                # Leaving chat\n                await client.kick_participant(chat, 'me')\n        \"\"\"\n        entity = await self.get_input_entity(entity)\n        user = await self.get_input_entity(user)\n\n        ty = helpers._entity_type(entity)\n        if ty == helpers._EntityType.CHAT:\n            resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user))\n        elif ty == helpers._EntityType.CHANNEL:\n            if isinstance(user, types.InputPeerSelf):\n                # Despite no longer being in the channel, the account still\n                # seems to get the service message.\n                resp = await self(functions.channels.LeaveChannelRequest(entity))\n            else:\n                resp = await self(functions.channels.EditBannedRequest(\n                    channel=entity,\n                    participant=user,\n                    banned_rights=types.ChatBannedRights(\n                        until_date=None, view_messages=True)\n                ))\n                await asyncio.sleep(0.5)\n                await self(functions.channels.EditBannedRequest(\n                    channel=entity,\n                    participant=user,\n                    banned_rights=types.ChatBannedRights(until_date=None)\n                ))\n        else:\n            raise ValueError('You must pass either a channel or a chat')\n\n        return self._get_response_message(None, resp, entity)\n\n    async def get_permissions(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            user: 'hints.EntityLike' = None\n    ) -> 'typing.Optional[custom.ParticipantPermissions]':\n        \"\"\"\n        Fetches the permissions of a user in a specific chat or channel or\n        get Default Restricted Rights of Chat or Channel.\n\n        .. note::\n\n            This request has to fetch the entire chat for small group chats,\n            which can get somewhat expensive, so use of a cache is advised.\n\n        Arguments\n            entity (`entity`):\n                The channel or chat the user is participant of.\n\n            user (`entity`, optional):\n                Target user.\n\n        Returns\n            A `ParticipantPermissions <telethon.tl.custom.participantpermissions.ParticipantPermissions>`\n            instance. Refer to its documentation to see what properties are\n            available.\n\n        Example\n            .. code-block:: python\n\n                permissions = await client.get_permissions(chat, user)\n                if permissions.is_admin:\n                    # do something\n\n                # Get Banned Permissions of Chat\n                await client.get_permissions(chat)\n        \"\"\"\n        entity = await self.get_entity(entity)\n\n        if not user:\n            if isinstance(entity, types.Channel):\n                FullChat = await self(functions.channels.GetFullChannelRequest(entity))\n            elif isinstance(entity, types.Chat):\n                FullChat = await self(functions.messages.GetFullChatRequest(entity.id))\n            else:\n                return\n            return FullChat.chats[0].default_banned_rights\n\n        entity = await self.get_input_entity(entity)\n        user = await self.get_input_entity(user)\n        if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:\n            participant = await self(functions.channels.GetParticipantRequest(\n                entity,\n                user\n            ))\n            return custom.ParticipantPermissions(participant.participant, False)\n        elif helpers._entity_type(entity) == helpers._EntityType.CHAT:\n            chat = await self(functions.messages.GetFullChatRequest(\n                entity.chat_id\n            ))\n            if isinstance(user, types.InputPeerSelf):\n                user = await self.get_me(input_peer=True)\n            for participant in chat.full_chat.participants.participants:\n                if participant.user_id == user.user_id:\n                    return custom.ParticipantPermissions(participant, True)\n            raise errors.UserNotParticipantError(None)\n\n        raise ValueError('You must pass either a channel or a chat')\n\n    async def get_stats(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'typing.Union[int, types.Message]' = None,\n    ):\n        \"\"\"\n        Retrieves statistics from the given megagroup or broadcast channel.\n\n        Note that some restrictions apply before being able to fetch statistics,\n        in particular the channel must have enough members (for megagroups, this\n        requires `at least 500 members`_).\n\n        Arguments\n            entity (`entity`):\n                The channel from which to get statistics.\n\n            message (`int` | ``Message``, optional):\n                The message ID from which to get statistics, if your goal is\n                to obtain the statistics of a single message.\n\n        Raises\n            If the given entity is not a channel (broadcast or megagroup),\n            a `TypeError` is raised.\n\n            If there are not enough members (poorly named) errors such as\n            ``telethon.errors.ChatAdminRequiredError`` will appear.\n\n        Returns\n            If both ``entity`` and ``message`` were provided, returns\n            :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or\n            :tl:`MegagroupStats`, depending on whether the input belonged to a\n            broadcast channel or megagroup.\n\n        Example\n            .. code-block:: python\n\n                # Some megagroup or channel username or ID to fetch\n                channel = -100123\n                stats = await client.get_stats(channel)\n                print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':')\n                print(stats.stringify())\n\n        .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more\n        \"\"\"\n        entity = await self.get_input_entity(entity)\n        if helpers._entity_type(entity) != helpers._EntityType.CHANNEL:\n            raise TypeError('You must pass a channel entity')\n\n        message = utils.get_message_id(message)\n        if message is not None:\n            try:\n                req = functions.stats.GetMessageStatsRequest(entity, message)\n                return await self(req)\n            except errors.StatsMigrateError as e:\n                dc = e.dc\n        else:\n            # Don't bother fetching the Channel entity (costs a request), instead\n            # try to guess and if it fails we know it's the other one (best case\n            # no extra request, worst just one).\n            try:\n                req = functions.stats.GetBroadcastStatsRequest(entity)\n                return await self(req)\n            except errors.StatsMigrateError as e:\n                dc = e.dc\n            except errors.BroadcastRequiredError:\n                req = functions.stats.GetMegagroupStatsRequest(entity)\n                try:\n                    return await self(req)\n                except errors.StatsMigrateError as e:\n                    dc = e.dc\n\n        sender = await self._borrow_exported_sender(dc)\n        try:\n            # req will be resolved to use the right types inside by now\n            return await sender.send(req)\n        finally:\n            await self._return_exported_sender(sender)\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/dialogs.py",
    "content": "import asyncio\nimport inspect\nimport itertools\nimport typing\n\nfrom .. import helpers, utils, hints, errors\nfrom ..requestiter import RequestIter\nfrom ..tl import types, functions, custom\n\n_MAX_CHUNK_SIZE = 100\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\ndef _dialog_message_key(peer, message_id):\n    \"\"\"\n    Get the key to get messages from a dialog.\n\n    We cannot just use the message ID because channels share message IDs,\n    and the peer ID is required to distinguish between them. But it is not\n    necessary in small group chats and private chats.\n    \"\"\"\n    return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id\n\n\nclass _DialogsIter(RequestIter):\n    async def _init(\n            self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder\n    ):\n        self.request = functions.messages.GetDialogsRequest(\n            offset_date=offset_date,\n            offset_id=offset_id,\n            offset_peer=offset_peer,\n            limit=1,\n            hash=0,\n            exclude_pinned=ignore_pinned,\n            folder_id=folder\n        )\n\n        if self.limit <= 0:\n            # Special case, get a single dialog and determine count\n            dialogs = await self.client(self.request)\n            self.total = getattr(dialogs, 'count', len(dialogs.dialogs))\n            raise StopAsyncIteration\n\n        self.seen = set()\n        self.offset_date = offset_date\n        self.ignore_migrated = ignore_migrated\n\n    async def _load_next_chunk(self):\n        self.request.limit = min(self.left, _MAX_CHUNK_SIZE)\n        r = await self.client(self.request)\n\n        self.total = getattr(r, 'count', len(r.dialogs))\n\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(r.users, r.chats)\n                    if not isinstance(x, (types.UserEmpty, types.ChatEmpty))}\n\n        self.client._mb_entity_cache.extend(r.users, r.chats)\n\n        messages = {}\n        for m in r.messages:\n            m._finish_init(self.client, entities, None)\n            messages[_dialog_message_key(m.peer_id, m.id)] = m\n\n        for d in r.dialogs:\n            # We check the offset date here because Telegram may ignore it\n            message = messages.get(_dialog_message_key(d.peer, d.top_message))\n            if self.offset_date:\n                date = getattr(message, 'date', None)\n                if not date or date.timestamp() > self.offset_date.timestamp():\n                    continue\n\n            peer_id = utils.get_peer_id(d.peer)\n            if peer_id not in self.seen:\n                self.seen.add(peer_id)\n                if peer_id not in entities:\n                    # > In which case can a UserEmpty appear in the list of banned members?\n                    # > In a very rare cases. This is possible but isn't an expected behavior.\n                    # Real world example: https://t.me/TelethonChat/271471\n                    continue\n\n                cd = custom.Dialog(self.client, d, entities, message)\n                if cd.dialog.pts:\n                    self.client._message_box.try_set_channel_state(\n                        utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts)\n\n                if not self.ignore_migrated or getattr(\n                        cd.entity, 'migrated_to', None) is None:\n                    self.buffer.append(cd)\n\n        if not self.buffer or len(r.dialogs) < self.request.limit\\\n                or not isinstance(r, types.messages.DialogsSlice):\n            # Buffer being empty means all returned dialogs were skipped (due to offsets).\n            # Less than we requested means we reached the end, or\n            # we didn't get a DialogsSlice which means we got all.\n            return True\n\n        # We can't use `messages[-1]` as the offset ID / date.\n        # Why? Because pinned dialogs will mess with the order\n        # in this list. Instead, we find the last dialog which\n        # has a message, and use it as an offset.\n        last_message = next(filter(None, (\n            messages.get(_dialog_message_key(d.peer, d.top_message))\n            for d in reversed(r.dialogs)\n        )), None)\n\n        self.request.exclude_pinned = True\n        self.request.offset_id = last_message.id if last_message else 0\n        self.request.offset_date = last_message.date if last_message else None\n        self.request.offset_peer = self.buffer[-1].input_entity\n\n\nclass _DraftsIter(RequestIter):\n    async def _init(self, entities, **kwargs):\n        if not entities:\n            r = await self.client(functions.messages.GetAllDraftsRequest())\n            items = r.updates\n        else:\n            peers = []\n            for entity in entities:\n                peers.append(types.InputDialogPeer(\n                    await self.client.get_input_entity(entity)))\n\n            r = await self.client(functions.messages.GetPeerDialogsRequest(peers))\n            items = r.dialogs\n\n        # TODO Maybe there should be a helper method for this?\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(r.users, r.chats)}\n\n        self.buffer.extend(\n            custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft)\n            for d in items\n        )\n\n    async def _load_next_chunk(self):\n        return []\n\n\nclass DialogMethods:\n\n    # region Public methods\n\n    def iter_dialogs(\n            self: 'TelegramClient',\n            limit: float = None,\n            *,\n            offset_date: 'hints.DateLike' = None,\n            offset_id: int = 0,\n            offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(),\n            ignore_pinned: bool = False,\n            ignore_migrated: bool = False,\n            folder: int = None,\n            archived: bool = None\n    ) -> _DialogsIter:\n        \"\"\"\n        Iterator over the dialogs (open conversations/subscribed channels).\n\n        The order is the same as the one seen in official applications\n        (first pinned, them from those with the most recent message to\n        those with the oldest message).\n\n        Arguments\n            limit (`int` | `None`):\n                How many dialogs to be retrieved as maximum. Can be set to\n                `None` to retrieve all dialogs. Note that this may take\n                whole minutes if you have hundreds of dialogs, as Telegram\n                will tell the library to slow down through a\n                ``FloodWaitError``.\n\n            offset_date (`datetime`, optional):\n                The offset date to be used.\n\n            offset_id (`int`, optional):\n                The message ID to be used as an offset.\n\n            offset_peer (:tl:`InputPeer`, optional):\n                The peer to be used as an offset.\n\n            ignore_pinned (`bool`, optional):\n                Whether pinned dialogs should be ignored or not.\n                When set to `True`, these won't be yielded at all.\n\n            ignore_migrated (`bool`, optional):\n                Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel`\n                should be included or not. By default all the chats in your\n                dialogs are returned, but setting this to `True` will ignore\n                (i.e. skip) them in the same way official applications do.\n\n            folder (`int`, optional):\n                The folder from which the dialogs should be retrieved.\n\n                If left unspecified, all dialogs (including those from\n                folders) will be returned.\n\n                If set to ``0``, all dialogs that don't belong to any\n                folder will be returned.\n\n                If set to a folder number like ``1``, only those from\n                said folder will be returned.\n\n                By default Telegram assigns the folder ID ``1`` to\n                archived chats, so you should use that if you need\n                to fetch the archived dialogs.\n\n            archived (`bool`, optional):\n                Alias for `folder`. If unspecified, all will be returned,\n                `False` implies ``folder=0`` and `True` implies ``folder=1``.\n        Yields\n            Instances of `Dialog <telethon.tl.custom.dialog.Dialog>`.\n\n        Example\n            .. code-block:: python\n\n                # Print all dialog IDs and the title, nicely formatted\n                async for dialog in client.iter_dialogs():\n                    print('{:>14}: {}'.format(dialog.id, dialog.title))\n        \"\"\"\n        if archived is not None:\n            folder = 1 if archived else 0\n\n        return _DialogsIter(\n            self,\n            limit,\n            offset_date=offset_date,\n            offset_id=offset_id,\n            offset_peer=offset_peer,\n            ignore_pinned=ignore_pinned,\n            ignore_migrated=ignore_migrated,\n            folder=folder\n        )\n\n    async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':\n        \"\"\"\n        Same as `iter_dialogs()`, but returns a\n        `TotalList <telethon.helpers.TotalList>` instead.\n\n        Example\n            .. code-block:: python\n\n                # Get all open conversation, print the title of the first\n                dialogs = await client.get_dialogs()\n                first = dialogs[0]\n                print(first.title)\n\n                # Use the dialog somewhere else\n                await client.send_message(first, 'hi')\n\n                # Getting only non-archived dialogs (both equivalent)\n                non_archived = await client.get_dialogs(folder=0)\n                non_archived = await client.get_dialogs(archived=False)\n\n                # Getting only archived dialogs (both equivalent)\n                archived = await client.get_dialogs(folder=1)\n                archived = await client.get_dialogs(archived=True)\n        \"\"\"\n        return await self.iter_dialogs(*args, **kwargs).collect()\n\n    get_dialogs.__signature__ = inspect.signature(iter_dialogs)\n\n    def iter_drafts(\n            self: 'TelegramClient',\n            entity: 'hints.EntitiesLike' = None\n    ) -> _DraftsIter:\n        \"\"\"\n        Iterator over draft messages.\n\n        The order is unspecified.\n\n        Arguments\n            entity (`hints.EntitiesLike`, optional):\n                The entity or entities for which to fetch the draft messages.\n                If left unspecified, all draft messages will be returned.\n\n        Yields\n            Instances of `Draft <telethon.tl.custom.draft.Draft>`.\n\n        Example\n            .. code-block:: python\n\n                # Clear all drafts\n                async for draft in client.get_drafts():\n                    await draft.delete()\n\n                # Getting the drafts with 'bot1' and 'bot2'\n                async for draft in client.iter_drafts(['bot1', 'bot2']):\n                    print(draft.text)\n        \"\"\"\n        if entity and not utils.is_list_like(entity):\n            entity = (entity,)\n\n        # TODO Passing a limit here makes no sense\n        return _DraftsIter(self, None, entities=entity)\n\n    async def get_drafts(\n            self: 'TelegramClient',\n            entity: 'hints.EntitiesLike' = None\n    ) -> 'hints.TotalList':\n        \"\"\"\n        Same as `iter_drafts()`, but returns a list instead.\n\n        Example\n            .. code-block:: python\n\n                # Get drafts, print the text of the first\n                drafts = await client.get_drafts()\n                print(drafts[0].text)\n\n                # Get the draft in your chat\n                draft = await client.get_drafts('me')\n                print(drafts.text)\n        \"\"\"\n        items = await self.iter_drafts(entity).collect()\n        if not entity or utils.is_list_like(entity):\n            return items\n        else:\n            return items[0]\n\n    async def edit_folder(\n            self: 'TelegramClient',\n            entity: 'hints.EntitiesLike' = None,\n            folder: typing.Union[int, typing.Sequence[int]] = None,\n            *,\n            unpack=None\n    ) -> types.Updates:\n        \"\"\"\n        Edits the folder used by one or more dialogs to archive them.\n\n        Arguments\n            entity (entities):\n                The entity or list of entities to move to the desired\n                archive folder.\n\n            folder (`int`):\n                The folder to which the dialog should be archived to.\n\n                If you want to \"archive\" a dialog, use ``folder=1``.\n\n                If you want to \"un-archive\" it, use ``folder=0``.\n\n                You may also pass a list with the same length as\n                `entities` if you want to control where each entity\n                will go.\n\n            unpack (`int`, optional):\n                If you want to unpack an archived folder, set this\n                parameter to the folder number that you want to\n                delete.\n\n                When you unpack a folder, all the dialogs inside are\n                moved to the folder number 0.\n\n                You can only use this parameter if the other two\n                are not set.\n\n        Returns\n            The :tl:`Updates` object that the request produces.\n\n        Example\n            .. code-block:: python\n\n                # Archiving the first 5 dialogs\n                dialogs = await client.get_dialogs(5)\n                await client.edit_folder(dialogs, 1)\n\n                # Un-archiving the third dialog (archiving to folder 0)\n                await client.edit_folder(dialog[2], 0)\n\n                # Moving the first dialog to folder 0 and the second to 1\n                dialogs = await client.get_dialogs(2)\n                await client.edit_folder(dialogs, [0, 1])\n\n                # Un-archiving all dialogs\n                await client.edit_folder(unpack=1)\n        \"\"\"\n        if (entity is None) == (unpack is None):\n            raise ValueError('You can only set either entities or unpack, not both')\n\n        if unpack is not None:\n            return await self(functions.folders.DeleteFolderRequest(\n                folder_id=unpack\n            ))\n\n        if not utils.is_list_like(entity):\n            entities = [await self.get_input_entity(entity)]\n        else:\n            entities = await asyncio.gather(\n                *(self.get_input_entity(x) for x in entity))\n\n        if folder is None:\n            raise ValueError('You must specify a folder')\n        elif not utils.is_list_like(folder):\n            folder = [folder] * len(entities)\n        elif len(entities) != len(folder):\n            raise ValueError('Number of folders does not match number of entities')\n\n        return await self(functions.folders.EditPeerFoldersRequest([\n            types.InputFolderPeer(x, folder_id=y)\n            for x, y in zip(entities, folder)\n        ]))\n\n    async def delete_dialog(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            *,\n            revoke: bool = False\n    ):\n        \"\"\"\n        Deletes a dialog (leaves a chat or channel).\n\n        This method can be used as a user and as a bot. However,\n        bots will only be able to use it to leave groups and channels\n        (trying to delete a private conversation will do nothing).\n\n        See also `Dialog.delete() <telethon.tl.custom.dialog.Dialog.delete>`.\n\n        Arguments\n            entity (entities):\n                The entity of the dialog to delete. If it's a chat or\n                channel, you will leave it. Note that the chat itself\n                is not deleted, only the dialog, because you left it.\n\n            revoke (`bool`, optional):\n                On private chats, you may revoke the messages from\n                the other peer too. By default, it's `False`. Set\n                it to `True` to delete the history for both.\n\n                This makes no difference for bot accounts, who can\n                only leave groups and channels.\n\n        Returns\n            The :tl:`Updates` object that the request produces,\n            or nothing for private conversations.\n\n        Example\n            .. code-block:: python\n\n                # Deleting the first dialog\n                dialogs = await client.get_dialogs(5)\n                await client.delete_dialog(dialogs[0])\n\n                # Leaving a channel by username\n                await client.delete_dialog('username')\n        \"\"\"\n        # If we have enough information (`Dialog.delete` gives it to us),\n        # then we know we don't have to kick ourselves in deactivated chats.\n        if isinstance(entity, types.Chat):\n            deactivated = entity.deactivated\n        else:\n            deactivated = False\n\n        entity = await self.get_input_entity(entity)\n        ty = helpers._entity_type(entity)\n        if ty == helpers._EntityType.CHANNEL:\n            return await self(functions.channels.LeaveChannelRequest(entity))\n\n        if ty == helpers._EntityType.CHAT and not deactivated:\n            try:\n                result = await self(functions.messages.DeleteChatUserRequest(\n                    entity.chat_id, types.InputUserSelf(), revoke_history=revoke\n                ))\n            except errors.PeerIdInvalidError:\n                # Happens if we didn't have the deactivated information\n                result = None\n        else:\n            result = None\n\n        if not await self.is_bot():\n            await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke))\n\n        return result\n\n    def conversation(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            *,\n            timeout: float = 60,\n            total_timeout: float = None,\n            max_messages: int = 100,\n            exclusive: bool = True,\n            replies_are_responses: bool = True) -> custom.Conversation:\n        \"\"\"\n        Creates a `Conversation <telethon.tl.custom.conversation.Conversation>`\n        with the given entity.\n\n        .. note::\n\n            This Conversation API has certain shortcomings, such as lacking\n            persistence, poor interaction with other event handlers, and\n            overcomplicated usage for anything beyond the simplest case.\n\n            If you plan to interact with a bot without handlers, this works\n            fine, but when running a bot yourself, you may instead prefer\n            to follow the advice from https://stackoverflow.com/a/62246569/.\n\n        This is not the same as just sending a message to create a \"dialog\"\n        with them, but rather a way to easily send messages and await for\n        responses or other reactions. Refer to its documentation for more.\n\n        Arguments\n            entity (`entity`):\n                The entity with which a new conversation should be opened.\n\n            timeout (`int` | `float`, optional):\n                The default timeout (in seconds) *per action* to be used. You\n                may also override this timeout on a per-method basis. By\n                default each action can take up to 60 seconds (the value of\n                this timeout).\n\n            total_timeout (`int` | `float`, optional):\n                The total timeout (in seconds) to use for the whole\n                conversation. This takes priority over per-action\n                timeouts. After these many seconds pass, subsequent\n                actions will result in ``asyncio.TimeoutError``.\n\n            max_messages (`int`, optional):\n                The maximum amount of messages this conversation will\n                remember. After these many messages arrive in the\n                specified chat, subsequent actions will result in\n                ``ValueError``.\n\n            exclusive (`bool`, optional):\n                By default, conversations are exclusive within a single\n                chat. That means that while a conversation is open in a\n                chat, you can't open another one in the same chat, unless\n                you disable this flag.\n\n                If you try opening an exclusive conversation for\n                a chat where it's already open, it will raise\n                ``AlreadyInConversationError``.\n\n            replies_are_responses (`bool`, optional):\n                Whether replies should be treated as responses or not.\n\n                If the setting is enabled, calls to `conv.get_response\n                <telethon.tl.custom.conversation.Conversation.get_response>`\n                and a subsequent call to `conv.get_reply\n                <telethon.tl.custom.conversation.Conversation.get_reply>`\n                will return different messages, otherwise they may return\n                the same message.\n\n                Consider the following scenario with one outgoing message,\n                1, and two incoming messages, the second one replying::\n\n                                        Hello! <1\n                    2> (reply to 1) Hi!\n                    3> (reply to 1) How are you?\n\n                And the following code:\n\n                .. code-block:: python\n\n                    async with client.conversation(chat) as conv:\n                        msg1 = await conv.send_message('Hello!')\n                        msg2 = await conv.get_response()\n                        msg3 = await conv.get_reply()\n\n                With the setting enabled, ``msg2`` will be ``'Hi!'`` and\n                ``msg3`` be ``'How are you?'`` since replies are also\n                responses, and a response was already returned.\n\n                With the setting disabled, both ``msg2`` and ``msg3`` will\n                be ``'Hi!'`` since one is a response and also a reply.\n\n        Returns\n            A `Conversation <telethon.tl.custom.conversation.Conversation>`.\n\n        Example\n            .. code-block:: python\n\n                # <you> denotes outgoing messages you sent\n                # <usr> denotes incoming response messages\n                with bot.conversation(chat) as conv:\n                    # <you> Hi!\n                    conv.send_message('Hi!')\n\n                    # <usr> Hello!\n                    hello = conv.get_response()\n\n                    # <you> Please tell me your name\n                    conv.send_message('Please tell me your name')\n\n                    # <usr> ?\n                    name = conv.get_response().raw_text\n\n                    while not any(x.isalpha() for x in name):\n                        # <you> Your name didn't have any letters! Try again\n                        conv.send_message(\"Your name didn't have any letters! Try again\")\n\n                        # <usr> Human\n                        name = conv.get_response().raw_text\n\n                    # <you> Thanks Human!\n                    conv.send_message('Thanks {}!'.format(name))\n        \"\"\"\n        return custom.Conversation(\n            self,\n            entity,\n            timeout=timeout,\n            total_timeout=total_timeout,\n            max_messages=max_messages,\n            exclusive=exclusive,\n            replies_are_responses=replies_are_responses\n\n        )\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/downloads.py",
    "content": "import datetime\nimport io\nimport os\nimport pathlib\nimport typing\nimport inspect\nimport asyncio\n\nfrom ..crypto import AES\n\nfrom .. import utils, helpers, errors, hints\nfrom ..requestiter import RequestIter\nfrom ..tl import TLObject, types, functions\n\ntry:\n    import aiohttp\nexcept ImportError:\n    aiohttp = None\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n# Chunk sizes for upload.getFile must be multiples of the smallest size\nMIN_CHUNK_SIZE = 4096\nMAX_CHUNK_SIZE = 512 * 1024\n\n# 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files.\nTIMED_OUT_SLEEP = 1\n\n\nclass _CdnRedirect(Exception):\n    def __init__(self, cdn_redirect=None):\n        self.cdn_redirect = cdn_redirect\n      \n  \nclass _DirectDownloadIter(RequestIter):\n    async def _init(\n            self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data, cdn_redirect=None):\n        self.request = functions.upload.GetFileRequest(\n            file, offset=offset, limit=request_size) \n        self._client = self.client\n        self._cdn_redirect = cdn_redirect\n        if cdn_redirect is not None:\n          self.request = functions.upload.GetCdnFileRequest(cdn_redirect.file_token, offset=offset, limit=request_size)\n          self._client = await self.client._get_cdn_client(cdn_redirect)\n        \n        self.total = file_size\n        self._stride = stride\n        self._chunk_size = chunk_size\n        self._last_part = None\n        self._msg_data = msg_data\n        self._timed_out = False\n        \n        self._exported = dc_id and self._client.session.dc_id != dc_id\n        if not self._exported:\n            # The used sender will also change if ``FileMigrateError`` occurs\n            self._sender = self.client._sender\n        else:\n            try:\n                self._sender = await self.client._borrow_exported_sender(dc_id)\n            except errors.DcIdInvalidError:\n                # Can't export a sender for the ID we are currently in\n                config = await self.client(functions.help.GetConfigRequest())\n                for option in config.dc_options:\n                    if option.ip_address == self.client.session.server_address:\n                        await utils.maybe_async(\n                            self.client.session.set_dc(\n                                option.id, option.ip_address, option.port\n                            )\n                        )\n                        await utils.maybe_async(self.client.session.save())\n                        break\n\n                # TODO Figure out why the session may have the wrong DC ID\n                self._sender = self.client._sender\n                self._exported = False\n\n    async def _load_next_chunk(self):\n        cur = await self._request()\n        self.buffer.append(cur)\n        if len(cur) < self.request.limit:\n            self.left = len(self.buffer)\n            await self.close()\n        else:\n            self.request.offset += self._stride\n\n    async def _request(self):\n        try:\n            result = await self._client._call(self._sender, self.request)\n            self._timed_out = False\n            if isinstance(result, types.upload.FileCdnRedirect):\n                if self.client._mb_entity_cache.self_bot:\n                    raise ValueError('FileCdnRedirect but the GetCdnFileRequest API access for bot users is restricted. Try to change api_id to avoid FileCdnRedirect')\n                raise _CdnRedirect(result)\n            if isinstance(result, types.upload.CdnFileReuploadNeeded):\n                await self.client._call(self.client._sender, functions.upload.ReuploadCdnFileRequest(file_token=self._cdn_redirect.file_token, request_token=result.request_token))\n                result = await self._client._call(self._sender, self.request)\n                return result.bytes\n            else:\n                return result.bytes\n\n        except errors.TimedOutError as e:\n            if self._timed_out:\n                self.client._log[__name__].warning('Got two timeouts in a row while downloading file')\n                raise\n\n            self._timed_out = True\n            self.client._log[__name__].info('Got timeout while downloading file, retrying once')\n            await asyncio.sleep(TIMED_OUT_SLEEP)\n            return await self._request()\n\n        except errors.FileMigrateError as e:\n            self.client._log[__name__].info('File lives in another DC')\n            self._sender = await self.client._borrow_exported_sender(e.new_dc)\n            self._exported = True\n            return await self._request()\n\n        except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e:\n            # Only implemented for documents which are the ones that may take that long to download\n            if not self._msg_data \\\n                    or not isinstance(self.request.location, types.InputDocumentFileLocation) \\\n                    or self.request.location.thumb_size != '':\n                raise\n\n            self.client._log[__name__].info('File ref expired during download; refetching message')\n            chat, msg_id = self._msg_data\n            msg = await self.client.get_messages(chat, ids=msg_id)\n\n            if not isinstance(msg.media, types.MessageMediaDocument):\n                raise\n\n            document = msg.media.document\n\n            # Message media may have been edited for something else\n            if document.id != self.request.location.id:\n                raise\n\n            self.request.location.file_reference = document.file_reference\n            return await self._request()\n\n    async def close(self):\n        if not self._sender:\n            return\n\n        try:\n            if self._exported:\n                await self.client._return_exported_sender(self._sender)\n            elif self._sender != self.client._sender:\n                await self._sender.disconnect()\n        finally:\n            self._sender = None\n\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, *args):\n        await self.close()\n\n    __enter__ = helpers._sync_enter\n    __exit__ = helpers._sync_exit\n\n\nclass _GenericDownloadIter(_DirectDownloadIter):\n    async def _load_next_chunk(self):\n        # 1. Fetch enough for one chunk\n        data = b''\n\n        # 1.1. ``bad`` is how much into the data we have we need to offset\n        bad = self.request.offset % self.request.limit\n        before = self.request.offset\n\n        # 1.2. We have to fetch from a valid offset, so remove that bad part\n        self.request.offset -= bad\n\n        done = False\n        while not done and len(data) - bad < self._chunk_size:\n            cur = await self._request()\n            self.request.offset += self.request.limit\n\n            data += cur\n            done = len(cur) < self.request.limit\n\n        # 1.3 Restore our last desired offset\n        self.request.offset = before\n\n        # 2. Fill the buffer with the data we have\n        # 2.1. Slicing `bytes` is expensive, yield `memoryview` instead\n        mem = memoryview(data)\n\n        # 2.2. The current chunk starts at ``bad`` offset into the data,\n        #      and each new chunk is ``stride`` bytes apart of the other\n        for i in range(bad, len(data), self._stride):\n            self.buffer.append(mem[i:i + self._chunk_size])\n\n            # 2.3. We will yield this offset, so move to the next one\n            self.request.offset += self._stride\n\n        # 2.4. If we are in the last chunk, we will return the last partial data\n        if done:\n            self.left = len(self.buffer)\n            await self.close()\n            return\n\n        # 2.5. If we are not done, we can't return incomplete chunks.\n        if len(self.buffer[-1]) != self._chunk_size:\n            self._last_part = self.buffer.pop().tobytes()\n\n            # 3. Be careful with the offsets. Re-fetching a bit of data\n            #    is fine, since it greatly simplifies things.\n            # TODO Try to not re-fetch data\n            self.request.offset -= self._stride\n\n\nclass DownloadMethods:\n\n    # region Public methods\n\n    async def download_profile_photo(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            file: 'hints.FileLike' = None,\n            *,\n            download_big: bool = True) -> typing.Optional[str]:\n        \"\"\"\n        Downloads the profile photo from the given user, chat or channel.\n\n        Arguments\n            entity (`entity`):\n                From who the photo will be downloaded.\n\n                .. note::\n\n                    This method expects the full entity (which has the data\n                    to download the photo), not an input variant.\n\n                    It's possible that sometimes you can't fetch the entity\n                    from its input (since you can get errors like\n                    ``ChannelPrivateError``) but you already have it through\n                    another call, like getting a forwarded message from it.\n\n            file (`str` | `file`, optional):\n                The output file path, directory, or stream-like object.\n                If the path exists and is a file, it will be overwritten.\n                If file is the type `bytes`, it will be downloaded in-memory\n                and returned as a bytestring (i.e. ``file=bytes``, without\n                parentheses or quotes).\n\n            download_big (`bool`, optional):\n                Whether to use the big version of the available photos.\n\n        Returns\n            `None` if no photo was provided, or if it was Empty. On success\n            the file path is returned since it may differ from the one given.\n\n        Example\n            .. code-block:: python\n\n                # Download your own profile photo\n                path = await client.download_profile_photo('me')\n                print(path)\n        \"\"\"\n        # hex(crc32(x.encode('ascii'))) for x in\n        # ('User', 'Chat', 'UserFull', 'ChatFull')\n        ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697)\n        # ('InputPeer', 'InputUser', 'InputChannel')\n        INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd)\n        if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS:\n            entity = await self.get_entity(entity)\n\n        thumb = -1 if download_big else 0\n\n        possible_names = []\n        if entity.SUBCLASS_OF_ID not in ENTITIES:\n            photo = entity\n        else:\n            if not hasattr(entity, 'photo'):\n                # Special case: may be a ChatFull with photo:Photo\n                # This is different from a normal UserProfilePhoto and Chat\n                if not hasattr(entity, 'chat_photo'):\n                    return None\n\n                return await self._download_photo(\n                    entity.chat_photo, file, date=None,\n                    thumb=thumb, progress_callback=None\n                )\n\n            for attr in ('username', 'first_name', 'title'):\n                possible_names.append(getattr(entity, attr, None))\n\n            photo = entity.photo\n\n        if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):\n            dc_id = photo.dc_id\n            loc = types.InputPeerPhotoFileLocation(\n                # min users can be used to download profile photos\n                # self.get_input_entity would otherwise not accept those\n                peer=utils.get_input_peer(entity, check_hash=False),\n                photo_id=photo.photo_id,\n                big=download_big\n            )\n        else:\n            # It doesn't make any sense to check if `photo` can be used\n            # as input location, because then this method would be able\n            # to \"download the profile photo of a message\", i.e. its\n            # media which should be done with `download_media` instead.\n            return None\n\n        file = self._get_proper_filename(\n            file, 'profile_photo', '.jpg',\n            possible_names=possible_names\n        )\n\n        try:\n            result = await self.download_file(loc, file, dc_id=dc_id)\n            return result if file is bytes else file\n        except errors.LocationInvalidError:\n            # See issue #500, Android app fails as of v4.6.0 (1155).\n            # The fix seems to be using the full channel chat photo.\n            ie = await self.get_input_entity(entity)\n            ty = helpers._entity_type(ie)\n            if ty == helpers._EntityType.CHANNEL:\n                full = await self(functions.channels.GetFullChannelRequest(ie))\n                return await self._download_photo(\n                    full.full_chat.chat_photo, file,\n                    date=None, progress_callback=None,\n                    thumb=thumb\n                )\n            else:\n                # Until there's a report for chats, no need to.\n                return None\n\n    async def download_media(\n            self: 'TelegramClient',\n            message: 'hints.MessageLike',\n            file: 'hints.FileLike' = None,\n            *,\n            thumb: 'typing.Union[int, types.TypePhotoSize]' = None,\n            progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:\n        \"\"\"\n        Downloads the given media from a message object.\n\n        Note that if the download is too slow, you should consider installing\n        ``cryptg`` (through ``pip install cryptg``) so that decrypting the\n        received data is done in C instead of Python (much faster).\n\n        See also `Message.download_media() <telethon.tl.custom.message.Message.download_media>`.\n\n        Arguments\n            message (`Message <telethon.tl.custom.message.Message>` | :tl:`Media`):\n                The media or message containing the media that will be downloaded.\n\n            file (`str` | `file`, optional):\n                The output file path, directory, or stream-like object.\n                If the path exists and is a file, it will be overwritten.\n                If file is the type `bytes`, it will be downloaded in-memory\n                and returned as a bytestring (i.e. ``file=bytes``, without\n                parentheses or quotes).\n\n            progress_callback (`callable`, optional):\n                A callback function accepting two parameters:\n                ``(received bytes, total)``.\n\n            thumb (`int` | :tl:`PhotoSize`, optional):\n                Which thumbnail size from the document or photo to download,\n                instead of downloading the document or photo itself.\n\n                If it's specified but the file does not have a thumbnail,\n                this method will return `None`.\n\n                The parameter should be an integer index between ``0`` and\n                ``len(sizes)``. ``0`` will download the smallest thumbnail,\n                and ``len(sizes) - 1`` will download the largest thumbnail.\n                You can also use negative indices, which work the same as\n                they do in Python's `list`.\n\n                You can also pass the :tl:`PhotoSize` instance to use.\n                Alternatively, the thumb size type `str` may be used.\n\n                In short, use ``thumb=0`` if you want the smallest thumbnail\n                and ``thumb=-1`` if you want the largest thumbnail.\n\n                .. note::\n                    The largest thumbnail may be a video instead of a photo,\n                    as they are available since layer 116 and are bigger than\n                    any of the photos.\n\n        Returns\n            `None` if no media was provided, or if it was Empty. On success\n            the file path is returned since it may differ from the one given.\n\n        Example\n            .. code-block:: python\n\n                path = await client.download_media(message)\n                await client.download_media(message, filename)\n                # or\n                path = await message.download_media()\n                await message.download_media(filename)\n\n                # Downloading to memory\n                blob = await client.download_media(message, bytes)\n\n                # Printing download progress\n                def callback(current, total):\n                    print('Downloaded', current, 'out of', total,\n                          'bytes: {:.2%}'.format(current / total))\n\n                await client.download_media(message, progress_callback=callback)\n        \"\"\"\n        # Downloading large documents may be slow enough to require a new file reference\n        # to be obtained mid-download. Store (input chat, message id) so that the message\n        # can be re-fetched.\n        msg_data = None\n\n        # TODO This won't work for messageService\n        if isinstance(message, types.Message):\n            date = message.date\n            media = message.media\n            msg_data = (message.input_chat, message.id) if message.input_chat else None\n        else:\n            date = datetime.datetime.now()\n            media = message\n\n        if isinstance(media, str):\n            media = utils.resolve_bot_file_id(media)\n\n        if isinstance(media, types.MessageService):\n            if isinstance(message.action,\n                          types.MessageActionChatEditPhoto):\n                media = media.photo\n\n        if isinstance(media, types.MessageMediaWebPage):\n            if isinstance(media.webpage, types.WebPage):\n                media = media.webpage.document or media.webpage.photo\n\n        if isinstance(media, (types.MessageMediaPhoto, types.Photo)):\n            return await self._download_photo(\n                media, file, date, thumb, progress_callback\n            )\n        elif isinstance(media, (types.MessageMediaDocument, types.Document)):\n            return await self._download_document(\n                media, file, date, thumb, progress_callback, msg_data\n            )\n        elif isinstance(media, types.MessageMediaContact) and thumb is None:\n            return self._download_contact(\n                media, file\n            )\n        elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None:\n            return await self._download_web_document(\n                media, file, progress_callback\n            )\n\n    async def download_file(\n            self: 'TelegramClient',\n            input_location: 'hints.FileLike',\n            file: 'hints.OutFileLike' = None,\n            *,\n            part_size_kb: float = None,\n            file_size: int = None,\n            progress_callback: 'hints.ProgressCallback' = None,\n            dc_id: int = None,\n            key: bytes = None,\n            iv: bytes = None) -> typing.Optional[bytes]:\n        \"\"\"\n        Low-level method to download files from their input location.\n\n        .. note::\n\n            Generally, you should instead use `download_media`.\n            This method is intended to be a bit more low-level.\n\n        Arguments\n            input_location (:tl:`InputFileLocation`):\n                The file location from which the file will be downloaded.\n                See `telethon.utils.get_input_location` source for a complete\n                list of supported types.\n\n            file (`str` | `file`, optional):\n                The output file path, directory, or stream-like object.\n                If the path exists and is a file, it will be overwritten.\n\n                If the file path is `None` or `bytes`, then the result\n                will be saved in memory and returned as `bytes`.\n\n            part_size_kb (`int`, optional):\n                Chunk size when downloading files. The larger, the less\n                requests will be made (up to 512KB maximum).\n\n            file_size (`int`, optional):\n                The file size that is about to be downloaded, if known.\n                Only used if ``progress_callback`` is specified.\n\n            progress_callback (`callable`, optional):\n                A callback function accepting two parameters:\n                ``(downloaded bytes, total)``. Note that the\n                ``total`` is the provided ``file_size``.\n\n            dc_id (`int`, optional):\n                The data center the library should connect to in order\n                to download the file. You shouldn't worry about this.\n\n            key ('bytes', optional):\n                In case of an encrypted upload (secret chats) a key is supplied\n\n            iv ('bytes', optional):\n                In case of an encrypted upload (secret chats) an iv is supplied\n\n\n        Example\n            .. code-block:: python\n\n                # Download a file and print its header\n                data = await client.download_file(input_file, bytes)\n                print(data[:16])\n        \"\"\"\n        return await self._download_file(\n            input_location,\n            file,\n            part_size_kb=part_size_kb,\n            file_size=file_size,\n            progress_callback=progress_callback,\n            dc_id=dc_id,\n            key=key,\n            iv=iv,\n        )\n\n    async def _download_file(\n            self: 'TelegramClient',\n            input_location: 'hints.FileLike',\n            file: 'hints.OutFileLike' = None,\n            *,\n            part_size_kb: float = None,\n            file_size: int = None,\n            progress_callback: 'hints.ProgressCallback' = None,\n            dc_id: int = None,\n            key: bytes = None,\n            iv: bytes = None,\n            msg_data: tuple = None,\n            cdn_redirect: types.upload.FileCdnRedirect = None\n    ) -> typing.Optional[bytes]:\n        if not part_size_kb:\n            if not file_size:\n                part_size_kb = 64  # Reasonable default\n            else:\n                part_size_kb = utils.get_appropriated_part_size(file_size)\n\n        part_size = int(part_size_kb * 1024)\n        if part_size % MIN_CHUNK_SIZE != 0:\n            raise ValueError(\n                'The part size must be evenly divisible by 4096.')\n\n        if isinstance(file, pathlib.Path):\n            file = str(file.absolute())\n\n        in_memory = file is None or file is bytes\n        if in_memory:\n            f = io.BytesIO()\n        elif isinstance(file, str):\n            # Ensure that we'll be able to download the media\n            helpers.ensure_parent_dir_exists(file)\n            f = open(file, 'wb')\n        else:\n            f = file\n\n        try:\n            async for chunk in self._iter_download(\n                    input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data, cdn_redirect=cdn_redirect):\n                if iv and key:\n                    chunk = AES.decrypt_ige(chunk, key, iv)\n                r = f.write(chunk)\n                if inspect.isawaitable(r):\n                    await r\n\n                if progress_callback:\n                    r = progress_callback(f.tell(), file_size)\n                    if inspect.isawaitable(r):\n                        await r\n\n            # Not all IO objects have flush (see #1227)\n            if callable(getattr(f, 'flush', None)):\n                f.flush()\n\n            if in_memory:\n                return f.getvalue()\n        except _CdnRedirect as e:\n          self._log[__name__].info('FileCdnRedirect to CDN data center %s', e.cdn_redirect.dc_id)\n          return await self._download_file(\n              input_location=input_location,\n              file=file,\n              part_size_kb=part_size_kb,\n              file_size=file_size,\n              progress_callback=progress_callback,\n              dc_id=e.cdn_redirect.dc_id,\n              key=e.cdn_redirect.encryption_key,\n              iv=e.cdn_redirect.encryption_iv,\n              msg_data=msg_data,\n              cdn_redirect=e.cdn_redirect\n          )\n        finally:\n            if isinstance(file, str) or in_memory:\n                f.close()\n\n    def iter_download(\n            self: 'TelegramClient',\n            file: 'hints.FileLike',\n            *,\n            offset: int = 0,\n            stride: int = None,\n            limit: int = None,\n            chunk_size: int = None,\n            request_size: int = MAX_CHUNK_SIZE,\n            file_size: int = None,\n            dc_id: int = None\n    ):\n        \"\"\"\n        Iterates over a file download, yielding chunks of the file.\n\n        This method can be used to stream files in a more convenient\n        way, since it offers more control (pausing, resuming, etc.)\n\n        .. note::\n\n            Using a value for `offset` or `stride` which is not a multiple\n            of the minimum allowed `request_size`, or if `chunk_size` is\n            different from `request_size`, the library will need to do a\n            bit more work to fetch the data in the way you intend it to.\n\n            You normally shouldn't worry about this.\n\n        Arguments\n            file (`hints.FileLike`):\n                The file of which contents you want to iterate over.\n\n            offset (`int`, optional):\n                The offset in bytes into the file from where the\n                download should start. For example, if a file is\n                1024KB long and you just want the last 512KB, you\n                would use ``offset=512 * 1024``.\n\n            stride (`int`, optional):\n                The stride of each chunk (how much the offset should\n                advance between reading each chunk). This parameter\n                should only be used for more advanced use cases.\n\n                It must be bigger than or equal to the `chunk_size`.\n\n            limit (`int`, optional):\n                The limit for how many *chunks* will be yielded at most.\n\n            chunk_size (`int`, optional):\n                The maximum size of the chunks that will be yielded.\n                Note that the last chunk may be less than this value.\n                By default, it equals to `request_size`.\n\n            request_size (`int`, optional):\n                How many bytes will be requested to Telegram when more\n                data is required. By default, as many bytes as possible\n                are requested. If you would like to request data in\n                smaller sizes, adjust this parameter.\n\n                Note that values outside the valid range will be clamped,\n                and the final value will also be a multiple of the minimum\n                allowed size.\n\n            file_size (`int`, optional):\n                If the file size is known beforehand, you should set\n                this parameter to said value. Depending on the type of\n                the input file passed, this may be set automatically.\n\n            dc_id (`int`, optional):\n                The data center the library should connect to in order\n                to download the file. You shouldn't worry about this.\n\n        Yields\n\n            `bytes` objects representing the chunks of the file if the\n            right conditions are met, or `memoryview` objects instead.\n\n        Example\n            .. code-block:: python\n\n                # Streaming `media` to an output file\n                # After the iteration ends, the sender is cleaned up\n                with open('photo.jpg', 'wb') as fd:\n                    async for chunk in client.iter_download(media):\n                        fd.write(chunk)\n\n                # Fetching only the header of a file (32 bytes)\n                # You should manually close the iterator in this case.\n                #\n                # \"stream\" is a common name for asynchronous generators,\n                # and iter_download will yield `bytes` (chunks of the file).\n                stream = client.iter_download(media, request_size=32)\n                header = await stream.__anext__()  # \"manual\" version of `async for`\n                await stream.close()\n                assert len(header) == 32\n        \"\"\"\n        return self._iter_download(\n            file,\n            offset=offset,\n            stride=stride,\n            limit=limit,\n            chunk_size=chunk_size,\n            request_size=request_size,\n            file_size=file_size,\n            dc_id=dc_id,\n        )\n\n    def _iter_download(\n            self: 'TelegramClient',\n            file: 'hints.FileLike',\n            *,\n            offset: int = 0,\n            stride: int = None,\n            limit: int = None,\n            chunk_size: int = None,\n            request_size: int = MAX_CHUNK_SIZE,\n            file_size: int = None,\n            dc_id: int = None,\n            msg_data: tuple = None,\n            cdn_redirect: types.upload.FileCdnRedirect = None\n    ):\n        info = utils._get_file_info(file)\n        if info.dc_id is not None:\n            dc_id = info.dc_id\n\n        if file_size is None:\n            file_size = info.size\n\n        file = info.location\n\n        if chunk_size is None:\n            chunk_size = request_size\n\n        if limit is None and file_size is not None:\n            limit = (file_size + chunk_size - 1) // chunk_size\n\n        if stride is None:\n            stride = chunk_size\n        elif stride < chunk_size:\n            raise ValueError('stride must be >= chunk_size')\n\n        request_size -= request_size % MIN_CHUNK_SIZE\n        if request_size < MIN_CHUNK_SIZE:\n            request_size = MIN_CHUNK_SIZE\n        elif request_size > MAX_CHUNK_SIZE:\n            request_size = MAX_CHUNK_SIZE\n\n        if chunk_size == request_size \\\n                and offset % MIN_CHUNK_SIZE == 0 \\\n                and stride % MIN_CHUNK_SIZE == 0 \\\n                and (limit is None or offset % limit == 0):\n            cls = _DirectDownloadIter\n            self._log[__name__].info('Starting direct file download in chunks of '\n                                     '%d at %d, stride %d', request_size, offset, stride)\n        else:\n            cls = _GenericDownloadIter\n            self._log[__name__].info('Starting indirect file download in chunks of '\n                                     '%d at %d, stride %d', request_size, offset, stride)\n\n        return cls(\n            self,\n            limit,\n            file=file,\n            dc_id=dc_id,\n            offset=offset,\n            stride=stride,\n            chunk_size=chunk_size,\n            request_size=request_size,\n            file_size=file_size,\n            msg_data=msg_data,\n            cdn_redirect=cdn_redirect\n        )\n\n    # endregion\n\n    # region Private methods\n\n    @staticmethod\n    def _get_thumb(thumbs, thumb):\n        if not thumbs:\n            return None\n\n        # Seems Telegram has changed the order and put `PhotoStrippedSize`\n        # last while this is the smallest (layer 116). Ensure we have the\n        # sizes sorted correctly with a custom function.\n        def sort_thumbs(thumb):\n            if isinstance(thumb, types.PhotoStrippedSize):\n                return 1, len(thumb.bytes)\n            if isinstance(thumb, types.PhotoCachedSize):\n                return 1, len(thumb.bytes)\n            if isinstance(thumb, types.PhotoSize):\n                return 1, thumb.size\n            if isinstance(thumb, types.PhotoSizeProgressive):\n                return 1, max(thumb.sizes)\n            if isinstance(thumb, types.VideoSize):\n                return 2, thumb.size\n\n            # Empty size or invalid should go last\n            return 0, 0\n\n        thumbs = list(sorted(thumbs, key=sort_thumbs))\n\n        for i in reversed(range(len(thumbs))):\n            # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually\n            # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this\n            # thumb size doesn't actually exist (#1655).\n            if isinstance(thumbs[i], types.PhotoPathSize):\n                thumbs.pop(i)\n\n        if thumb is None:\n            return thumbs[-1]\n        elif isinstance(thumb, int):\n            return thumbs[thumb]\n        elif isinstance(thumb, str):\n            return next((t for t in thumbs if t.type == thumb), None)\n        elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize,\n                                types.PhotoStrippedSize, types.VideoSize)):\n            return thumb\n        else:\n            return None\n\n    def _download_cached_photo_size(self: 'TelegramClient', size, file):\n        # No need to download anything, simply write the bytes\n        if isinstance(size, types.PhotoStrippedSize):\n            data = utils.stripped_photo_to_jpg(size.bytes)\n        else:\n            data = size.bytes\n\n        if file is bytes:\n            return data\n        elif isinstance(file, str):\n            helpers.ensure_parent_dir_exists(file)\n            f = open(file, 'wb')\n        else:\n            f = file\n\n        try:\n            f.write(data)\n        finally:\n            if isinstance(file, str):\n                f.close()\n        return file\n\n    async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback):\n        \"\"\"Specialized version of .download_media() for photos\"\"\"\n        # Determine the photo and its largest size\n        if isinstance(photo, types.MessageMediaPhoto):\n            photo = photo.photo\n        if not isinstance(photo, types.Photo):\n            return\n\n        # Include video sizes here (but they may be None so provide an empty list)\n        size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb)\n        if not size or isinstance(size, types.PhotoSizeEmpty):\n            return\n\n        if isinstance(size, types.VideoSize):\n            file = self._get_proper_filename(file, 'video', '.mp4', date=date)\n        else:\n            file = self._get_proper_filename(file, 'photo', '.jpg', date=date)\n\n        if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):\n            return self._download_cached_photo_size(size, file)\n\n        if isinstance(size, types.PhotoSizeProgressive):\n            file_size = max(size.sizes)\n        else:\n            file_size = size.size\n\n        result = await self.download_file(\n            types.InputPhotoFileLocation(\n                id=photo.id,\n                access_hash=photo.access_hash,\n                file_reference=photo.file_reference,\n                thumb_size=size.type\n            ),\n            file,\n            file_size=file_size,\n            progress_callback=progress_callback\n        )\n        return result if file is bytes else file\n\n    @staticmethod\n    def _get_kind_and_names(attributes):\n        \"\"\"Gets kind and possible names for :tl:`DocumentAttribute`.\"\"\"\n        kind = 'document'\n        possible_names = []\n        for attr in attributes:\n            if isinstance(attr, types.DocumentAttributeFilename):\n                possible_names.insert(0, attr.file_name)\n\n            elif isinstance(attr, types.DocumentAttributeAudio):\n                kind = 'audio'\n                if attr.performer and attr.title:\n                    possible_names.append('{} - {}'.format(\n                        attr.performer, attr.title\n                    ))\n                elif attr.performer:\n                    possible_names.append(attr.performer)\n                elif attr.title:\n                    possible_names.append(attr.title)\n                elif attr.voice:\n                    kind = 'voice'\n\n        return kind, possible_names\n\n    async def _download_document(\n            self, document, file, date, thumb, progress_callback, msg_data):\n        \"\"\"Specialized version of .download_media() for documents.\"\"\"\n        if isinstance(document, types.MessageMediaDocument):\n            document = document.document\n        if not isinstance(document, types.Document):\n            return\n\n        if thumb is None:\n            kind, possible_names = self._get_kind_and_names(document.attributes)\n            file = self._get_proper_filename(\n                file, kind, utils.get_extension(document),\n                date=date, possible_names=possible_names\n            )\n            size = None\n        else:\n            file = self._get_proper_filename(file, 'photo', '.jpg', date=date)\n            size = self._get_thumb(document.thumbs, thumb)\n            if not size or isinstance(size, types.PhotoSizeEmpty):\n                return\n\n            if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):\n                return self._download_cached_photo_size(size, file)\n\n        result = await self._download_file(\n            types.InputDocumentFileLocation(\n                id=document.id,\n                access_hash=document.access_hash,\n                file_reference=document.file_reference,\n                thumb_size=size.type if size else ''\n            ),\n            file,\n            file_size=size.size if size else document.size,\n            progress_callback=progress_callback,\n            msg_data=msg_data,\n        )\n\n        return result if file is bytes else file\n\n    @classmethod\n    def _download_contact(cls, mm_contact, file):\n        \"\"\"\n        Specialized version of .download_media() for contacts.\n        Will make use of the vCard 4.0 format.\n        \"\"\"\n        first_name = mm_contact.first_name\n        last_name = mm_contact.last_name\n        phone_number = mm_contact.phone_number\n\n        # Remove these pesky characters\n        first_name = first_name.replace(';', '')\n        last_name = (last_name or '').replace(';', '')\n        result = (\n            'BEGIN:VCARD\\n'\n            'VERSION:4.0\\n'\n            'N:{f};{l};;;\\n'\n            'FN:{f} {l}\\n'\n            'TEL;TYPE=cell;VALUE=uri:tel:+{p}\\n'\n            'END:VCARD\\n'\n        ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8')\n\n        file = cls._get_proper_filename(\n            file, 'contact', '.vcard',\n            possible_names=[first_name, phone_number, last_name]\n        )\n        if file is bytes:\n            return result\n        f = file if hasattr(file, 'write') else open(file, 'wb')\n\n        try:\n            f.write(result)\n        finally:\n            # Only close the stream if we opened it\n            if f != file:\n                f.close()\n\n        return file\n\n    @classmethod\n    async def _download_web_document(cls, web, file, progress_callback):\n        \"\"\"\n        Specialized version of .download_media() for web documents.\n        \"\"\"\n        if not aiohttp:\n            raise ValueError(\n                'Cannot download web documents without the aiohttp '\n                'dependency install it (pip install aiohttp)'\n            )\n\n        # TODO Better way to get opened handles of files and auto-close\n        kind, possible_names = cls._get_kind_and_names(web.attributes)\n        file = cls._get_proper_filename(\n            file, kind, utils.get_extension(web),\n            possible_names=possible_names\n        )\n        if file is bytes:\n            f = io.BytesIO()\n        elif hasattr(file, 'write'):\n            f = file\n        else:\n            f = open(file, 'wb')\n\n        try:\n            async with aiohttp.ClientSession() as session:\n                # TODO Use progress_callback; get content length from response\n                # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319\n                async with session.get(web.url) as response:\n                    while True:\n                        chunk = await response.content.read(128 * 1024)\n                        if not chunk:\n                            break\n                        f.write(chunk)\n        finally:\n            if f != file:\n                f.close()\n\n        return f.getvalue() if file is bytes else file\n\n    @staticmethod\n    def _get_proper_filename(file, kind, extension,\n                             date=None, possible_names=None):\n        \"\"\"Gets a proper filename for 'file', if this is a path.\n\n           'kind' should be the kind of the output file (photo, document...)\n           'extension' should be the extension to be added to the file if\n                       the filename doesn't have any yet\n           'date' should be when this file was originally sent, if known\n           'possible_names' should be an ordered list of possible names\n\n           If no modification is made to the path, any existing file\n           will be overwritten.\n           If any modification is made to the path, this method will\n           ensure that no existing file will be overwritten.\n        \"\"\"\n        if isinstance(file, pathlib.Path):\n            file = str(file.absolute())\n\n        if file is not None and not isinstance(file, str):\n            # Probably a stream-like object, we cannot set a filename here\n            return file\n\n        if file is None:\n            file = ''\n        elif os.path.isfile(file):\n            # Make no modifications to valid existing paths\n            return file\n\n        if os.path.isdir(file) or not file:\n            try:\n                isreserved = getattr(os.path, 'isreserved', lambda _: False)  # Python 3.13 and above\n                name = None if possible_names is None else next(\n                    x  # basename to prevent path traversal (#4713)\n                    for x in map(os.path.basename, possible_names)\n                    if x and not isreserved(x)\n                )\n            except StopIteration:\n                name = None\n\n            if not name:\n                if not date:\n                    date = datetime.datetime.now()\n                name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format(\n                    kind,\n                    date.year, date.month, date.day,\n                    date.hour, date.minute, date.second,\n                )\n            file = os.path.join(file, name)\n\n        directory, name = os.path.split(file)\n        name, ext = os.path.splitext(name)\n        if not ext:\n            ext = extension\n\n        result = os.path.join(directory, name + ext)\n        if not os.path.isfile(result):\n            return result\n\n        i = 1\n        while True:\n            result = os.path.join(directory, '{} ({}){}'.format(name, i, ext))\n            if not os.path.isfile(result):\n                return result\n            i += 1\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/messageparse.py",
    "content": "import itertools\nimport re\nimport typing\n\nfrom .. import helpers, utils\nfrom ..tl import types\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nclass MessageParseMethods:\n\n    # region Public properties\n\n    @property\n    def parse_mode(self: 'TelegramClient'):\n        \"\"\"\n        This property is the default parse mode used when sending messages.\n        Defaults to `telethon.extensions.markdown`. It will always\n        be either `None` or an object with ``parse`` and ``unparse``\n        methods.\n\n        When setting a different value it should be one of:\n\n        * Object with ``parse`` and ``unparse`` methods.\n        * A ``callable`` to act as the parse method.\n        * A `str` indicating the ``parse_mode``. For Markdown ``'md'``\n          or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'``\n          may be used.\n\n        The ``parse`` method should be a function accepting a single\n        parameter, the text to parse, and returning a tuple consisting\n        of ``(parsed message str, [MessageEntity instances])``.\n\n        The ``unparse`` method should be the inverse of ``parse`` such\n        that ``assert text == unparse(*parse(text))``.\n\n        See :tl:`MessageEntity` for allowed message entities.\n\n        Example\n            .. code-block:: python\n\n                # Disabling default formatting\n                client.parse_mode = None\n\n                # Enabling HTML as the default format\n                client.parse_mode = 'html'\n        \"\"\"\n        return self._parse_mode\n\n    @parse_mode.setter\n    def parse_mode(self: 'TelegramClient', mode: str):\n        self._parse_mode = utils.sanitize_parse_mode(mode)\n\n    # endregion\n\n    # region Private methods\n\n    async def _replace_with_mention(self: 'TelegramClient', entities, i, user):\n        \"\"\"\n        Helper method to replace ``entities[i]`` to mention ``user``,\n        or do nothing if it can't be found.\n        \"\"\"\n        try:\n            entities[i] = types.InputMessageEntityMentionName(\n                entities[i].offset, entities[i].length,\n                await self.get_input_entity(user)\n            )\n            return True\n        except (ValueError, TypeError):\n            return False\n\n    async def _parse_message_text(self: 'TelegramClient', message, parse_mode):\n        \"\"\"\n        Returns a (parsed message, entities) tuple depending on ``parse_mode``.\n        \"\"\"\n        if parse_mode == ():\n            parse_mode = self._parse_mode\n        else:\n            parse_mode = utils.sanitize_parse_mode(parse_mode)\n\n        if not parse_mode:\n            return message, []\n\n        original_message = message\n        message, msg_entities = parse_mode.parse(message)\n        if original_message and not message and not msg_entities:\n            raise ValueError(\"Failed to parse message\")\n\n        for i in reversed(range(len(msg_entities))):\n            e = msg_entities[i]\n            if not e.length:\n                # 0-length MessageEntity is no longer valid #3884.\n                # Because the user can provide their own parser (with reasonable 0-length\n                # entities), strip them here rather than fixing the built-in parsers.\n                del msg_entities[i]\n            elif isinstance(e, types.MessageEntityTextUrl):\n                m = re.match(r'^@|\\+|tg://user\\?id=(\\d+)', e.url)\n                if m:\n                    user = int(m.group(1)) if m.group(1) else e.url\n                    is_mention = await self._replace_with_mention(msg_entities, i, user)\n                    if not is_mention:\n                        del msg_entities[i]\n            elif isinstance(e, (types.MessageEntityMentionName,\n                                types.InputMessageEntityMentionName)):\n                is_mention = await self._replace_with_mention(msg_entities, i, e.user_id)\n                if not is_mention:\n                    del msg_entities[i]\n\n        return message, msg_entities\n\n    def _get_response_message(self: 'TelegramClient', request, result, input_chat):\n        \"\"\"\n        Extracts the response message known a request and Update result.\n        The request may also be the ID of the message to match.\n\n        If ``request is None`` this method returns ``{id: message}``.\n\n        If ``request.random_id`` is a list, this method returns a list too.\n        \"\"\"\n        if isinstance(result, types.UpdateShort):\n            updates = [result.update]\n            entities = {}\n        elif isinstance(result, (types.Updates, types.UpdatesCombined)):\n            updates = result.updates\n            entities = {utils.get_peer_id(x): x\n                        for x in\n                        itertools.chain(result.users, result.chats)}\n        else:\n            return None\n\n        random_to_id = {}\n        id_to_message = {}\n        for update in updates:\n            if isinstance(update, types.UpdateMessageID):\n                random_to_id[update.random_id] = update.id\n\n            elif isinstance(update, (\n                    types.UpdateNewChannelMessage, types.UpdateNewMessage)):\n                update.message._finish_init(self, entities, input_chat)\n\n                # Pinning a message with `updatePinnedMessage` seems to\n                # always produce a service message we can't map so return\n                # it directly. The same happens for kicking users.\n                #\n                # It could also be a list (e.g. when sending albums).\n                #\n                # TODO this method is getting messier and messier as time goes on\n                if hasattr(request, 'random_id') or utils.is_list_like(request):\n                    id_to_message[update.message.id] = update.message\n                else:\n                    return update.message\n\n            elif (isinstance(update, types.UpdateEditMessage)\n                  and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL):\n                update.message._finish_init(self, entities, input_chat)\n\n                # Live locations use `sendMedia` but Telegram responds with\n                # `updateEditMessage`, which means we won't have `id` field.\n                if hasattr(request, 'random_id'):\n                    id_to_message[update.message.id] = update.message\n                elif request.id == update.message.id:\n                    return update.message\n\n            elif (isinstance(update, types.UpdateEditChannelMessage)\n                  and utils.get_peer_id(request.peer) ==\n                  utils.get_peer_id(update.message.peer_id)):\n                if request.id == update.message.id:\n                    update.message._finish_init(self, entities, input_chat)\n                    return update.message\n\n            elif isinstance(update, types.UpdateNewScheduledMessage):\n                update.message._finish_init(self, entities, input_chat)\n                # Scheduled IDs may collide with normal IDs. However, for a\n                # single request there *shouldn't* be a mix between \"some\n                # scheduled and some not\".\n                id_to_message[update.message.id] = update.message\n\n            elif isinstance(update, types.UpdateMessagePoll):\n                if request.media.poll.id == update.poll_id:\n                    m = types.Message(\n                        id=request.id,\n                        peer_id=utils.get_peer(request.peer),\n                        media=types.MessageMediaPoll(\n                            poll=update.poll,\n                            results=update.results\n                        )\n                    )\n                    m._finish_init(self, entities, input_chat)\n                    return m\n\n        if request is None:\n            return id_to_message\n\n        random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None)\n        if random_id is None:\n            # Can happen when pinning a message does not actually produce a service message.\n            self._log[__name__].warning(\n                'No random_id in %s to map to, returning None message for %s', request, result)\n            return None\n\n        if not utils.is_list_like(random_id):\n            msg = id_to_message.get(random_to_id.get(random_id))\n\n            if not msg:\n                self._log[__name__].warning(\n                    'Request %s had missing message mapping %s', request, result)\n\n            return msg\n\n        try:\n            return [id_to_message[random_to_id[rnd]] for rnd in random_id]\n        except KeyError:\n            # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets\n            # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at\n            # Telegram), in which case we get some \"missing\" message mappings.\n            # Log them with the hope that we can better work around them.\n            #\n            # This also happens when trying to forward messages that can't\n            # be forwarded because they don't exist (0, service, deleted)\n            # among others which could be (like deleted or existing).\n            self._log[__name__].warning(\n                'Request %s had missing message mappings %s', request, result)\n\n        return [\n            id_to_message.get(random_to_id[rnd])\n            if rnd in random_to_id\n            else None\n            for rnd in random_id\n        ]\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/messages.py",
    "content": "import inspect\nimport itertools\nimport typing\nimport warnings\n\nfrom .. import helpers, utils, errors, hints\nfrom ..requestiter import RequestIter\nfrom ..tl import types, functions\n\n_MAX_CHUNK_SIZE = 100\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nclass _MessagesIter(RequestIter):\n    \"\"\"\n    Common factor for all requests that need to iterate over messages.\n    \"\"\"\n    async def _init(\n            self, entity, offset_id, min_id, max_id,\n            from_user, offset_date, add_offset, filter, search, reply_to,\n            scheduled\n    ):\n        # Note that entity being `None` will perform a global search.\n        if entity:\n            self.entity = await self.client.get_input_entity(entity)\n        else:\n            self.entity = None\n            if self.reverse:\n                raise ValueError('Cannot reverse global search')\n\n        # Telegram doesn't like min_id/max_id. If these IDs are low enough\n        # (starting from last_id - 100), the request will return nothing.\n        #\n        # We can emulate their behaviour locally by setting offset = max_id\n        # and simply stopping once we hit a message with ID <= min_id.\n        if self.reverse:\n            offset_id = max(offset_id, min_id)\n            if offset_id and max_id:\n                if max_id - offset_id <= 1:\n                    raise StopAsyncIteration\n\n            if not max_id:\n                max_id = float('inf')\n        else:\n            offset_id = max(offset_id, max_id)\n            if offset_id and min_id:\n                if offset_id - min_id <= 1:\n                    raise StopAsyncIteration\n\n        if self.reverse:\n            if offset_id:\n                offset_id += 1\n            elif not offset_date:\n                # offset_id has priority over offset_date, so don't\n                # set offset_id to 1 if we want to offset by date.\n                offset_id = 1\n\n        if from_user:\n            from_user = await self.client.get_input_entity(from_user)\n            self.from_id = await self.client.get_peer_id(from_user)\n        else:\n            self.from_id = None\n\n        # `messages.searchGlobal` only works with text `search` or `filter` queries.\n        # If we want to perform global a search with `from_user` we have to perform\n        # a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`.\n        if not self.entity and from_user:\n            self.entity = types.InputPeerEmpty()\n\n        if filter is None:\n            filter = types.InputMessagesFilterEmpty()\n        else:\n            filter = filter() if isinstance(filter, type) else filter\n\n        if not self.entity:\n            self.request = functions.messages.SearchGlobalRequest(\n                q=search or '',\n                filter=filter,\n                min_date=None,\n                max_date=offset_date,\n                offset_rate=0,\n                offset_peer=types.InputPeerEmpty(),\n                offset_id=offset_id,\n                limit=1\n            )\n        elif scheduled:\n            self.request = functions.messages.GetScheduledHistoryRequest(\n                peer=entity,\n                hash=0\n            )\n        elif reply_to is not None:\n            self.request = functions.messages.GetRepliesRequest(\n                peer=self.entity,\n                msg_id=reply_to,\n                offset_id=offset_id,\n                offset_date=offset_date,\n                add_offset=add_offset,\n                limit=1,\n                max_id=0,\n                min_id=0,\n                hash=0\n            )\n        elif search is not None or not isinstance(filter, types.InputMessagesFilterEmpty) or from_user:\n            # Telegram completely ignores `from_id` in private chats\n            ty = helpers._entity_type(self.entity)\n            if ty == helpers._EntityType.USER:\n                # Don't bother sending `from_user` (it's ignored anyway),\n                # but keep `from_id` defined above to check it locally.\n                from_user = None\n            else:\n                # Do send `from_user` to do the filtering server-side,\n                # and set `from_id` to None to avoid checking it locally.\n                self.from_id = None\n\n            self.request = functions.messages.SearchRequest(\n                peer=self.entity,\n                q=search or '',\n                filter=filter,\n                min_date=None,\n                max_date=offset_date,\n                offset_id=offset_id,\n                add_offset=add_offset,\n                limit=0,  # Search actually returns 0 items if we ask it to\n                max_id=0,\n                min_id=0,\n                hash=0,\n                from_id=from_user\n            )\n\n            # Workaround issue #1124 until a better solution is found.\n            # Telegram seemingly ignores `max_date` if `filter` (and\n            # nothing else) is specified, so we have to rely on doing\n            # a first request to offset from the ID instead.\n            #\n            # Even better, using `filter` and `from_id` seems to always\n            # trigger `RPC_CALL_FAIL` which is \"internal issues\"...\n            if not isinstance(filter, types.InputMessagesFilterEmpty) \\\n                    and offset_date and not search and not offset_id:\n                async for m in self.client.iter_messages(\n                        self.entity, 1, offset_date=offset_date):\n                    self.request.offset_id = m.id + 1\n        else:\n            self.request = functions.messages.GetHistoryRequest(\n                peer=self.entity,\n                limit=1,\n                offset_date=offset_date,\n                offset_id=offset_id,\n                min_id=0,\n                max_id=0,\n                add_offset=add_offset,\n                hash=0\n            )\n\n        if self.limit <= 0:\n            # No messages, but we still need to know the total message count\n            result = await self.client(self.request)\n            if isinstance(result, types.messages.MessagesNotModified):\n                self.total = result.count\n            else:\n                self.total = getattr(result, 'count', len(result.messages))\n            raise StopAsyncIteration\n\n        if self.wait_time is None:\n            self.wait_time = 1 if self.limit > 3000 else 0\n\n        # When going in reverse we need an offset of `-limit`, but we\n        # also want to respect what the user passed, so add them together.\n        if self.reverse and hasattr(self.request, 'add_offset'):\n            self.request.add_offset -= _MAX_CHUNK_SIZE\n\n        self.add_offset = add_offset\n        self.max_id = max_id\n        self.min_id = min_id\n        self.last_id = 0 if self.reverse else float('inf')\n\n    async def _load_next_chunk(self):\n        if hasattr(self.request, 'limit'):\n            self.request.limit = min(self.left, _MAX_CHUNK_SIZE)\n            if self.reverse and self.request.limit != _MAX_CHUNK_SIZE:\n                # Remember that we need -limit when going in reverse\n                self.request.add_offset = self.add_offset - self.request.limit\n\n        r = await self.client(self.request)\n        self.total = getattr(r, 'count', len(r.messages))\n\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(r.users, r.chats)}\n\n        messages = reversed(r.messages) if self.reverse else r.messages\n        for message in messages:\n            if (isinstance(message, types.MessageEmpty)\n                    or self.from_id and message.sender_id != self.from_id):\n                continue\n\n            if not self._message_in_range(message):\n                return True\n\n            # There has been reports that on bad connections this method\n            # was returning duplicated IDs sometimes. Using ``last_id``\n            # is an attempt to avoid these duplicates, since the message\n            # IDs are returned in descending order (or asc if reverse).\n            self.last_id = message.id\n            message._finish_init(self.client, entities, self.entity)\n            self.buffer.append(message)\n\n        # Not a slice (using offset would return the same, with e.g. SearchGlobal).\n        if isinstance(r, types.messages.Messages) or not hasattr(self.request, 'limit'):\n            return True\n\n        # Some channels are \"buggy\" and may return less messages than\n        # requested (apparently, the messages excluded are, for example,\n        # \"not displayable due to local laws\").\n        #\n        # This means it's not safe to rely on `len(r.messages) < req.limit` as\n        # the stop condition. Unfortunately more requests must be made.\n        #\n        # However we can still check if the highest ID is equal to or lower\n        # than the limit, in which case there won't be any more messages\n        # because the lowest message ID is 1.\n        #\n        # We also assume the API will always return, at least, one message if\n        # there is more to fetch.\n        if not r.messages or (not self.reverse and r.messages[0].id <= self.request.limit):\n            return True\n\n        # Get the last message that's not empty (in some rare cases\n        # it can happen that the last message is :tl:`MessageEmpty`)\n        if self.buffer:\n            self._update_offset(self.buffer[-1], r)\n        else:\n            # There are some cases where all the messages we get start\n            # being empty. This can happen on migrated mega-groups if\n            # the history was cleared, and we're using search. Telegram\n            # acts incredibly weird sometimes. Messages are returned but\n            # only \"empty\", not their contents. If this is the case we\n            # should just give up since there won't be any new Message.\n            return True\n\n    def _message_in_range(self, message):\n        \"\"\"\n        Determine whether the given message is in the range or\n        it should be ignored (and avoid loading more chunks).\n        \"\"\"\n        # No entity means message IDs between chats may vary\n        if self.entity:\n            if self.reverse:\n                if message.id <= self.last_id or message.id >= self.max_id:\n                    return False\n            else:\n                if message.id >= self.last_id or message.id <= self.min_id:\n                    return False\n\n        return True\n\n    def _update_offset(self, last_message, response):\n        \"\"\"\n        After making the request, update its offset with the last message.\n        \"\"\"\n        self.request.offset_id = last_message.id\n        if self.reverse:\n            # We want to skip the one we already have\n            self.request.offset_id += 1\n\n        if isinstance(self.request, functions.messages.SearchRequest):\n            # Unlike getHistory and searchGlobal that use *offset* date,\n            # this is *max* date. This means that doing a search in reverse\n            # will break it. Since it's not really needed once we're going\n            # (only for the first request), it's safe to just clear it off.\n            self.request.max_date = None\n        else:\n            # getHistory, searchGlobal and getReplies call it offset_date\n            self.request.offset_date = last_message.date\n\n        if isinstance(self.request, functions.messages.SearchGlobalRequest):\n            if last_message.input_chat:\n                self.request.offset_peer = last_message.input_chat\n            else:\n                self.request.offset_peer = types.InputPeerEmpty()\n\n            self.request.offset_rate = getattr(response, 'next_rate', 0)\n\n\nclass _IDsIter(RequestIter):\n    async def _init(self, entity, ids):\n        self.total = len(ids)\n        self._ids = list(reversed(ids)) if self.reverse else ids\n        self._offset = 0\n        self._entity = (await self.client.get_input_entity(entity)) if entity else None\n        self._ty = helpers._entity_type(self._entity) if self._entity else None\n\n        # 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.)\n        if self.wait_time is None:\n            self.wait_time = 10 if self.limit > 300 else 0\n\n    async def _load_next_chunk(self):\n        ids = self._ids[self._offset:self._offset + _MAX_CHUNK_SIZE]\n        if not ids:\n            raise StopAsyncIteration\n\n        self._offset += _MAX_CHUNK_SIZE\n\n        from_id = None  # By default, no need to validate from_id\n        if self._ty == helpers._EntityType.CHANNEL:\n            try:\n                r = await self.client(\n                    functions.channels.GetMessagesRequest(self._entity, ids))\n            except errors.MessageIdsEmptyError:\n                # All IDs were invalid, use a dummy result\n                r = types.messages.MessagesNotModified(len(ids))\n        else:\n            r = await self.client(functions.messages.GetMessagesRequest(ids))\n            if self._entity:\n                from_id = await self.client._get_peer(self._entity)\n\n        if isinstance(r, types.messages.MessagesNotModified):\n            self.buffer.extend(None for _ in ids)\n            return\n\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(r.users, r.chats)}\n\n        # Telegram seems to return the messages in the order in which\n        # we asked them for, so we don't need to check it ourselves,\n        # unless some messages were invalid in which case Telegram\n        # may decide to not send them at all.\n        #\n        # The passed message IDs may not belong to the desired entity\n        # since the user can enter arbitrary numbers which can belong to\n        # arbitrary chats. Validate these unless ``from_id is None``.\n        for message in r.messages:\n            if isinstance(message, types.MessageEmpty) or (\n                    from_id and message.peer_id != from_id):\n                self.buffer.append(None)\n            else:\n                message._finish_init(self.client, entities, self._entity)\n                self.buffer.append(message)\n\n\nclass MessageMethods:\n\n    # region Public methods\n\n    # region Message retrieval\n\n    def iter_messages(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            limit: float = None,\n            *,\n            offset_date: 'hints.DateLike' = None,\n            offset_id: int = 0,\n            max_id: int = 0,\n            min_id: int = 0,\n            add_offset: int = 0,\n            search: str = None,\n            filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None,\n            from_user: 'hints.EntityLike' = None,\n            wait_time: float = None,\n            ids: 'typing.Union[int, typing.Sequence[int]]' = None,\n            reverse: bool = False,\n            reply_to: int = None,\n            scheduled: bool = False\n    ) -> 'typing.Union[_MessagesIter, _IDsIter]':\n        \"\"\"\n        Iterator over the messages for the given chat.\n\n        The default order is from newest to oldest, but this\n        behaviour can be changed with the `reverse` parameter.\n\n        If either `search`, `filter` or `from_user` are provided,\n        :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`.\n\n        .. note::\n\n            Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to\n            be around 30 seconds per 10 requests, therefore a sleep of 1\n            second is the default for this limit (or above).\n\n        Arguments\n            entity (`entity`):\n                The entity from whom to retrieve the message history.\n\n                It may be `None` to perform a global search, or\n                to get messages by their ID from no particular chat.\n                Note that some of the offsets will not work if this\n                is the case.\n\n                Note that if you want to perform a global search,\n                you **must** set a non-empty `search` string, a `filter`.\n                or `from_user`.\n\n            limit (`int` | `None`, optional):\n                Number of messages to be retrieved. Due to limitations with\n                the API retrieving more than 3000 messages will take longer\n                than half a minute (or even more based on previous calls).\n\n                The limit may also be `None`, which would eventually return\n                the whole history.\n\n            offset_date (`datetime`):\n                Offset date (messages *previous* to this date will be\n                retrieved). Exclusive.\n\n            offset_id (`int`):\n                Offset message ID (only messages *previous* to the given\n                ID will be retrieved). Exclusive.\n\n            max_id (`int`):\n                All the messages with a higher (newer) ID or equal to this will\n                be excluded.\n\n            min_id (`int`):\n                All the messages with a lower (older) ID or equal to this will\n                be excluded.\n\n            add_offset (`int`):\n                Additional message offset (all of the specified offsets +\n                this offset = older messages).\n\n            search (`str`):\n                The string to be used as a search query.\n\n            filter (:tl:`MessagesFilter` | `type`):\n                The filter to use when returning messages. For instance,\n                :tl:`InputMessagesFilterPhotos` would yield only messages\n                containing photos.\n\n            from_user (`entity`):\n                Only messages from this entity will be returned.\n\n            wait_time (`int`):\n                Wait time (in seconds) between different\n                :tl:`GetHistoryRequest`. Use this parameter to avoid hitting\n                the ``FloodWaitError`` as needed. If left to `None`, it will\n                default to 1 second only if the limit is higher than 3000.\n\n                If the ``ids`` parameter is used, this time will default\n                to 10 seconds only if the amount of IDs is higher than 300.\n\n            ids (`int`, `list`):\n                A single integer ID (or several IDs) for the message that\n                should be returned. This parameter takes precedence over\n                the rest (which will be ignored if this is set). This can\n                for instance be used to get the message with ID 123 from\n                a channel. Note that if the message doesn't exist, `None`\n                will appear in its place, so that zipping the list of IDs\n                with the messages can match one-to-one.\n\n                .. note::\n\n                    At the time of writing, Telegram will **not** return\n                    :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that\n                    failed (i.e. the message is not replying to any, or is\n                    replying to a deleted message). This means that it is\n                    **not** possible to match messages one-by-one, so be\n                    careful if you use non-integers in this parameter.\n\n            reverse (`bool`, optional):\n                If set to `True`, the messages will be returned in reverse\n                order (from oldest to newest, instead of the default newest\n                to oldest). This also means that the meaning of `offset_id`\n                and `offset_date` parameters is reversed, although they will\n                still be exclusive. `min_id` becomes equivalent to `offset_id`\n                instead of being `max_id` as well since messages are returned\n                in ascending order.\n\n                You cannot use this if both `entity` and `ids` are `None`.\n\n            reply_to (`int`, optional):\n                If set to a message ID, the messages that reply to this ID\n                will be returned. This feature is also known as comments in\n                posts of broadcast channels, or viewing threads in groups.\n\n                This feature can only be used in broadcast channels and their\n                linked megagroups. Using it in a chat or private conversation\n                will result in ``telethon.errors.PeerIdInvalidError`` to occur.\n\n                When using this parameter, the ``filter`` and ``search``\n                parameters have no effect, since Telegram's API doesn't\n                support searching messages in replies.\n\n                .. note::\n\n                    This feature is used to get replies to a message in the\n                    *discussion* group. If the same broadcast channel sends\n                    a message and replies to it itself, that reply will not\n                    be included in the results.\n\n            scheduled (`bool`, optional):\n                If set to `True`, messages which are scheduled will be returned.\n                All other parameter will be ignored for this, except `entity`.\n\n        Yields\n            Instances of `Message <telethon.tl.custom.message.Message>`.\n\n        Example\n            .. code-block:: python\n\n                # From most-recent to oldest\n                async for message in client.iter_messages(chat):\n                    print(message.id, message.text)\n\n                # From oldest to most-recent\n                async for message in client.iter_messages(chat, reverse=True):\n                    print(message.id, message.text)\n\n                # Filter by sender\n                async for message in client.iter_messages(chat, from_user='me'):\n                    print(message.text)\n\n                # Server-side search with fuzzy text\n                async for message in client.iter_messages(chat, search='hello'):\n                    print(message.id)\n\n                # Filter by message type:\n                from telethon.tl.types import InputMessagesFilterPhotos\n                async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos):\n                    print(message.photo)\n\n                # Getting comments from a post in a channel:\n                async for message in client.iter_messages(channel, reply_to=123):\n                    print(message.chat.title, message.text)\n        \"\"\"\n        if ids is not None:\n            if not utils.is_list_like(ids):\n                ids = [ids]\n\n            return _IDsIter(\n                client=self,\n                reverse=reverse,\n                wait_time=wait_time,\n                limit=len(ids),\n                entity=entity,\n                ids=ids\n            )\n\n        return _MessagesIter(\n            client=self,\n            reverse=reverse,\n            wait_time=wait_time,\n            limit=limit,\n            entity=entity,\n            offset_id=offset_id,\n            min_id=min_id,\n            max_id=max_id,\n            from_user=from_user,\n            offset_date=offset_date,\n            add_offset=add_offset,\n            filter=filter,\n            search=search,\n            reply_to=reply_to,\n            scheduled=scheduled\n        )\n\n    async def get_messages(\n            self: 'TelegramClient', *args, **kwargs\n    ) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]:\n        \"\"\"\n        Same as `iter_messages()`, but returns a\n        `TotalList <telethon.helpers.TotalList>` instead.\n\n        If the `limit` is not set, it will be 1 by default unless both\n        `min_id` **and** `max_id` are set (as *named* arguments), in\n        which case the entire range will be returned.\n\n        This is so because any integer limit would be rather arbitrary and\n        it's common to only want to fetch one message, but if a range is\n        specified it makes sense that it should return the entirety of it.\n\n        If `ids` is present in the *named* arguments and is not a list,\n        a single `Message <telethon.tl.custom.message.Message>` will be\n        returned for convenience instead of a list.\n\n        Example\n            .. code-block:: python\n\n                # Get 0 photos and print the total to show how many photos there are\n                from telethon.tl.types import InputMessagesFilterPhotos\n                photos = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos)\n                print(photos.total)\n\n                # Get all the photos\n                photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos)\n\n                # Get messages by ID:\n                message_1337 = await client.get_messages(chat, ids=1337)\n        \"\"\"\n        if len(args) == 1 and 'limit' not in kwargs:\n            if 'min_id' in kwargs and 'max_id' in kwargs:\n                kwargs['limit'] = None\n            else:\n                kwargs['limit'] = 1\n\n        it = self.iter_messages(*args, **kwargs)\n\n        ids = kwargs.get('ids')\n        if ids and not utils.is_list_like(ids):\n            async for message in it:\n                return message\n            else:\n                # Iterator exhausted = empty, to handle InputMessageReplyTo\n                return None\n\n        return await it.collect()\n\n    get_messages.__signature__ = inspect.signature(iter_messages)\n\n    # endregion\n\n    # region Message sending/editing/deleting\n\n    async def _get_comment_data(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'typing.Union[int, types.Message]'\n    ):\n        r = await self(functions.messages.GetDiscussionMessageRequest(\n            peer=entity,\n            msg_id=utils.get_message_id(message)\n        ))\n        m = min(r.messages, key=lambda msg: msg.id)\n        chat = next(c for c in r.chats if c.id == m.peer_id.channel_id)\n        return utils.get_input_peer(chat), m.id\n\n    async def send_message(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'hints.MessageLike' = '',\n            *,\n            reply_to: 'typing.Union[int, types.Message]' = None,\n            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,\n            parse_mode: typing.Optional[str] = (),\n            formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,\n            link_preview: bool = True,\n            file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None,\n            thumb: 'hints.FileLike' = None,\n            force_document: bool = False,\n            clear_draft: bool = False,\n            buttons: typing.Optional['hints.MarkupLike'] = None,\n            silent: bool = None,\n            background: bool = None,\n            supports_streaming: bool = False,\n            schedule: 'hints.DateLike' = None,\n            comment_to: 'typing.Union[int, types.Message]' = None,\n            nosound_video: bool = None,\n            send_as: typing.Optional['hints.EntityLike'] = None,\n            message_effect_id: typing.Optional[int] = None\n    ) -> 'types.Message':\n        \"\"\"\n        Sends a message to the specified user, chat or channel.\n\n        The default parse mode is the same as the official applications\n        (a custom flavour of markdown). ``**bold**, `code` or __italic__``\n        are available. In addition you can send ``[links](https://example.com)``\n        and ``[mentions](@username)`` (or using IDs like in the Bot API:\n        ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three\n        backticks.\n\n        Sending a ``/start`` command with a parameter (like ``?start=data``)\n        is also done through this method. Simply send ``'/start data'`` to\n        the bot.\n\n        See also `Message.respond() <telethon.tl.custom.message.Message.respond>`\n        and `Message.reply() <telethon.tl.custom.message.Message.reply>`.\n\n        Arguments\n            entity (`entity`):\n                To who will it be sent.\n\n            message (`str` | `Message <telethon.tl.custom.message.Message>`):\n                The message to be sent, or another message object to resend.\n\n                The maximum length for a message is 35,000 bytes or 4,096\n                characters. Longer messages will not be sliced automatically,\n                and you should slice them manually if the text to send is\n                longer than said length.\n\n            reply_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):\n                Whether to reply to a message or not. If an integer is provided,\n                it should be the ID of the message that it should reply to.\n\n            attributes (`list`, optional):\n                Optional attributes that override the inferred ones, like\n                :tl:`DocumentAttributeFilename` and so on.\n\n            parse_mode (`object`, optional):\n                See the `TelegramClient.parse_mode\n                <telethon.client.messageparse.MessageParseMethods.parse_mode>`\n                property for allowed values. Markdown parsing will be used by\n                default.\n\n            formatting_entities (`list`, optional):\n                A list of message formatting entities. When provided, the ``parse_mode`` is ignored.\n\n            link_preview (`bool`, optional):\n                Should the link preview be shown?\n\n            file (`file`, optional):\n                Sends a message with a file attached (e.g. a photo,\n                video, audio or document). The ``message`` may be empty.\n\n            thumb (`str` | `bytes` | `file`, optional):\n                Optional JPEG thumbnail (for documents). **Telegram will\n                ignore this parameter** unless you pass a ``.jpg`` file!\n                The file must also be small in dimensions and in disk size.\n                Successful thumbnails were files below 20kB and 320x320px.\n                Width/height and dimensions/size ratios may be important.\n                For Telegram to accept a thumbnail, you must provide the\n                dimensions of the underlying media through ``attributes=``\n                with :tl:`DocumentAttributeVideo` or by installing the\n                optional ``hachoir`` dependency.\n\n            force_document (`bool`, optional):\n                Whether to send the given file as a document or not.\n\n            clear_draft (`bool`, optional):\n                Whether the existing draft should be cleared or not.\n\n            buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):\n                The matrix (list of lists), row list or button to be shown\n                after sending the message. This parameter will only work if\n                you have signed in as a bot. You can also pass your own\n                :tl:`ReplyMarkup` here.\n\n                All the following limits apply together:\n\n                * There can be 100 buttons at most (any more are ignored).\n                * There can be 8 buttons per row at most (more are ignored).\n                * The maximum callback data per button is 64 bytes.\n                * The maximum data that can be embedded in total is just\n                  over 4KB, shared between inline callback data and text.\n\n            silent (`bool`, optional):\n                Whether the message should notify people in a broadcast\n                channel or not. Defaults to `False`, which means it will\n                notify them. Set it to `True` to alter this behaviour.\n\n            background (`bool`, optional):\n                Whether the message should be send in background.\n\n            supports_streaming (`bool`, optional):\n                Whether the sent video supports streaming or not. Note that\n                Telegram only recognizes as streamable some formats like MP4,\n                and others like AVI or MKV will not work. You should convert\n                these to MP4 before sending if you want them to be streamable.\n                Unsupported formats will result in ``VideoContentTypeError``.\n\n            schedule (`hints.DateLike`, optional):\n                If set, the message won't send immediately, and instead\n                it will be scheduled to be automatically sent at a later\n                time.\n\n            comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):\n                Similar to ``reply_to``, but replies in the linked group of a\n                broadcast channel instead (effectively leaving a \"comment to\"\n                the specified message).\n\n                This parameter takes precedence over ``reply_to``. If there is\n                no linked chat, `telethon.errors.sgIdInvalidError` is raised.\n\n            nosound_video (`bool`, optional):\n                Only applicable when sending a video file without an audio\n                track. If set to ``True``, the video will be displayed in\n                Telegram as a video. If set to ``False``, Telegram will attempt\n                to display the video as an animated gif. (It may still display\n                as a video due to other factors.) The value is ignored if set\n                on non-video files. This is set to ``True`` for albums, as gifs\n                cannot be sent in albums.\n\n            send_as (`entity`):\n                Unique identifier (int) or username (str) of the chat or channel to send the message as.\n                You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.\n                Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,\n                This setting applies to the current message and will remain effective for future messages unless explicitly changed.\n                To set this behavior permanently for all messages, use SaveDefaultSendAs.\n\n            message_effect_id (`int`, optional):\n                Unique identifier of the message effect to be added to the message; for private chats only\n\n        Returns\n            The sent `custom.Message <telethon.tl.custom.message.Message>`.\n\n        Example\n            .. code-block:: python\n\n                # Markdown is the default\n                await client.send_message('me', 'Hello **world**!')\n\n                # Default to another parse mode\n                client.parse_mode = 'html'\n\n                await client.send_message('me', 'Some <b>bold</b> and <i>italic</i> text')\n                await client.send_message('me', 'An <a href=\"https://example.com\">URL</a>')\n                # code and pre tags also work, but those break the documentation :)\n                await client.send_message('me', '<a href=\"tg://user?id=me\">Mentions</a>')\n\n                # Explicit parse mode\n                # No parse mode by default\n                client.parse_mode = None\n\n                # ...but here I want markdown\n                await client.send_message('me', 'Hello, **world**!', parse_mode='md')\n\n                # ...and here I need HTML\n                await client.send_message('me', 'Hello, <i>world</i>!', parse_mode='html')\n\n                # If you logged in as a bot account, you can send buttons\n                from telethon import events, Button\n\n                @client.on(events.CallbackQuery)\n                async def callback(event):\n                    await event.edit('Thank you for clicking {}!'.format(event.data))\n\n                # Single inline button\n                await client.send_message(chat, 'A single button, with \"clk1\" as data',\n                                          buttons=Button.inline('Click me', b'clk1'))\n\n                # Matrix of inline buttons\n                await client.send_message(chat, 'Pick one from this grid', buttons=[\n                    [Button.inline('Left'), Button.inline('Right')],\n                    [Button.url('Check this site!', 'https://example.com')]\n                ])\n\n                # Reply keyboard\n                await client.send_message(chat, 'Welcome', buttons=[\n                    Button.text('Thanks!', resize=True, single_use=True),\n                    Button.request_phone('Send phone'),\n                    Button.request_location('Send location')\n                ])\n\n                # Forcing replies or clearing buttons.\n                await client.send_message(chat, 'Reply to me', buttons=Button.force_reply())\n                await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear())\n\n                # Scheduling a message to be sent after 5 minutes\n                from datetime import timedelta\n                await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5))\n        \"\"\"\n        if file is not None:\n            if isinstance(message, types.Message):\n                formatting_entities = formatting_entities or message.entities\n                message = message.message\n            return await self.send_file(\n                entity, file, caption=message, reply_to=reply_to,\n                attributes=attributes, parse_mode=parse_mode,\n                force_document=force_document, thumb=thumb,\n                buttons=buttons, clear_draft=clear_draft, silent=silent,\n                schedule=schedule, supports_streaming=supports_streaming,\n                formatting_entities=formatting_entities,\n                comment_to=comment_to, background=background,\n                nosound_video=nosound_video,\n                send_as=send_as, message_effect_id=message_effect_id\n            )\n\n        entity = await self.get_input_entity(entity)\n        if comment_to is not None:\n            entity, reply_to = await self._get_comment_data(entity, comment_to)\n        else:\n            reply_to = utils.get_message_id(reply_to)\n\n        if isinstance(message, types.Message):\n            if buttons is None:\n                markup = message.reply_markup\n            else:\n                markup = self.build_reply_markup(buttons)\n\n            if silent is None:\n                silent = message.silent\n\n            if (message.media and not isinstance(\n                    message.media, types.MessageMediaWebPage)):\n                return await self.send_file(\n                    entity,\n                    message.media,\n                    caption=message.message,\n                    silent=silent,\n                    background=background,\n                    reply_to=reply_to,\n                    buttons=markup,\n                    formatting_entities=message.entities,\n                    parse_mode=None,  # explicitly disable parse_mode to force using even empty formatting_entities\n                    schedule=schedule,\n                    send_as=send_as, message_effect_id=message_effect_id\n                )\n\n            request = functions.messages.SendMessageRequest(\n                peer=entity,\n                message=message.message or '',\n                silent=silent,\n                background=background,\n                reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),\n                reply_markup=markup,\n                entities=message.entities,\n                clear_draft=clear_draft,\n                no_webpage=not isinstance(\n                    message.media, types.MessageMediaWebPage),\n                schedule_date=schedule,\n                send_as=await self.get_input_entity(send_as) if send_as else None,\n                effect=message_effect_id\n            )\n            message = message.message\n        else:\n            if formatting_entities is None:\n                message, formatting_entities = await self._parse_message_text(message, parse_mode)\n            if not message:\n                raise ValueError(\n                    'The message cannot be empty unless a file is provided'\n                )\n\n            request = functions.messages.SendMessageRequest(\n                peer=entity,\n                message=message,\n                entities=formatting_entities,\n                no_webpage=not link_preview,\n                reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),\n                clear_draft=clear_draft,\n                silent=silent,\n                background=background,\n                reply_markup=self.build_reply_markup(buttons),\n                schedule_date=schedule,\n                send_as=await self.get_input_entity(send_as) if send_as else None,\n                effect=message_effect_id\n            )\n\n        result = await self(request)\n        if isinstance(result, types.UpdateShortSentMessage):\n            message = types.Message(\n                id=result.id,\n                peer_id=await self._get_peer(entity),\n                message=message,\n                date=result.date,\n                out=result.out,\n                media=result.media,\n                entities=result.entities,\n                reply_markup=request.reply_markup,\n                ttl_period=result.ttl_period,\n                reply_to=request.reply_to\n            )\n            message._finish_init(self, {}, entity)\n            return message\n\n        return self._get_response_message(request, result, entity)\n\n    async def forward_messages(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',\n            from_peer: 'hints.EntityLike' = None,\n            *,\n            background: bool = None,\n            with_my_score: bool = None,\n            silent: bool = None,\n            as_album: bool = None,\n            schedule: 'hints.DateLike' = None,\n            drop_author: bool = None,\n            drop_media_captions: bool = None,\n    ) -> 'typing.Sequence[types.Message]':\n        \"\"\"\n        Forwards the given messages to the specified entity.\n\n        If you want to \"forward\" a message without the forward header\n        (the \"forwarded from\" text), you should use `send_message` with\n        the original message instead. This will send a copy of it.\n\n        See also `Message.forward_to() <telethon.tl.custom.message.Message.forward_to>`.\n\n        Arguments\n            entity (`entity`):\n                To which entity the message(s) will be forwarded.\n\n            messages (`list` | `int` | `Message <telethon.tl.custom.message.Message>`):\n                The message(s) to forward, or their integer IDs.\n\n            from_peer (`entity`):\n                If the given messages are integer IDs and not instances\n                of the ``Message`` class, this *must* be specified in\n                order for the forward to work. This parameter indicates\n                the entity from which the messages should be forwarded.\n\n            silent (`bool`, optional):\n                Whether the message should notify people with sound or not.\n                Defaults to `False` (send with a notification sound unless\n                the person has the chat muted). Set it to `True` to alter\n                this behaviour.\n\n            background (`bool`, optional):\n                Whether the message should be forwarded in background.\n\n            with_my_score (`bool`, optional):\n                Whether forwarded should contain your game score.\n\n            as_album (`bool`, optional):\n                This flag no longer has any effect.\n\n            schedule (`hints.DateLike`, optional):\n                If set, the message(s) won't forward immediately, and\n                instead they will be scheduled to be automatically sent\n                at a later time.\n\n            drop_author (`bool`, optional):\n                Whether to forward messages without quoting the original author.\n\n            drop_media_captions (`bool`, optional):\n                Whether to strip captions from media. Setting this to `True` requires that `drop_author` also be set to `True`.\n\n        Returns\n            The list of forwarded `Message <telethon.tl.custom.message.Message>`,\n            or a single one if a list wasn't provided as input.\n\n            Note that if all messages are invalid (i.e. deleted) the call\n            will fail with ``MessageIdInvalidError``. If only some are\n            invalid, the list will have `None` instead of those messages.\n\n        Example\n            .. code-block:: python\n\n                # a single one\n                await client.forward_messages(chat, message)\n                # or\n                await client.forward_messages(chat, message_id, from_chat)\n                # or\n                await message.forward_to(chat)\n\n                # multiple\n                await client.forward_messages(chat, messages)\n                # or\n                await client.forward_messages(chat, message_ids, from_chat)\n\n                # Forwarding as a copy\n                await client.send_message(chat, message)\n        \"\"\"\n        if as_album is not None:\n            warnings.warn('the as_album argument is deprecated and no longer has any effect')\n\n        single = not utils.is_list_like(messages)\n        if single:\n            messages = (messages,)\n\n        entity = await self.get_input_entity(entity)\n\n        if from_peer:\n            from_peer = await self.get_input_entity(from_peer)\n            from_peer_id = await self.get_peer_id(from_peer)\n        else:\n            from_peer_id = None\n\n        def get_key(m):\n            if isinstance(m, int):\n                if from_peer_id is not None:\n                    return from_peer_id\n\n                raise ValueError('from_peer must be given if integer IDs are used')\n            elif isinstance(m, types.Message):\n                return m.chat_id\n            else:\n                raise TypeError('Cannot forward messages of type {}'.format(type(m)))\n\n        sent = []\n        for _chat_id, chunk in itertools.groupby(messages, key=get_key):\n            chunk = list(chunk)\n            if isinstance(chunk[0], int):\n                chat = from_peer\n            else:\n                chat = from_peer or await self.get_input_entity(chunk[0].peer_id)\n                chunk = [m.id for m in chunk]\n\n            req = functions.messages.ForwardMessagesRequest(\n                from_peer=chat,\n                id=chunk,\n                to_peer=entity,\n                silent=silent,\n                background=background,\n                with_my_score=with_my_score,\n                schedule_date=schedule,\n                drop_author=drop_author,\n                drop_media_captions=drop_media_captions\n            )\n            result = await self(req)\n            sent.extend(self._get_response_message(req, result, entity))\n\n        return sent[0] if single else sent\n\n    async def edit_message(\n            self: 'TelegramClient',\n            entity: 'typing.Union[hints.EntityLike, types.Message]',\n            message: 'typing.Union[int, types.Message, types.InputMessageID, str]' = None,\n            text: str = None,\n            *,\n            parse_mode: str = (),\n            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,\n            formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,\n            link_preview: bool = True,\n            file: 'hints.FileLike' = None,\n            thumb: 'hints.FileLike' = None,\n            force_document: bool = False,\n            buttons: typing.Optional['hints.MarkupLike'] = None,\n            supports_streaming: bool = False,\n            schedule: 'hints.DateLike' = None\n    ) -> 'types.Message':\n        \"\"\"\n        Edits the given message to change its text or media.\n\n        See also `Message.edit() <telethon.tl.custom.message.Message.edit>`.\n\n        Arguments\n            entity (`entity` | `Message <telethon.tl.custom.message.Message>`):\n                From which chat to edit the message. This can also be\n                the message to be edited, and the entity will be inferred\n                from it, so the next parameter will be assumed to be the\n                message text.\n\n                You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`,\n                which is the only way to edit messages that were sent\n                after the user selects an inline query result.\n\n            message (`int` | `Message <telethon.tl.custom.message.Message>` | :tl:`InputMessageID` | `str`):\n                The ID of the message (or `Message\n                <telethon.tl.custom.message.Message>` itself) to be edited.\n                If the `entity` was a `Message\n                <telethon.tl.custom.message.Message>`, then this message\n                will be treated as the new text.\n\n            text (`str`, optional):\n                The new text of the message. Does nothing if the `entity`\n                was a `Message <telethon.tl.custom.message.Message>`.\n\n            parse_mode (`object`, optional):\n                See the `TelegramClient.parse_mode\n                <telethon.client.messageparse.MessageParseMethods.parse_mode>`\n                property for allowed values. Markdown parsing will be used by\n                default.\n\n            attributes (`list`, optional):\n                Optional attributes that override the inferred ones, like\n                :tl:`DocumentAttributeFilename` and so on.\n\n            formatting_entities (`list`, optional):\n                A list of message formatting entities. When provided, the ``parse_mode`` is ignored.\n\n            link_preview (`bool`, optional):\n                Should the link preview be shown?\n\n            file (`str` | `bytes` | `file` | `media`, optional):\n                The file object that should replace the existing media\n                in the message.\n\n            thumb (`str` | `bytes` | `file`, optional):\n                Optional JPEG thumbnail (for documents). **Telegram will\n                ignore this parameter** unless you pass a ``.jpg`` file!\n                The file must also be small in dimensions and in disk size.\n                Successful thumbnails were files below 20kB and 320x320px.\n                Width/height and dimensions/size ratios may be important.\n                For Telegram to accept a thumbnail, you must provide the\n                dimensions of the underlying media through ``attributes=``\n                with :tl:`DocumentAttributeVideo` or by installing the\n                optional ``hachoir`` dependency.\n\n            force_document (`bool`, optional):\n                Whether to send the given file as a document or not.\n\n            buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):\n                The matrix (list of lists), row list or button to be shown\n                after sending the message. This parameter will only work if\n                you have signed in as a bot. You can also pass your own\n                :tl:`ReplyMarkup` here.\n\n            supports_streaming (`bool`, optional):\n                Whether the sent video supports streaming or not. Note that\n                Telegram only recognizes as streamable some formats like MP4,\n                and others like AVI or MKV will not work. You should convert\n                these to MP4 before sending if you want them to be streamable.\n                Unsupported formats will result in ``VideoContentTypeError``.\n\n            schedule (`hints.DateLike`, optional):\n                If set, the message won't be edited immediately, and instead\n                it will be scheduled to be automatically edited at a later\n                time.\n\n                Note that this parameter will have no effect if you are\n                trying to edit a message that was sent via inline bots.\n\n        Returns\n            The edited `Message <telethon.tl.custom.message.Message>`,\n            unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which\n            case this method returns a boolean.\n\n        Raises\n            ``MessageAuthorRequiredError`` if you're not the author of the\n            message but tried editing it anyway.\n\n            ``MessageNotModifiedError`` if the contents of the message were\n            not modified at all.\n\n            ``MessageIdInvalidError`` if the ID of the message is invalid\n            (the ID itself may be correct, but the message with that ID\n            cannot be edited). For example, when trying to edit messages\n            with a reply markup (or clear markup) this error will be raised.\n\n        Example\n            .. code-block:: python\n\n                message = await client.send_message(chat, 'hello')\n\n                await client.edit_message(chat, message, 'hello!')\n                # or\n                await client.edit_message(chat, message.id, 'hello!!')\n                # or\n                await client.edit_message(message, 'hello!!!')\n        \"\"\"\n        if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):\n            text = text or message\n            message = entity\n        elif isinstance(entity, types.Message):\n            text = message  # Shift the parameters to the right\n            message = entity\n            entity = entity.peer_id\n\n        if formatting_entities is None:\n            text, formatting_entities = await self._parse_message_text(text, parse_mode)\n        file_handle, media, image = await self._file_to_media(file,\n                supports_streaming=supports_streaming,\n                thumb=thumb,\n                attributes=attributes,\n                force_document=force_document)\n\n        if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):\n            request = functions.messages.EditInlineBotMessageRequest(\n                id=entity,\n                message=text,\n                no_webpage=not link_preview,\n                entities=formatting_entities,\n                media=media,\n                reply_markup=self.build_reply_markup(buttons)\n            )\n            # Invoke `messages.editInlineBotMessage` from the right datacenter.\n            # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing.\n            exported = self.session.dc_id != entity.dc_id\n            if exported:\n                try:\n                    sender = await self._borrow_exported_sender(entity.dc_id)\n                    return await self._call(sender, request)\n                finally:\n                    await self._return_exported_sender(sender)\n            else:\n                return await self(request)\n\n        entity = await self.get_input_entity(entity)\n        request = functions.messages.EditMessageRequest(\n            peer=entity,\n            id=utils.get_message_id(message),\n            message=text,\n            no_webpage=not link_preview,\n            entities=formatting_entities,\n            media=media,\n            reply_markup=self.build_reply_markup(buttons),\n            schedule_date=schedule\n        )\n        msg = self._get_response_message(request, await self(request), entity)\n        return msg\n\n    async def delete_messages(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]',\n            *,\n            revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]':\n        \"\"\"\n        Deletes the given messages, optionally \"for everyone\".\n\n        See also `Message.delete() <telethon.tl.custom.message.Message.delete>`.\n\n        .. warning::\n\n            This method does **not** validate that the message IDs belong\n            to the chat that you passed! It's possible for the method to\n            delete messages from different private chats and small group\n            chats at once, so make sure to pass the right IDs.\n\n        Arguments\n            entity (`entity`):\n                From who the message will be deleted. This can actually\n                be `None` for normal chats, but **must** be present\n                for channels and megagroups.\n\n            message_ids (`list` | `int` | `Message <telethon.tl.custom.message.Message>`):\n                The IDs (or ID) or messages to be deleted.\n\n            revoke (`bool`, optional):\n                Whether the message should be deleted for everyone or not.\n                By default it has the opposite behaviour of official clients,\n                and it will delete the message for everyone.\n\n                `Since 24 March 2019\n                <https://telegram.org/blog/unsend-privacy-emoji>`_, you can\n                also revoke messages of any age (i.e. messages sent long in\n                the past) the *other* person sent in private conversations\n                (and of course your messages too).\n\n                Disabling this has no effect on channels or megagroups,\n                since it will unconditionally delete the message for everyone.\n\n        Returns\n            A list of :tl:`AffectedMessages`, each item being the result\n            for the delete calls of the messages in chunks of 100 each.\n\n        Example\n            .. code-block:: python\n\n                await client.delete_messages(chat, messages)\n        \"\"\"\n        if not utils.is_list_like(message_ids):\n            message_ids = (message_ids,)\n\n        message_ids = (\n            m.id if isinstance(m, (\n                types.Message, types.MessageService, types.MessageEmpty))\n            else int(m) for m in message_ids\n        )\n\n        if entity:\n            entity = await self.get_input_entity(entity)\n            ty = helpers._entity_type(entity)\n        else:\n            # no entity (None), set a value that's not a channel for private delete\n            ty = helpers._EntityType.USER\n\n        if ty == helpers._EntityType.CHANNEL:\n            return await self([functions.channels.DeleteMessagesRequest(\n                         entity, list(c)) for c in utils.chunks(message_ids)])\n        else:\n            return await self([functions.messages.DeleteMessagesRequest(\n                         list(c), revoke) for c in utils.chunks(message_ids)])\n\n    # endregion\n\n    # region Miscellaneous\n\n    async def send_read_acknowledge(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None,\n            *,\n            max_id: int = None,\n            clear_mentions: bool = False,\n            clear_reactions: bool = False) -> bool:\n        \"\"\"\n        Marks messages as read and optionally clears mentions.\n\n        This effectively marks a message as read (or more than one) in the\n        given conversation.\n\n        If neither message nor maximum ID are provided, all messages will be\n        marked as read by assuming that ``max_id = 0``.\n\n        If a message or maximum ID is provided, all the messages up to and\n        including such ID will be marked as read (for all messages whose ID\n        ≤ max_id).\n\n        See also `Message.mark_read() <telethon.tl.custom.message.Message.mark_read>`.\n\n        Arguments\n            entity (`entity`):\n                The chat where these messages are located.\n\n            message (`list` | `Message <telethon.tl.custom.message.Message>`):\n                Either a list of messages or a single message.\n\n            max_id (`int`):\n                Until which message should the read acknowledge be sent for.\n                This has priority over the ``message`` parameter.\n\n            clear_mentions (`bool`):\n                Whether the mention badge should be cleared (so that\n                there are no more mentions) or not for the given entity.\n\n                If no message is provided, this will be the only action\n                taken.\n\n            clear_reactions (`bool`):\n                Whether the reactions badge should be cleared (so that\n                there are no more reaction notifications) or not for the given entity.\n\n                If no message is provided, this will be the only action\n                taken.\n\n        Example\n            .. code-block:: python\n\n                # using a Message object\n                await client.send_read_acknowledge(chat, message)\n                # ...or using the int ID of a Message\n                await client.send_read_acknowledge(chat, message_id)\n                # ...or passing a list of messages to mark as read\n                await client.send_read_acknowledge(chat, messages)\n        \"\"\"\n        if max_id is None:\n            if not message:\n                max_id = 0\n            else:\n                if utils.is_list_like(message):\n                    max_id = max(msg.id for msg in message)\n                else:\n                    max_id = message.id\n\n        entity = await self.get_input_entity(entity)\n        if clear_mentions:\n            await self(functions.messages.ReadMentionsRequest(entity))\n            if max_id is None and not clear_reactions:\n                return True\n        if clear_reactions:\n            await self(functions.messages.ReadReactionsRequest(entity))\n            if max_id is None:\n                return True\n\n        if max_id is not None:\n            if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:\n                return await self(functions.channels.ReadHistoryRequest(\n                    utils.get_input_channel(entity), max_id=max_id))\n            else:\n                return await self(functions.messages.ReadHistoryRequest(\n                    entity, max_id=max_id))\n\n        return False\n\n    async def pin_message(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'typing.Optional[hints.MessageIDLike]',\n            *,\n            notify: bool = False,\n            pm_oneside: bool = False\n    ):\n        \"\"\"\n        Pins a message in a chat.\n\n        The default behaviour is to *not* notify members, unlike the\n        official applications.\n\n        See also `Message.pin() <telethon.tl.custom.message.Message.pin>`.\n\n        Arguments\n            entity (`entity`):\n                The chat where the message should be pinned.\n\n            message (`int` | `Message <telethon.tl.custom.message.Message>`):\n                The message or the message ID to pin. If it's\n                `None`, all messages will be unpinned instead.\n\n            notify (`bool`, optional):\n                Whether the pin should notify people or not.\n\n            pm_oneside (`bool`, optional):\n                Whether the message should be pinned for everyone or not.\n                By default it has the opposite behaviour of official clients,\n                and it will pin the message for both sides, in private chats.\n\n        Example\n            .. code-block:: python\n\n                # Send and pin a message to annoy everyone\n                message = await client.send_message(chat, 'Pinotifying is fun!')\n                await client.pin_message(chat, message, notify=True)\n        \"\"\"\n        return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside)\n\n    async def unpin_message(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            message: 'typing.Optional[hints.MessageIDLike]' = None,\n            *,\n            notify: bool = False\n    ):\n        \"\"\"\n        Unpins a message in a chat.\n\n        If no message ID is specified, all pinned messages will be unpinned.\n\n        See also `Message.unpin() <telethon.tl.custom.message.Message.unpin>`.\n\n        Arguments\n            entity (`entity`):\n                The chat where the message should be pinned.\n\n            message (`int` | `Message <telethon.tl.custom.message.Message>`):\n                The message or the message ID to unpin. If it's\n                `None`, all messages will be unpinned instead.\n\n        Example\n            .. code-block:: python\n\n                # Unpin all messages from a chat\n                await client.unpin_message(chat)\n        \"\"\"\n        return await self._pin(entity, message, unpin=True, notify=notify)\n\n    async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False):\n        message = utils.get_message_id(message) or 0\n        entity = await self.get_input_entity(entity)\n        if message <= 0:  # old behaviour accepted negative IDs to unpin\n            await self(functions.messages.UnpinAllMessagesRequest(entity))\n            return\n\n        request = functions.messages.UpdatePinnedMessageRequest(\n            peer=entity,\n            id=message,\n            silent=not notify,\n            unpin=unpin,\n            pm_oneside=pm_oneside\n        )\n        result = await self(request)\n\n        # Unpinning does not produce a service message.\n        # Pinning a message that was already pinned also produces no service message.\n        # Pinning a message in your own chat does not produce a service message,\n        # but pinning on a private conversation with someone else does.\n        if unpin or not result.updates:\n            return\n\n        # Pinning a message that doesn't exist would RPC-error earlier\n        return self._get_response_message(request, result, entity)\n\n    # endregion\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/telegrambaseclient.py",
    "content": "import abc\nimport inspect\nimport re\nimport asyncio\nimport collections\nimport logging\nimport platform\nimport time\nimport typing\nimport datetime\nimport pathlib\nimport warnings\n\nfrom .. import utils, version, helpers, __name__ as __base_name__\nfrom ..crypto import rsa\nfrom ..extensions import markdown\nfrom ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy\nfrom ..sessions import Session, SQLiteSession, MemorySession\nfrom ..tl import functions, types\nfrom ..tl.alltlobjects import LAYER\nfrom .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType\n\ntry:\n    import python_socks\nexcept ImportError:\n    python_socks = None\n\nDEFAULT_DC_ID = 2\nDEFAULT_IPV4_IP = '149.154.167.51'\nDEFAULT_IPV6_IP = '2001:67c:4e8:f002::a'\nDEFAULT_PORT = 443\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n_base_log = logging.getLogger(__base_name__)\n\n\n# In seconds, how long to wait before disconnecting a exported sender.\n_DISCONNECT_EXPORTED_AFTER = 60\n\n\nclass _ExportState:\n    def __init__(self):\n        # ``n`` is the amount of borrows a given sender has;\n        # once ``n`` reaches ``0``, disconnect the sender after a while.\n        self._n = 0\n        self._zero_ts = 0\n        self._connected = False\n\n    def add_borrow(self):\n        self._n += 1\n        self._connected = True\n\n    def add_return(self):\n        self._n -= 1\n        assert self._n >= 0, 'returned sender more than it was borrowed'\n        if self._n == 0:\n            self._zero_ts = time.time()\n\n    def should_disconnect(self):\n        return (self._n == 0\n                and self._connected\n                and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER)\n\n    def need_connect(self):\n        return not self._connected\n\n    def mark_disconnected(self):\n        assert self.should_disconnect(), 'marked as disconnected when it was borrowed'\n        self._connected = False\n\n\n# TODO How hard would it be to support both `trio` and `asyncio`?\nclass TelegramBaseClient(abc.ABC):\n    \"\"\"\n    This is the abstract base class for the client. It defines some\n    basic stuff like connecting, switching data center, etc, and\n    leaves the `__call__` unimplemented.\n\n    Arguments\n        session (`str` | `telethon.sessions.abstract.Session`, `None`):\n            The file name of the session file to be used if a string is\n            given (it may be a full path), or the Session instance to be\n            used otherwise. If it's `None`, the session will not be saved,\n            and you should call :meth:`.log_out()` when you're done.\n\n            Note that if you pass a string it will be a file in the current\n            working directory, although you can also pass absolute paths.\n\n            The session file contains enough information for you to login\n            without re-sending the code, so if you have to enter the code\n            more than once, maybe you're changing the working directory,\n            renaming or removing the file, or using random names.\n\n        api_id (`int` | `str`):\n            The API ID you obtained from https://my.telegram.org.\n\n        api_hash (`str`):\n            The API hash you obtained from https://my.telegram.org.\n\n        connection (`telethon.network.connection.common.Connection`, optional):\n            The connection instance to be used when creating a new connection\n            to the servers. It **must** be a type.\n\n            Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`.\n\n        use_ipv6 (`bool`, optional):\n            Whether to connect to the servers through IPv6 or not.\n            By default this is `False` as IPv6 support is not\n            too widespread yet.\n\n        proxy (`dict`, optional):\n            An iterable consisting of the proxy info. If `connection` is\n            one of `MTProxy`, then it should contain MTProxy credentials:\n            ``hostname, port, secret``. Otherwise, it's meant to store\n            function parameters for PySocks, like ``type, hostname, port``.\n            See https://github.com/romis2012/python-socks for more.\n\n        local_addr (`str` | `tuple`, optional):\n            Local host address (and port, optionally) used to bind the socket to locally.\n            You only need to use this if you have multiple network cards and\n            want to use a specific one.\n\n        timeout (`int` | `float`, optional):\n            The timeout in seconds to be used when connecting.\n            This is **not** the timeout to be used when ``await``'ing for\n            invoked requests, and you should use ``asyncio.wait`` or\n            ``asyncio.wait_for`` for that.\n\n        request_retries (`int` | `None`, optional):\n            How many times a request should be retried. Request are retried\n            when Telegram is having internal issues (due to either\n            ``errors.ServerError`` or ``errors.RpcCallFailError``),\n            when there is a ``errors.FloodWaitError`` less than\n            `flood_sleep_threshold`, or when there's a migrate error.\n\n            May take a negative or `None` value for infinite retries, but\n            this is not recommended, since some requests can always trigger\n            a call fail (such as searching for messages).\n\n        connection_retries (`int` | `None`, optional):\n            How many times the reconnection should retry, either on the\n            initial connection or when Telegram disconnects us. May be\n            set to a negative or `None` value for infinite retries, but\n            this is not recommended, since the program can get stuck in an\n            infinite loop.\n\n        retry_delay (`int` | `float`, optional):\n            The delay in seconds to sleep between automatic reconnections.\n\n        auto_reconnect (`bool`, optional):\n            Whether reconnection should be retried `connection_retries`\n            times automatically if Telegram disconnects us or not.\n\n        sequential_updates (`bool`, optional):\n            By default every incoming update will create a new task, so\n            you can handle several updates in parallel. Some scripts need\n            the order in which updates are processed to be sequential, and\n            this setting allows them to do so.\n\n            If set to `True`, incoming updates will be put in a queue\n            and processed sequentially. This means your event handlers\n            should *not* perform long-running operations since new\n            updates are put inside of an unbounded queue.\n\n        flood_sleep_threshold (`int` | `float`, optional):\n            The threshold below which the library should automatically\n            sleep on flood wait and slow mode wait errors (inclusive). For instance, if a\n            ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold`\n            is 20s, the library will ``sleep`` automatically. If the error\n            was for 21s, it would ``raise FloodWaitError`` instead. Values\n            larger than a day (like ``float('inf')``) will be changed to a day.\n\n        raise_last_call_error (`bool`, optional):\n            When API calls fail in a way that causes Telethon to retry\n            automatically, should the RPC error of the last attempt be raised\n            instead of a generic ValueError. This is mostly useful for\n            detecting when Telegram has internal issues.\n\n        device_model (`str`, optional):\n            \"Device model\" to be sent when creating the initial connection.\n            Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown.\n\n        system_version (`str`, optional):\n            \"System version\" to be sent when creating the initial connection.\n            Defaults to ``platform.uname().release`` stripped of everything ahead of -.\n\n        app_version (`str`, optional):\n            \"App version\" to be sent when creating the initial connection.\n            Defaults to `telethon.version.__version__`.\n\n        lang_code (`str`, optional):\n            \"Language code\" to be sent when creating the initial connection.\n            Defaults to ``'en'``.\n\n        system_lang_code (`str`, optional):\n            \"System lang code\"  to be sent when creating the initial connection.\n            Defaults to `lang_code`.\n\n        loop (`asyncio.AbstractEventLoop`, optional):\n            Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`.\n            This argument is ignored.\n\n        base_logger (`str` | `logging.Logger`, optional):\n            Base logger name or instance to use.\n            If a `str` is given, it'll be passed to `logging.getLogger()`. If a\n            `logging.Logger` is given, it'll be used directly. If something\n            else or nothing is given, the default logger will be used.\n\n        receive_updates (`bool`, optional):\n            Whether the client will receive updates or not. By default, updates\n            will be received from Telegram as they occur.\n\n            Turning this off means that Telegram will not send updates at all\n            so event handlers, conversations, and QR login will not work.\n            However, certain scripts don't need updates, so this will reduce\n            the amount of bandwidth used.\n\n        entity_cache_limit (`int`, optional):\n            How many users, chats and channels to keep in the in-memory cache\n            at most. This limit is checked against when processing updates.\n\n            When this limit is reached or exceeded, all entities that are not\n            required for update handling will be flushed to the session file.\n\n            Note that this implies that there is a lower bound to the amount\n            of entities that must be kept in memory.\n\n            Setting this limit too low will cause the library to attempt to\n            flush entities to the session file even if no entities can be\n            removed from the in-memory cache, which will degrade performance.\n    \"\"\"\n\n    # Current TelegramClient version\n    __version__ = version.__version__\n\n    # Cached server configuration (with .dc_options), can be \"global\"\n    _config = None\n    _cdn_config = None\n\n    # region Initialization\n\n    def __init__(\n            self: 'TelegramClient',\n            session: 'typing.Union[str, pathlib.Path, Session]',\n            api_id: int,\n            api_hash: str,\n            *,\n            connection: 'typing.Type[Connection]' = ConnectionTcpFull,\n            use_ipv6: bool = False,\n            proxy: typing.Union[tuple, dict] = None,\n            local_addr: typing.Union[str, tuple] = None,\n            timeout: int = 10,\n            request_retries: int = 5,\n            connection_retries: int = 5,\n            retry_delay: int = 1,\n            auto_reconnect: bool = True,\n            sequential_updates: bool = False,\n            flood_sleep_threshold: int = 60,\n            raise_last_call_error: bool = False,\n            device_model: str = None,\n            system_version: str = None,\n            app_version: str = None,\n            lang_code: str = 'en',\n            system_lang_code: str = 'en',\n            loop: asyncio.AbstractEventLoop = None,\n            base_logger: typing.Union[str, logging.Logger] = None,\n            receive_updates: bool = True,\n            catch_up: bool = False,\n            entity_cache_limit: int = 5000\n    ):\n        if not api_id or not api_hash:\n            raise ValueError(\n                \"Your API ID or Hash cannot be empty or None. \"\n                \"Refer to telethon.rtfd.io for more information.\")\n\n        self._use_ipv6 = use_ipv6\n\n        if isinstance(base_logger, str):\n            base_logger = logging.getLogger(base_logger)\n        elif not isinstance(base_logger, logging.Logger):\n            base_logger = _base_log\n\n        class _Loggers(dict):\n            def __missing__(self, key):\n                if key.startswith(\"telethon.\"):\n                    key = key.split('.', maxsplit=1)[1]\n\n                return base_logger.getChild(key)\n\n        self._log = _Loggers()\n\n        # Determine what session object we have\n        if isinstance(session, (str, pathlib.Path)):\n            try:\n                session = SQLiteSession(str(session))\n            except ImportError:\n                import warnings\n                warnings.warn(\n                    'The sqlite3 module is not available under this '\n                    'Python installation and no custom session '\n                    'instance was given; using MemorySession.\\n'\n                    'You will need to re-login every time unless '\n                    'you use another session storage'\n                )\n                session = MemorySession()\n        elif session is None:\n            session = MemorySession()\n        elif not isinstance(session, Session):\n            raise TypeError(\n                'The given session must be a str or a Session instance.'\n            )\n\n        self.flood_sleep_threshold = flood_sleep_threshold\n\n        # TODO Use AsyncClassWrapper(session)\n        # ChatGetter and SenderGetter can use the in-memory _mb_entity_cache\n        # to avoid network access and the need for await in session files.\n        #\n        # The session files only wants the entities to persist\n        # them to disk, and to save additional useful information.\n        # TODO Session should probably return all cached\n        #      info of entities, not just the input versions\n        self.session = session\n        self.api_id = int(api_id)\n        self.api_hash = api_hash\n\n        if local_addr is not None:\n            if use_ipv6 is False and ':' in local_addr:\n                raise TypeError(\n                    'A local IPv6 address must only be used with `use_ipv6=True`.'\n                )\n            elif use_ipv6 is True and ':' not in local_addr:\n                raise TypeError(\n                    '`use_ipv6=True` must only be used with a local IPv6 address.'\n                )\n\n        self._raise_last_call_error = raise_last_call_error\n\n        self._request_retries = request_retries\n        self._connection_retries = connection_retries\n        self._retry_delay = retry_delay or 0\n        self._proxy = proxy\n        self._local_addr = local_addr\n        self._timeout = timeout\n        self._auto_reconnect = auto_reconnect\n\n        if proxy and not python_socks:\n            warnings.warn('proxy argument will be ignored because python-socks is not installed')\n\n        assert isinstance(connection, type)\n        self._connection = connection\n        init_proxy = None if not issubclass(connection, TcpMTProxy) else \\\n            types.InputClientProxy(*connection.address_info(proxy))\n\n        # Used on connection. Capture the variables in a lambda since\n        # exporting clients need to create this InvokeWithLayerRequest.\n        system = platform.uname()\n\n        if system.machine in ('x86_64', 'AMD64'):\n            default_device_model = 'PC 64bit'\n        elif system.machine in ('i386','i686','x86'):\n            default_device_model = 'PC 32bit'\n        else:\n            default_device_model = system.machine\n        default_system_version = re.sub(r'-.+','',system.release)\n\n        self._init_request = functions.InitConnectionRequest(\n            api_id=self.api_id,\n            device_model=device_model or default_device_model or 'Unknown',\n            system_version=system_version or default_system_version or '1.0',\n            app_version=app_version or self.__version__,\n            lang_code=lang_code,\n            system_lang_code=system_lang_code,\n            lang_pack='',  # \"langPacks are for official apps only\"\n            query=None,\n            proxy=init_proxy\n        )\n\n        # Remember flood-waited requests to avoid making them again\n        self._flood_waited_requests = {}\n\n        # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders\n        self._borrowed_senders = {}\n        self._borrow_sender_lock = asyncio.Lock()\n        self._exported_sessions = {}\n\n        self._loop = None  # only used as a sanity check\n        self._updates_error = None\n        self._updates_handle = None\n        self._keepalive_handle = None\n        self._last_request = time.time()\n        self._no_updates = not receive_updates\n\n        # Used for non-sequential updates, in order to terminate all pending tasks on disconnect.\n        self._sequential_updates = sequential_updates\n        self._event_handler_tasks = set()\n\n        self._authorized = None  # None = unknown, False = no, True = yes\n\n        # Some further state for subclasses\n        self._event_builders = []\n\n        # {chat_id: {Conversation}}\n        self._conversations = collections.defaultdict(set)\n\n        # Hack to workaround the fact Telegram may send album updates as\n        # different Updates when being sent from a different data center.\n        # {grouped_id: AlbumHack}\n        #\n        # FIXME: We don't bother cleaning this up because it's not really\n        #        worth it, albums are pretty rare and this only holds them\n        #        for a second at most.\n        self._albums = {}\n\n        # Default parse mode\n        self._parse_mode = markdown\n\n        # Some fields to easy signing in. Let {phone: hash} be\n        # a dictionary because the user may change their mind.\n        self._phone_code_hash = {}\n        self._phone = None\n        self._tos = None\n\n        # A place to store if channels are a megagroup or not (see `edit_admin`)\n        self._megagroup_cache = {}\n\n        # This is backported from v2 in a very ad-hoc way just to get proper update handling\n        self._catch_up = catch_up\n        self._updates_queue = asyncio.Queue()\n        self._message_box = MessageBox(self._log['messagebox'])\n        self._mb_entity_cache = MbEntityCache()  # required for proper update handling (to know when to getDifference)\n        self._entity_cache_limit = entity_cache_limit\n\n        self._sender = MTProtoSender(\n            self.session.auth_key,\n            loggers=self._log,\n            retries=self._connection_retries,\n            delay=self._retry_delay,\n            auto_reconnect=self._auto_reconnect,\n            connect_timeout=self._timeout,\n            auth_key_callback=self._auth_key_callback,\n            updates_queue=self._updates_queue,\n            auto_reconnect_callback=self._handle_auto_reconnect\n        )\n\n\n    # endregion\n\n    # region Properties\n\n    @property\n    def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop:\n        \"\"\"\n        Property with the ``asyncio`` event loop used by this client.\n\n        Example\n            .. code-block:: python\n\n                # Download media in the background\n                task = client.loop.create_task(message.download_media())\n\n                # Do some work\n                ...\n\n                # Join the task (wait for it to complete)\n                await task\n        \"\"\"\n        return helpers.get_running_loop()\n\n    @property\n    def disconnected(self: 'TelegramClient') -> asyncio.Future:\n        \"\"\"\n        Property with a ``Future`` that resolves upon disconnection.\n\n        Example\n            .. code-block:: python\n\n                # Wait for a disconnection to occur\n                try:\n                    await client.disconnected\n                except OSError:\n                    print('Error on disconnect')\n        \"\"\"\n        return self._sender.disconnected\n\n    @property\n    def flood_sleep_threshold(self):\n        return self._flood_sleep_threshold\n\n    @flood_sleep_threshold.setter\n    def flood_sleep_threshold(self, value):\n        # None -> 0, negative values don't really matter\n        self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60)\n\n    # endregion\n\n    # region Connecting\n\n    async def connect(self: 'TelegramClient') -> None:\n        \"\"\"\n        Connects to Telegram.\n\n        .. note::\n\n            Connect means connect and nothing else, and only one low-level\n            request is made to notify Telegram about which layer we will be\n            using.\n\n            Before Telegram sends you updates, you need to make a high-level\n            request, like `client.get_me() <telethon.client.users.UserMethods.get_me>`,\n            as described in https://core.telegram.org/api/updates.\n\n        Example\n            .. code-block:: python\n\n                try:\n                    await client.connect()\n                except OSError:\n                    print('Failed to connect')\n        \"\"\"\n        if self.session is None:\n            raise ValueError('TelegramClient instance cannot be reused after logging out')\n\n        if self._loop is None:\n            self._loop = helpers.get_running_loop()\n        elif self._loop != helpers.get_running_loop():\n            raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')\n\n        # Current proxy implementation requires `sock_connect`, and some\n        # event loops lack this method. If the current loop is missing it,\n        # bail out early and suggest an alternative.\n        #\n        # TODO A better fix is obviously avoiding the use of `sock_connect`\n        #\n        # See https://github.com/LonamiWebs/Telethon/issues/1337 for details.\n        if not callable(getattr(self._loop, 'sock_connect', None)):\n            raise TypeError(\n                'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\\n\\n'\n                'Change the event loop in use to use proxies:\\n'\n                '# https://github.com/LonamiWebs/Telethon/issues/1337\\n'\n                'import asyncio\\n'\n                'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format(\n                    self._loop.__class__.__name__\n                )\n            )\n\n        # ':' in session.server_address is True if it's an IPv6 address\n        if (not self.session.server_address or\n                (':' in self.session.server_address) != self._use_ipv6):\n            await utils.maybe_async(\n                self.session.set_dc(\n                    DEFAULT_DC_ID,\n                    DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,\n                    DEFAULT_PORT\n                )\n            )\n            await utils.maybe_async(self.session.save())\n\n        if not await self._sender.connect(self._connection(\n            self.session.server_address,\n            self.session.port,\n            self.session.dc_id,\n            loggers=self._log,\n            proxy=self._proxy,\n            local_addr=self._local_addr\n        )):\n            # We don't want to init or modify anything if we were already connected\n            return\n\n        self.session.auth_key = self._sender.auth_key\n        await utils.maybe_async(self.session.save())\n\n        try:\n            # See comment when saving entities to understand this hack\n            self_entity = await utils.maybe_async(self.session.get_input_entity(0))\n            self_id = self_entity.access_hash\n            self_user = await utils.maybe_async(self.session.get_input_entity(self_id))\n            self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash)\n        except ValueError:\n            pass\n\n        if self._catch_up:\n            ss = SessionState(0, 0, False, 0, 0, 0, 0, None)\n            cs = []\n\n            update_states = await utils.maybe_async(self.session.get_update_states())\n            for entity_id, state in update_states:\n                if entity_id == 0:\n                    # TODO current session doesn't store self-user info but adding that is breaking on downstream session impls\n                    ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None)\n                else:\n                    cs.append(ChannelState(entity_id, state.pts))\n\n            self._message_box.load(ss, cs)\n            for state in cs:\n                try:\n                    entity = await utils.maybe_async(self.session.get_input_entity(state.channel_id))\n                except ValueError:\n                    self._log[__name__].warning(\n                        'No access_hash in cache for channel %s, will not catch up', state.channel_id)\n                else:\n                    self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash))\n\n        self._init_request.query = functions.help.GetConfigRequest()\n\n        req = self._init_request\n        if self._no_updates:\n            req = functions.InvokeWithoutUpdatesRequest(req)\n\n        await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req))\n\n        if self._message_box.is_empty():\n            me = await self.get_me()\n            if me:\n                await self._on_login(me)  # also calls GetState to initialize the MessageBox\n\n        self._updates_handle = self.loop.create_task(self._update_loop())\n        self._keepalive_handle = self.loop.create_task(self._keepalive_loop())\n\n    def is_connected(self: 'TelegramClient') -> bool:\n        \"\"\"\n        Returns `True` if the user has connected.\n\n        This method is **not** asynchronous (don't use ``await`` on it).\n\n        Example\n            .. code-block:: python\n\n                while client.is_connected():\n                    await asyncio.sleep(1)\n        \"\"\"\n        sender = getattr(self, '_sender', None)\n        return sender and sender.is_connected()\n\n    def disconnect(self: 'TelegramClient'):\n        \"\"\"\n        Disconnects from Telegram.\n\n        If the event loop is already running, this method returns a\n        coroutine that you should await on your own code; otherwise\n        the loop is ran until said coroutine completes.\n\n        Event handlers which are currently running will be cancelled before\n        this function returns (in order to properly clean-up their tasks).\n        In particular, this means that using ``disconnect`` in a handler\n        will cause code after the ``disconnect`` to never run. If this is\n        needed, consider spawning a separate task to do the remaining work.\n\n        Example\n            .. code-block:: python\n\n                # You don't need to use this if you used \"with client\"\n                await client.disconnect()\n        \"\"\"\n        if self.loop.is_running():\n            # Disconnect may be called from an event handler, which would\n            # cancel itself during itself and never actually complete the\n            # disconnection. Shield the task to prevent disconnect itself\n            # from being cancelled. See issue #3942 for more details.\n            return asyncio.shield(self.loop.create_task(self._disconnect_coro()))\n        else:\n            try:\n                self.loop.run_until_complete(self._disconnect_coro())\n            except RuntimeError:\n                # Python 3.5.x complains when called from\n                # `__aexit__` and there were pending updates with:\n                #   \"Event loop stopped before Future completed.\"\n                #\n                # However, it doesn't really make a lot of sense.\n                pass\n\n    def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]):\n        \"\"\"\n        Changes the proxy which will be used on next (re)connection.\n\n        Method has no immediate effects if the client is currently connected.\n\n        The new proxy will take it's effect on the next reconnection attempt:\n            - on a call `await client.connect()` (after complete disconnect)\n            - on auto-reconnect attempt (e.g, after previous connection was lost)\n        \"\"\"\n        init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \\\n            types.InputClientProxy(*self._connection.address_info(proxy))\n\n        self._init_request.proxy = init_proxy\n        self._proxy = proxy\n\n        # While `await client.connect()` passes new proxy on each new call,\n        # auto-reconnect attempts use already set up `_connection` inside\n        # the `_sender`, so the only way to change proxy between those\n        # is to directly inject parameters.\n\n        connection = getattr(self._sender, \"_connection\", None)\n        if connection:\n            if isinstance(connection, TcpMTProxy):\n                connection._ip = proxy[0]\n                connection._port = proxy[1]\n            else:\n                connection._proxy = proxy\n\n    async def _save_states_and_entities(self: 'TelegramClient'):\n        # As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``.\n        # This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved.\n        # Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.\n        # It doesn't matter if we put users in the list of chats.\n        if self._mb_entity_cache.self_id:\n            await utils.maybe_async(\n                self.session.process_entities(\n                    types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], [])\n                )\n            )\n\n        ss, cs = self._message_box.session_state()\n        await utils.maybe_async(self.session.set_update_state(0, types.updates.State(**ss, unread_count=0)))\n        now = datetime.datetime.now()  # any datetime works; channels don't need it\n        for channel_id, pts in cs.items():\n            await utils.maybe_async(\n                self.session.set_update_state(\n                    channel_id, types.updates.State(pts, 0, now, 0, unread_count=0)\n                )\n            )\n\n    async def _disconnect_coro(self: 'TelegramClient'):\n        if self.session is None:\n            return  # already logged out and disconnected\n\n        await self._disconnect()\n\n        # Also clean-up all exported senders because we're done with them\n        async with self._borrow_sender_lock:\n            for state, sender in self._borrowed_senders.values():\n                # Note that we're not checking for `state.should_disconnect()`.\n                # If the user wants to disconnect the client, ALL connections\n                # to Telegram (including exported senders) should be closed.\n                #\n                # Disconnect should never raise, so there's no try/except.\n                await sender.disconnect()\n                # Can't use `mark_disconnected` because it may be borrowed.\n                state._connected = False\n\n            # If any was borrowed\n            self._borrowed_senders.clear()\n\n        # trio's nurseries would handle this for us, but this is asyncio.\n        # All tasks spawned in the background should properly be terminated.\n        if self._event_handler_tasks:\n            for task in self._event_handler_tasks:\n                task.cancel()\n\n            await asyncio.wait(self._event_handler_tasks)\n            self._event_handler_tasks.clear()\n\n        await self._save_states_and_entities()\n\n        await utils.maybe_async(self.session.close())\n\n    async def _disconnect(self: 'TelegramClient'):\n        \"\"\"\n        Disconnect only, without closing the session. Used in reconnections\n        to different data centers, where we don't want to close the session\n        file; user disconnects however should close it since it means that\n        their job with the client is complete and we should clean it up all.\n        \"\"\"\n        await self._sender.disconnect()\n        await helpers._cancel(self._log[__name__],\n                              updates_handle=self._updates_handle,\n                              keepalive_handle=self._keepalive_handle)\n\n    async def _switch_dc(self: 'TelegramClient', new_dc):\n        \"\"\"\n        Permanently switches the current connection to the new data center.\n        \"\"\"\n        self._log[__name__].info('Reconnecting to new data center %s', new_dc)\n        dc = await self._get_dc(new_dc)\n\n        await utils.maybe_async(self.session.set_dc(dc.id, dc.ip_address, dc.port))\n        # auth_key's are associated with a server, which has now changed\n        # so it's not valid anymore. Set to None to force recreating it.\n        self._sender.auth_key.key = None\n        self.session.auth_key = None\n        await utils.maybe_async(self.session.save())\n        await self._disconnect()\n        return await self.connect()\n\n    async def _auth_key_callback(self: 'TelegramClient', auth_key):\n        \"\"\"\n        Callback from the sender whenever it needed to generate a\n        new authorization key. This means we are not authorized.\n        \"\"\"\n        self.session.auth_key = auth_key\n        await utils.maybe_async(self.session.save())\n\n    # endregion\n\n    # region Working with different connections/Data Centers\n\n    async def _get_dc(self: 'TelegramClient', dc_id, cdn=False):\n        \"\"\"Gets the Data Center (DC) associated to 'dc_id'\"\"\"\n        cls = self.__class__\n        if not cls._config:\n            cls._config = await self(functions.help.GetConfigRequest())\n\n        if cdn and not self._cdn_config:\n            cls._cdn_config = await self(functions.help.GetCdnConfigRequest())\n            for pk in cls._cdn_config.public_keys:\n                if pk.dc_id == dc_id:\n                    rsa.add_key(pk.public_key, old=False)\n\n        try:\n            return next(\n                dc for dc in cls._config.dc_options\n                if dc.id == dc_id\n                and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn\n            )\n        except StopIteration:\n            self._log[__name__].warning(\n                'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check',\n                dc_id, cdn, self._use_ipv6\n            )\n            try:\n                return next(\n                    dc for dc in cls._config.dc_options\n                    if dc.id == dc_id and bool(dc.cdn) == cdn\n                )\n            except StopIteration:\n                raise ValueError(f'Failed to get DC {dc_id} (cdn = {cdn})')\n\n    async def _create_exported_sender(self: 'TelegramClient', dc_id):\n        \"\"\"\n        Creates a new exported `MTProtoSender` for the given `dc_id` and\n        returns it. This method should be used by `_borrow_exported_sender`.\n        \"\"\"\n        # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt\n        # for clearly showing how to export the authorization\n        dc = await self._get_dc(dc_id)\n        # Can't reuse self._sender._connection as it has its own seqno.\n        #\n        # If one were to do that, Telegram would reset the connection\n        # with no further clues.\n        sender = MTProtoSender(None, loggers=self._log)\n        await sender.connect(self._connection(\n            dc.ip_address,\n            dc.port,\n            dc.id,\n            loggers=self._log,\n            proxy=self._proxy,\n            local_addr=self._local_addr\n        ))\n        self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc)\n        auth = await self(functions.auth.ExportAuthorizationRequest(dc_id))\n        self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes)\n        req = functions.InvokeWithLayerRequest(LAYER, self._init_request)\n        await sender.send(req)\n        return sender\n\n    async def _borrow_exported_sender(self: 'TelegramClient', dc_id):\n        \"\"\"\n        Borrows a connected `MTProtoSender` for the given `dc_id`.\n        If it's not cached, creates a new one if it doesn't exist yet,\n        and imports a freshly exported authorization key for it to be usable.\n\n        Once its job is over it should be `_return_exported_sender`.\n        \"\"\"\n        async with self._borrow_sender_lock:\n            self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id)\n            state, sender = self._borrowed_senders.get(dc_id, (None, None))\n\n            if state is None:\n                state = _ExportState()\n                sender = await self._create_exported_sender(dc_id)\n                sender.dc_id = dc_id\n                self._borrowed_senders[dc_id] = (state, sender)\n\n            elif state.need_connect():\n                dc = await self._get_dc(dc_id)\n                await sender.connect(self._connection(\n                    dc.ip_address,\n                    dc.port,\n                    dc.id,\n                    loggers=self._log,\n                    proxy=self._proxy,\n                    local_addr=self._local_addr\n                ))\n\n            state.add_borrow()\n            return sender\n\n    async def _return_exported_sender(self: 'TelegramClient', sender):\n        \"\"\"\n        Returns a borrowed exported sender. If all borrows have\n        been returned, the sender is cleanly disconnected.\n        \"\"\"\n        async with self._borrow_sender_lock:\n            self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id)\n            state, _ = self._borrowed_senders[sender.dc_id]\n            state.add_return()\n\n    async def _clean_exported_senders(self: 'TelegramClient'):\n        \"\"\"\n        Cleans-up all unused exported senders by disconnecting them.\n        \"\"\"\n        async with self._borrow_sender_lock:\n            for dc_id, (state, sender) in self._borrowed_senders.items():\n                if state.should_disconnect():\n                    self._log[__name__].info(\n                        'Disconnecting borrowed sender for DC %d', dc_id)\n\n                    # Disconnect should never raise\n                    await sender.disconnect()\n                    state.mark_disconnected()\n\n    async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):\n        \"\"\"Similar to ._borrow_exported_client, but for CDNs\"\"\"\n        session = self._exported_sessions.get(cdn_redirect.dc_id)\n        if not session:\n            dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)\n            session = await utils.maybe_async(self.session.clone())\n            await utils.maybe_async(session.set_dc(dc.id, dc.ip_address, dc.port))\n            self._exported_sessions[cdn_redirect.dc_id] = session\n\n        self._log[__name__].info('Creating new CDN client')\n        client = self.__class__(\n            session, self.api_id, self.api_hash,\n            proxy=self._proxy,\n            timeout=self._timeout,\n            loop=self.loop\n        )\n\n        session.auth_key = self._sender.auth_key\n        await client._sender.connect(self._connection(\n            session.server_address,\n            session.port,\n            session.dc_id,\n            loggers=self._log,\n            proxy=self._proxy,\n            local_addr=self._local_addr\n        ))\n        return client\n\n    # endregion\n\n    # region Invoking Telegram requests\n\n    @abc.abstractmethod\n    def __call__(self: 'TelegramClient', request, ordered=False):\n        \"\"\"\n        Invokes (sends) one or more MTProtoRequests and returns (receives)\n        their result.\n\n        Args:\n            request (`TLObject` | `list`):\n                The request or requests to be invoked.\n\n            ordered (`bool`, optional):\n                Whether the requests (if more than one was given) should be\n                executed sequentially on the server. They run in arbitrary\n                order by default.\n\n            flood_sleep_threshold (`int` | `None`, optional):\n                The flood sleep threshold to use for this request. This overrides\n                the default value stored in\n                `client.flood_sleep_threshold <telethon.client.telegrambaseclient.TelegramBaseClient.flood_sleep_threshold>`\n\n        Returns:\n            The result of the request (often a `TLObject`) or a list of\n            results if more than one request was given.\n        \"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def _update_loop(self: 'TelegramClient'):\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def _handle_auto_reconnect(self: 'TelegramClient'):\n        raise NotImplementedError\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/telegramclient.py",
    "content": "from . import (\n    AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods,\n    BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,\n    MessageParseMethods, UserMethods, TelegramBaseClient\n)\n\n\nclass TelegramClient(\n    AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods,\n    BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods,\n    MessageParseMethods, UserMethods, TelegramBaseClient\n):\n    pass\n"
  },
  {
    "path": "telethon/client/updates.py",
    "content": "import asyncio\nimport inspect\nimport itertools\nimport random\nimport sys\nimport time\nimport traceback\nimport typing\nimport logging\nimport warnings\nfrom collections import deque\n\ntry:\n    import sqlite3\n\n    OperationalError = sqlite3.OperationalError\nexcept ImportError as e:\n    sqlite3 = None\n\n    class OperationalError(Exception):\n        pass  # won't be created and thus never caught\n\nfrom .. import events, utils, errors\nfrom ..events.common import EventBuilder, EventCommon\nfrom ..tl import types, functions\nfrom .._updates import GapError, PrematureEndReason\nfrom ..helpers import get_running_loop\nfrom ..version import __version__\n\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nCallback = typing.Callable[[typing.Any], typing.Any]\n\nclass UpdateMethods:\n\n    # region Public methods\n\n    async def _run_until_disconnected(self: 'TelegramClient'):\n        try:\n            # Make a high-level request to notify that we want updates\n            await self(functions.updates.GetStateRequest())\n            result = await self.disconnected\n            if self._updates_error is not None:\n                raise self._updates_error\n            return result\n        except KeyboardInterrupt:\n            pass\n        finally:\n            await self.disconnect()\n\n    async def set_receive_updates(self: 'TelegramClient', receive_updates):\n        \"\"\"\n        Change the value of `receive_updates`.\n\n        This is an `async` method, because in order for Telegram to start\n        sending updates again, a request must be made.\n        \"\"\"\n        self._no_updates = not receive_updates\n        if receive_updates:\n            await self(functions.updates.GetStateRequest())\n\n    def run_until_disconnected(self: 'TelegramClient'):\n        \"\"\"\n        Runs the event loop until the library is disconnected.\n\n        It also notifies Telegram that we want to receive updates\n        as described in https://core.telegram.org/api/updates.\n        If an unexpected error occurs during update handling,\n        the client will disconnect and said error will be raised.\n\n        Manual disconnections can be made by calling `disconnect()\n        <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`\n        or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on\n        the console window running the script).\n\n        If a disconnection error occurs (i.e. the library fails to reconnect\n        automatically), said error will be raised through here, so you have a\n        chance to ``except`` it on your own code.\n\n        If the loop is already running, this method returns a coroutine\n        that you should await on your own code.\n\n        .. note::\n\n            If you want to handle ``KeyboardInterrupt`` in your code,\n            simply run the event loop in your code too in any way, such as\n            ``loop.run_forever()`` or ``await client.disconnected`` (e.g.\n            ``loop.run_until_complete(client.disconnected)``).\n\n        Example\n            .. code-block:: python\n\n                # Blocks the current task here until a disconnection occurs.\n                #\n                # You will still receive updates, since this prevents the\n                # script from exiting.\n                await client.run_until_disconnected()\n        \"\"\"\n        if self.loop.is_running():\n            return self._run_until_disconnected()\n        try:\n            return self.loop.run_until_complete(self._run_until_disconnected())\n        except KeyboardInterrupt:\n            pass\n        finally:\n            # No loop.run_until_complete; it's already syncified\n            self.disconnect()\n\n    def on(self: 'TelegramClient', event: EventBuilder):\n        \"\"\"\n        Decorator used to `add_event_handler` more conveniently.\n\n\n        Arguments\n            event (`_EventBuilder` | `type`):\n                The event builder class or instance to be used,\n                for instance ``events.NewMessage``.\n\n        Example\n            .. code-block:: python\n\n                from telethon import TelegramClient, events\n                client = TelegramClient(...)\n\n                # Here we use client.on\n                @client.on(events.NewMessage)\n                async def handler(event):\n                    ...\n        \"\"\"\n        def decorator(f):\n            self.add_event_handler(f, event)\n            return f\n\n        return decorator\n\n    def add_event_handler(\n            self: 'TelegramClient',\n            callback: Callback,\n            event: EventBuilder = None):\n        \"\"\"\n        Registers a new event handler callback.\n\n        The callback will be called when the specified event occurs.\n\n        Arguments\n            callback (`callable`):\n                The callable function accepting one parameter to be used.\n\n                Note that if you have used `telethon.events.register` in\n                the callback, ``event`` will be ignored, and instead the\n                events you previously registered will be used.\n\n            event (`_EventBuilder` | `type`, optional):\n                The event builder class or instance to be used,\n                for instance ``events.NewMessage``.\n\n                If left unspecified, `telethon.events.raw.Raw` (the\n                :tl:`Update` objects with no further processing) will\n                be passed instead.\n\n        Example\n            .. code-block:: python\n\n                from telethon import TelegramClient, events\n                client = TelegramClient(...)\n\n                async def handler(event):\n                    ...\n\n                client.add_event_handler(handler, events.NewMessage)\n        \"\"\"\n        builders = events._get_handlers(callback)\n        if builders is not None:\n            for event in builders:\n                self._event_builders.append((event, callback))\n            return\n\n        if isinstance(event, type):\n            event = event()\n        elif not event:\n            event = events.Raw()\n\n        self._event_builders.append((event, callback))\n\n    def remove_event_handler(\n            self: 'TelegramClient',\n            callback: Callback,\n            event: EventBuilder = None) -> int:\n        \"\"\"\n        Inverse operation of `add_event_handler()`.\n\n        If no event is given, all events for this callback are removed.\n        Returns how many callbacks were removed.\n\n        Example\n            .. code-block:: python\n\n                @client.on(events.Raw)\n                @client.on(events.NewMessage)\n                async def handler(event):\n                    ...\n\n                # Removes only the \"Raw\" handling\n                # \"handler\" will still receive \"events.NewMessage\"\n                client.remove_event_handler(handler, events.Raw)\n\n                # \"handler\" will stop receiving anything\n                client.remove_event_handler(handler)\n        \"\"\"\n        found = 0\n        if event and not isinstance(event, type):\n            event = type(event)\n\n        i = len(self._event_builders)\n        while i:\n            i -= 1\n            ev, cb = self._event_builders[i]\n            if cb == callback and (not event or isinstance(ev, event)):\n                del self._event_builders[i]\n                found += 1\n\n        return found\n\n    def list_event_handlers(self: 'TelegramClient')\\\n            -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]':\n        \"\"\"\n        Lists all registered event handlers.\n\n        Returns\n            A list of pairs consisting of ``(callback, event)``.\n\n        Example\n            .. code-block:: python\n\n                @client.on(events.NewMessage(pattern='hello'))\n                async def on_greeting(event):\n                    '''Greets someone'''\n                    await event.reply('Hi')\n\n                for callback, event in client.list_event_handlers():\n                    print(id(callback), type(event))\n        \"\"\"\n        return [(callback, event) for event, callback in self._event_builders]\n\n    async def catch_up(self: 'TelegramClient'):\n        \"\"\"\n        \"Catches up\" on the missed updates while the client was offline.\n        You should call this method after registering the event handlers\n        so that the updates it loads can by processed by your script.\n\n        This can also be used to forcibly fetch new updates if there are any.\n\n        Example\n            .. code-block:: python\n\n                await client.catch_up()\n        \"\"\"\n        await self._updates_queue.put(types.UpdatesTooLong())\n\n    # endregion\n\n    # region Private methods\n\n    async def _update_loop(self: 'TelegramClient'):\n        # If the MessageBox is not empty, the account had to be logged-in to fill in its state.\n        # This flag is used to propagate the \"you got logged-out\" error up (but getting logged-out\n        # can only happen if it was once logged-in).\n        was_once_logged_in = self._authorized is True or not self._message_box.is_empty()\n\n        self._updates_error = None\n        try:\n            if self._catch_up:\n                # User wants to catch up as soon as the client is up and running,\n                # so this is the best place to do it.\n                await self.catch_up()\n\n            updates_to_dispatch = deque()\n\n            while self.is_connected():\n                if updates_to_dispatch:\n                    if self._sequential_updates:\n                        await self._dispatch_update(updates_to_dispatch.popleft())\n                    else:\n                        while updates_to_dispatch:\n                            # TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed\n                            task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft()))\n                            self._event_handler_tasks.add(task)\n                            task.add_done_callback(self._event_handler_tasks.discard)\n\n                    continue\n\n                if len(self._mb_entity_cache) >= self._entity_cache_limit:\n                    self._log[__name__].info(\n                        'In-memory entity cache limit reached (%s/%s), flushing to session',\n                        len(self._mb_entity_cache),\n                        self._entity_cache_limit\n                    )\n                    await self._save_states_and_entities()\n                    self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map)\n                    if len(self._mb_entity_cache) >= self._entity_cache_limit:\n                        warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit')\n\n                    self._log[__name__].info(\n                        'In-memory entity cache at %s/%s after flushing to session',\n                        len(self._mb_entity_cache),\n                        self._entity_cache_limit\n                    )\n\n\n                get_diff = self._message_box.get_difference()\n                if get_diff:\n                    self._log[__name__].debug('Getting difference for account updates')\n                    try:\n                        diff = await self(get_diff)\n                    except (\n                        errors.ServerError,\n                        errors.TimedOutError,\n                        errors.FloodWaitError,\n                        ValueError\n                    ) as e:\n                        # Telegram is having issues\n                        self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__)\n                        self._message_box.end_difference()\n                        continue\n                    except (errors.UnauthorizedError, errors.AuthKeyError) as e:\n                        # Not logged in or broken authorization key, can't get difference\n                        self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__)\n                        self._message_box.end_difference()\n                        if was_once_logged_in:\n                            self._updates_error = e\n                            await self.disconnect()\n                            break\n                        continue\n                    except (errors.TypeNotFoundError, OperationalError) as e:\n                        # User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use\n                        self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e)\n                        self._message_box.end_difference()\n                        self._updates_error = e\n                        await self.disconnect()\n                        break\n                    except errors.RPCError as e:\n                        # Fallback; treat as transient error (the amount of \"fatal errors\" reported seem to indicate this is most likely what we need to do)\n                        self._log[__name__].warning(\n                            \"Cannot get difference due to unexpected error (this may be a bug \"\n                            f\"in Telethon v{__version__} in that it could be handled better, but it's unlikely): %s\",\n                            e\n                        )\n                        self._message_box.end_difference()\n                        continue\n                    except OSError as e:\n                        # Network is likely down, but it's unclear for how long.\n                        # If disconnect is called this task will be cancelled along with the sleep.\n                        # If disconnect is not called, getting difference should be retried after a few seconds.\n                        self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e)\n                        await asyncio.sleep(5)\n                        continue\n                    updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache)\n                    if updates:\n                        self._log[__name__].info('Got difference for account updates')\n\n                    _preprocess_updates = await self._preprocess_updates(updates, users, chats)\n                    updates_to_dispatch.extend(_preprocess_updates)\n                    continue\n\n                get_diff = self._message_box.get_channel_difference(self._mb_entity_cache)\n                if get_diff:\n                    self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id)\n                    try:\n                        diff = await self(get_diff)\n                    except (errors.UnauthorizedError, errors.AuthKeyError) as e:\n                        # Not logged in or broken authorization key, can't get difference\n                        self._log[__name__].warning(\n                            'Cannot get difference for channel %s since the account is not logged in: %s',\n                            get_diff.channel.channel_id, type(e).__name__\n                        )\n                        self._message_box.end_channel_difference(\n                            get_diff,\n                            PrematureEndReason.TEMPORARY_SERVER_ISSUES,\n                            self._mb_entity_cache\n                        )\n                        if was_once_logged_in:\n                            self._updates_error = e\n                            await self.disconnect()\n                            break\n                        continue\n                    except (errors.TypeNotFoundError, OperationalError) as e:\n                        self._log[__name__].warning(\n                            'Cannot get difference for channel %s since the account is likely misusing the session: %s',\n                            get_diff.channel.channel_id, e\n                        )\n                        self._message_box.end_channel_difference(\n                            get_diff,\n                            PrematureEndReason.TEMPORARY_SERVER_ISSUES,\n                            self._mb_entity_cache\n                        )\n                        self._updates_error = e\n                        await self.disconnect()\n                        break\n                    except (\n                        errors.PersistentTimestampOutdatedError,\n                        errors.PersistentTimestampInvalidError,\n                        errors.ServerError,\n                        errors.TimedOutError,\n                        errors.FloodWaitError,\n                        ValueError\n                    ) as e:\n                        # According to Telegram's docs:\n                        # \"Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL).\"\n                        # We can treat this as \"empty difference\" and not update the local pts.\n                        # Then this same call will be retried when another gap is detected or timeout expires.\n                        #\n                        # Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few\n                        # seconds, but if Telegram is having issues it's probably best to wait for it to send another\n                        # update (hinting it may be okay now) and retry then.\n                        #\n                        # This is a bit hacky because MessageBox doesn't really have a way to \"not update\" the pts.\n                        # Instead we manually extract the previously-known pts and use that.\n                        #\n                        # For PersistentTimestampInvalidError:\n                        # Somehow our pts is either too new or the server does not know about this.\n                        # We treat this as PersistentTimestampOutdatedError for now.\n                        # TODO investigate why/when this happens and if this is the proper solution\n                        self._log[__name__].warning(\n                            'Getting difference for channel updates %s caused %s;'\n                            ' ending getting difference prematurely until server issues are resolved',\n                            get_diff.channel.channel_id, type(e).__name__\n                        )\n                        self._message_box.end_channel_difference(\n                            get_diff,\n                            PrematureEndReason.TEMPORARY_SERVER_ISSUES,\n                            self._mb_entity_cache\n                        )\n                        continue\n                    except (errors.ChannelPrivateError, errors.ChannelInvalidError):\n                        # Timeout triggered a get difference, but we have been banned in the channel since then.\n                        # Because we can no longer fetch updates from this channel, we should stop keeping track\n                        # of it entirely.\n                        self._log[__name__].info(\n                            'Account is now banned in %d so we can no longer fetch updates from it',\n                            get_diff.channel.channel_id\n                        )\n                        self._message_box.end_channel_difference(\n                            get_diff,\n                            PrematureEndReason.BANNED,\n                            self._mb_entity_cache\n                        )\n                        continue\n                    except errors.RPCError as e:\n                        # Fallback; treat as transient error (the amount of \"fatal errors\" reported seem to indicate this is most likely what we need to do)\n                        self._log[__name__].warning(\n                            \"Cannot get difference for channel %d due to unexpected error (this may be a bug \"\n                            f\"in Telethon v{__version__} in that it could be handled better, but it's unlikely): %s\",\n                            get_diff.channel.channel_id, e\n                        )\n                        self._message_box.end_channel_difference(\n                            get_diff,\n                            PrematureEndReason.TEMPORARY_SERVER_ISSUES,\n                            self._mb_entity_cache\n                        )\n                        continue\n                    except OSError as e:\n                        self._log[__name__].info(\n                            'Cannot get difference for channel %d since the network is down: %s: %s',\n                            get_diff.channel.channel_id, type(e).__name__, e\n                        )\n                        await asyncio.sleep(5)\n                        continue\n\n                    updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache)\n                    if updates:\n                        self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id)\n\n                    _preprocess_updates = await self._preprocess_updates(updates, users, chats)\n                    updates_to_dispatch.extend(_preprocess_updates)\n                    continue\n\n                deadline = self._message_box.check_deadlines()\n                deadline_delay = deadline - get_running_loop().time()\n                if deadline_delay > 0:\n                    # Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs).\n                    try:\n                        updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay)\n                    except asyncio.TimeoutError:\n                        self._log[__name__].debug('Timeout waiting for updates expired')\n                        continue\n                else:\n                    continue\n\n                processed = []\n                try:\n                    users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)\n                except GapError:\n                    continue  # get(_channel)_difference will start returning requests\n\n                _preprocess_updates = await self._preprocess_updates(processed, users, chats)\n                updates_to_dispatch.extend(_preprocess_updates)\n        except asyncio.CancelledError:\n            pass\n        except Exception as e:\n            self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)')\n            self._updates_error = e\n            await self.disconnect()\n\n    async def _preprocess_updates(self, updates, users, chats):\n        self._mb_entity_cache.extend(users, chats)\n        await utils.maybe_async(self.session.process_entities(types.contacts.ResolvedPeer(None, users, chats)))\n        entities = {utils.get_peer_id(x): x\n                    for x in itertools.chain(users, chats)}\n        for u in updates:\n            u._entities = entities\n        return updates\n\n    async def _keepalive_loop(self: 'TelegramClient'):\n        # Pings' ID don't really need to be secure, just \"random\"\n        rnd = lambda: random.randrange(-2**63, 2**63)\n        while self.is_connected():\n            try:\n                await asyncio.wait_for(\n                    self.disconnected, timeout=60\n                )\n                continue  # We actually just want to act upon timeout\n            except asyncio.TimeoutError:\n                pass\n            except asyncio.CancelledError:\n                return\n            except Exception:\n                continue  # Any disconnected exception should be ignored\n\n            # Check if we have any exported senders to clean-up periodically\n            await self._clean_exported_senders()\n\n            # Don't bother sending pings until the low-level connection is\n            # ready, otherwise a lot of pings will be batched to be sent upon\n            # reconnect, when we really don't care about that.\n            if not self._sender._transport_connected():\n                continue\n\n            # We also don't really care about their result.\n            # Just send them periodically.\n            try:\n                self._sender._keepalive_ping(rnd())\n            except (ConnectionError, asyncio.CancelledError):\n                return\n\n            # Entities and cached files are not saved when they are\n            # inserted because this is a rather expensive operation\n            # (default's sqlite3 takes ~0.1s to commit changes). Do\n            # it every minute instead. No-op if there's nothing new.\n            await self._save_states_and_entities()\n\n            await utils.maybe_async(self.session.save())\n\n    async def _dispatch_update(self: 'TelegramClient', update):\n        # TODO only used for AlbumHack, and MessageBox is not really designed for this\n        others = None\n\n        if not self._mb_entity_cache.self_id:\n            # Some updates require our own ID, so we must make sure\n            # that the event builder has offline access to it. Calling\n            # `get_me()` will cache it under `self._mb_entity_cache`.\n            #\n            # It will return `None` if we haven't logged in yet which is\n            # fine, we will just retry next time anyway.\n            try:\n                await self.get_me(input_peer=True)\n            except OSError:\n                pass  # might not have connection\n\n        built = EventBuilderDict(self, update, others)\n        for conv_set in self._conversations.values():\n            for conv in conv_set:\n                ev = built[events.NewMessage]\n                if ev:\n                    conv._on_new_message(ev)\n\n                ev = built[events.MessageEdited]\n                if ev:\n                    conv._on_edit(ev)\n\n                ev = built[events.MessageRead]\n                if ev:\n                    conv._on_read(ev)\n\n                if conv._custom:\n                    await conv._check_custom(built)\n\n        for builder, callback in self._event_builders:\n            event = built[type(builder)]\n            if not event:\n                continue\n\n            if not builder.resolved:\n                await builder.resolve(self)\n\n            filter = builder.filter(event)\n            if inspect.isawaitable(filter):\n                filter = await filter\n            if not filter:\n                continue\n\n            try:\n                await callback(event)\n            except errors.AlreadyInConversationError:\n                name = getattr(callback, '__name__', repr(callback))\n                self._log[__name__].debug(\n                    'Event handler \"%s\" already has an open conversation, '\n                    'ignoring new one', name)\n            except events.StopPropagation:\n                name = getattr(callback, '__name__', repr(callback))\n                self._log[__name__].debug(\n                    'Event handler \"%s\" stopped chain of propagation '\n                    'for event %s.', name, type(event).__name__\n                )\n                break\n            except Exception as e:\n                if not isinstance(e, asyncio.CancelledError) or self.is_connected():\n                    name = getattr(callback, '__name__', repr(callback))\n                    self._log[__name__].exception('Unhandled exception on %s', name)\n\n    async def _dispatch_event(self: 'TelegramClient', event):\n        \"\"\"\n        Dispatches a single, out-of-order event. Used by `AlbumHack`.\n        \"\"\"\n        # We're duplicating a most logic from `_dispatch_update`, but all in\n        # the name of speed; we don't want to make it worse for all updates\n        # just because albums may need it.\n        for builder, callback in self._event_builders:\n            if isinstance(builder, events.Raw):\n                continue\n            if not isinstance(event, builder.Event):\n                continue\n\n            if not builder.resolved:\n                await builder.resolve(self)\n\n            filter = builder.filter(event)\n            if inspect.isawaitable(filter):\n                filter = await filter\n            if not filter:\n                continue\n\n            try:\n                await callback(event)\n            except errors.AlreadyInConversationError:\n                name = getattr(callback, '__name__', repr(callback))\n                self._log[__name__].debug(\n                    'Event handler \"%s\" already has an open conversation, '\n                    'ignoring new one', name)\n            except events.StopPropagation:\n                name = getattr(callback, '__name__', repr(callback))\n                self._log[__name__].debug(\n                    'Event handler \"%s\" stopped chain of propagation '\n                    'for event %s.', name, type(event).__name__\n                )\n                break\n            except Exception as e:\n                if not isinstance(e, asyncio.CancelledError) or self.is_connected():\n                    name = getattr(callback, '__name__', repr(callback))\n                    self._log[__name__].exception('Unhandled exception on %s', name)\n\n    async def _handle_auto_reconnect(self: 'TelegramClient'):\n        # TODO Catch-up\n        # For now we make a high-level request to let Telegram\n        # know we are still interested in receiving more updates.\n        try:\n            await self.get_me()\n        except Exception as e:\n            self._log[__name__].warning('Error executing high-level request '\n                                        'after reconnect: %s: %s', type(e), e)\n\n        return\n        try:\n            self._log[__name__].info(\n                'Asking for the current state after reconnect...')\n\n            # TODO consider:\n            # If there aren't many updates while the client is disconnected\n            # (I tried with up to 20), Telegram seems to send them without\n            # asking for them (via updates.getDifference).\n            #\n            # On disconnection, the library should probably set a \"need\n            # difference\" or \"catching up\" flag so that any new updates are\n            # ignored, and then the library should call updates.getDifference\n            # itself to fetch them.\n            #\n            # In any case (either there are too many updates and Telegram\n            # didn't send them, or there isn't a lot and Telegram sent them\n            # but we dropped them), we fetch the new difference to get all\n            # missed updates. I feel like this would be the best solution.\n\n            # If a disconnection occurs, the old known state will be\n            # the latest one we were aware of, so we can catch up since\n            # the most recent state we were aware of.\n            await self.catch_up()\n\n            self._log[__name__].info('Successfully fetched missed updates')\n        except errors.RPCError as e:\n            self._log[__name__].warning('Failed to get missed updates after '\n                                        'reconnect: %r', e)\n        except Exception:\n            self._log[__name__].exception(\n                'Unhandled exception while getting update difference after reconnect')\n\n    # endregion\n\n\nclass EventBuilderDict:\n    \"\"\"\n    Helper \"dictionary\" to return events from types and cache them.\n    \"\"\"\n    def __init__(self, client: 'TelegramClient', update, others):\n        self.client = client\n        self.update = update\n        self.others = others\n\n    def __getitem__(self, builder):\n        try:\n            return self.__dict__[builder]\n        except KeyError:\n            event = self.__dict__[builder] = builder.build(\n                self.update, self.others, self.client._self_id)\n\n            if isinstance(event, EventCommon):\n                event.original_update = self.update\n                event._entities = self.update._entities\n                event._set_client(self.client)\n            elif event:\n                event._client = self.client\n\n            return event\n"
  },
  {
    "path": "telethon/client/uploads.py",
    "content": "import hashlib\nimport io\nimport itertools\nimport os\nimport pathlib\nimport re\nimport typing\nfrom io import BytesIO\n\nfrom ..crypto import AES\n\nfrom .. import utils, helpers, hints\nfrom ..tl import types, functions, custom\n\ntry:\n    import PIL\n    import PIL.Image\nexcept ImportError:\n    PIL = None\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\nclass _CacheType:\n    \"\"\"Like functools.partial but pretends to be the wrapped class.\"\"\"\n    def __init__(self, cls):\n        self._cls = cls\n\n    def __call__(self, *args, **kwargs):\n        return self._cls(*args, file_reference=b'', **kwargs)\n\n    def __eq__(self, other):\n        return self._cls == other\n\n\ndef _resize_photo_if_needed(\n        file, is_image, width=2560, height=2560, background=(255, 255, 255)):\n\n    # https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254\n    if (not is_image\n            or PIL is None\n            or (isinstance(file, io.IOBase) and not file.seekable())):\n        return file\n\n    if isinstance(file, bytes):\n        file = io.BytesIO(file)\n\n    if isinstance(file, io.IOBase):\n        # Pillow seeks to 0 unconditionally later anyway\n        old_pos = file.tell()\n        file.seek(0, io.SEEK_END)\n        before = file.tell()\n    elif isinstance(file, str) and os.path.exists(file):\n        # Check if file exists as a path and if so, get its size on disk\n        before = os.path.getsize(file)\n    else:\n        # Would be weird...\n        before = None\n\n    try:\n        # Don't use a `with` block for `image`, or `file` would be closed.\n        # See https://github.com/LonamiWebs/Telethon/issues/1121 for more.\n        image = PIL.Image.open(file)\n        try:\n            kwargs = {'exif': image.info['exif']}\n        except KeyError:\n            kwargs = {}\n\n        if image.mode == 'RGB':\n            # 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\n            if image.width <= width and image.height <= height and (before <= 10000000 if before else False):\n                return file\n\n            # If the image is already RGB, don't convert it\n            # certain modes such as 'P' have no alpha index but can't be saved as JPEG directly\n            image.thumbnail((width, height), PIL.Image.LANCZOS)\n            result = image\n        else:\n            # We could save the resized image with the original format, but\n            # JPEG often compresses better -> smaller size -> faster upload\n            # We need to mask away the alpha channel ([3]), since otherwise\n            # IOError is raised when trying to save alpha channels in JPEG.\n            image.thumbnail((width, height), PIL.Image.LANCZOS)\n            result = PIL.Image.new('RGB', image.size, background)\n            mask = None\n\n            if image.has_transparency_data:\n                if image.mode == 'RGBA':\n                    mask = image.getchannel('A')\n                else:\n                    mask = image.convert('RGBA').getchannel('A')\n\n            result.paste(image, mask=mask)\n\n        buffer = io.BytesIO()\n        result.save(buffer, 'JPEG', progressive=True, **kwargs)\n        buffer.seek(0)\n        buffer.name = 'a.jpg'\n        return buffer\n    except IOError:\n        return file\n    finally:\n        # The original position might matter\n        if isinstance(file, io.IOBase):\n            file.seek(old_pos)\n\n\nclass UploadMethods:\n\n    # region Public methods\n\n    async def send_file(\n            self: 'TelegramClient',\n            entity: 'hints.EntityLike',\n            file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]',\n            *,\n            caption: typing.Union[str, typing.Sequence[str]] = None,\n            force_document: bool = False,\n            mime_type: str = None,\n            file_size: int = None,\n            clear_draft: bool = False,\n            progress_callback: 'hints.ProgressCallback' = None,\n            reply_to: 'hints.MessageIDLike' = None,\n            attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None,\n            thumb: 'hints.FileLike' = None,\n            allow_cache: bool = True,\n            parse_mode: str = (),\n            formatting_entities: typing.Optional[\n                typing.Union[\n                    typing.List[types.TypeMessageEntity], typing.List[typing.List[types.TypeMessageEntity]]\n                ]\n            ] = None,\n            voice_note: bool = False,\n            video_note: bool = False,\n            buttons: typing.Optional['hints.MarkupLike'] = None,\n            silent: bool = None,\n            background: bool = None,\n            supports_streaming: bool = False,\n            schedule: 'hints.DateLike' = None,\n            comment_to: 'typing.Union[int, types.Message]' = None,\n            ttl: int = None,\n            nosound_video: bool = None,\n            send_as: typing.Optional['hints.EntityLike'] = None,\n            message_effect_id: typing.Optional[int] = None,\n            **kwargs) -> typing.Union[typing.List[typing.Any], typing.Any]:\n        \"\"\"\n        Sends message with the given file to the specified entity.\n\n        .. note::\n\n            If the ``hachoir3`` package (``hachoir`` module) is installed,\n            it will be used to determine metadata from audio and video files.\n\n            If the ``pillow`` package is installed and you are sending a photo,\n            it will be resized to fit within the maximum dimensions allowed\n            by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This\n            cannot be done if you are sending :tl:`InputFile`, however.\n\n        Arguments\n            entity (`entity`):\n                Who will receive the file.\n\n            file (`str` | `bytes` | `file` | `media`):\n                The file to send, which can be one of:\n\n                * A local file path to an in-disk file. The file name\n                  will be the path's base name.\n\n                * A `bytes` byte array with the file's data to send\n                  (for example, by using ``text.encode('utf-8')``).\n                  A default file name will be used.\n\n                * A bytes `io.IOBase` stream over the file to send\n                  (for example, by using ``open(file, 'rb')``).\n                  Its ``.name`` property will be used for the file name,\n                  or a default if it doesn't have one.\n\n                * An external URL to a file over the internet. This will\n                  send the file as \"external\" media, and Telegram is the\n                  one that will fetch the media and send it.\n\n                * A Bot API-like ``file_id``. You can convert previously\n                  sent media to file IDs for later reusing with\n                  `telethon.utils.pack_bot_file_id`.\n\n                * A handle to an existing file (for example, if you sent a\n                  message with media before, you can use its ``message.media``\n                  as a file here).\n\n                * A handle to an uploaded file (from `upload_file`).\n\n                * A :tl:`InputMedia` instance. For example, if you want to\n                  send a dice use :tl:`InputMediaDice`, or if you want to\n                  send a contact use :tl:`InputMediaContact`.\n\n                To send an album, you should provide a list in this parameter.\n\n                If a list or similar is provided, the files in it will be\n                sent as an album in the order in which they appear, sliced\n                in chunks of 10 if more than 10 are given.\n\n            caption (`str`, optional):\n                Optional caption for the sent media message. When sending an\n                album, the caption may be a list of strings, which will be\n                assigned to the files pairwise.\n\n            force_document (`bool`, optional):\n                If left to `False` and the file is a path that ends with\n                the extension of an image file or a video file, it will be\n                sent as such. Otherwise always as a document.\n\n            mime_type (`str`, optional):\n                Custom mime type to use for the file to be sent (for example,\n                ``audio/mpeg``, ``audio/x-vorbis+ogg``, etc.).\n                It can change the type of files displayed.\n                If not set to any value, the mime type will be determined\n                automatically based on the file's extension.\n\n            file_size (`int`, optional):\n                The size of the file to be uploaded if it needs to be uploaded,\n                which will be determined automatically if not specified.\n\n                If the file size can't be determined beforehand, the entire\n                file will be read in-memory to find out how large it is.\n\n            clear_draft (`bool`, optional):\n                Whether the existing draft should be cleared or not.\n\n            progress_callback (`callable`, optional):\n                A callback function accepting two parameters:\n                ``(sent bytes, total)``.\n\n            reply_to (`int` | `Message <telethon.tl.custom.message.Message>`):\n                Same as `reply_to` from `send_message`.\n\n            attributes (`list`, optional):\n                Optional attributes that override the inferred ones, like\n                :tl:`DocumentAttributeFilename` and so on.\n\n            thumb (`str` | `bytes` | `file`, optional):\n                Optional JPEG thumbnail (for documents). **Telegram will\n                ignore this parameter** unless you pass a ``.jpg`` file!\n\n                The file must also be small in dimensions and in disk size.\n                Successful thumbnails were files below 20kB and 320x320px.\n                Width/height and dimensions/size ratios may be important.\n                For Telegram to accept a thumbnail, you must provide the\n                dimensions of the underlying media through ``attributes=``\n                with :tl:`DocumentAttributeVideo` or by installing the\n                optional ``hachoir`` dependency.\n\n\n            allow_cache (`bool`, optional):\n                This parameter currently does nothing, but is kept for\n                backward-compatibility (and it may get its use back in\n                the future).\n\n            parse_mode (`object`, optional):\n                See the `TelegramClient.parse_mode\n                <telethon.client.messageparse.MessageParseMethods.parse_mode>`\n                property for allowed values. Markdown parsing will be used by\n                default.\n\n            formatting_entities (`list`, optional):\n                Optional formatting entities for the sent media message. When sending an album,\n                `formatting_entities` can be a list of lists, where each inner list contains\n                `types.TypeMessageEntity`. Each inner list will be assigned to the corresponding\n                file in a pairwise manner with the caption. If provided, the ``parse_mode``\n                parameter will be ignored.\n\n            voice_note (`bool`, optional):\n                If `True` the audio will be sent as a voice note.\n\n            video_note (`bool`, optional):\n                If `True` the video will be sent as a video note,\n                also known as a round video message.\n\n            buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`):\n                The matrix (list of lists), row list or button to be shown\n                after sending the message. This parameter will only work if\n                you have signed in as a bot. You can also pass your own\n                :tl:`ReplyMarkup` here.\n\n            silent (`bool`, optional):\n                Whether the message should notify people with sound or not.\n                Defaults to `False` (send with a notification sound unless\n                the person has the chat muted). Set it to `True` to alter\n                this behaviour.\n\n            background (`bool`, optional):\n                Whether the message should be send in background.\n\n            supports_streaming (`bool`, optional):\n                Whether the sent video supports streaming or not. Note that\n                Telegram only recognizes as streamable some formats like MP4,\n                and others like AVI or MKV will not work. You should convert\n                these to MP4 before sending if you want them to be streamable.\n                Unsupported formats will result in ``VideoContentTypeError``.\n\n            schedule (`hints.DateLike`, optional):\n                If set, the file won't send immediately, and instead\n                it will be scheduled to be automatically sent at a later\n                time.\n\n            comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):\n                Similar to ``reply_to``, but replies in the linked group of a\n                broadcast channel instead (effectively leaving a \"comment to\"\n                the specified message).\n\n                This parameter takes precedence over ``reply_to``. If there is\n                no linked chat, `telethon.errors.sgIdInvalidError` is raised.\n\n            ttl (`int`. optional):\n                The Time-To-Live of the file (also known as \"self-destruct timer\"\n                or \"self-destructing media\"). If set, files can only be viewed for\n                a short period of time before they disappear from the message\n                history automatically.\n\n                The value must be at least 1 second, and at most 60 seconds,\n                otherwise Telegram will ignore this parameter.\n\n                Not all types of media can be used with this parameter, such\n                as text documents, which will fail with ``TtlMediaInvalidError``.\n\n            nosound_video (`bool`, optional):\n                Only applicable when sending a video file without an audio\n                track. If set to ``True``, the video will be displayed in\n                Telegram as a video. If set to ``False``, Telegram will attempt\n                to display the video as an animated gif. (It may still display\n                as a video due to other factors.) The value is ignored if set\n                on non-video files. This is set to ``True`` for albums, as gifs\n                cannot be sent in albums.\n\n            send_as (`entity`):\n                Unique identifier (int) or username (str) of the chat or channel to send the message as.\n                You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.\n                Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,\n                This setting applies to the current message and will remain effective for future messages unless explicitly changed.\n                To set this behavior permanently for all messages, use SaveDefaultSendAs.\n\n            message_effect_id (`int`, optional):\n                Unique identifier of the message effect to be added to the message; for private chats only\n\n        Returns\n            The `Message <telethon.tl.custom.message.Message>` (or messages)\n            containing the sent file, or messages if a list of them was passed.\n\n        Example\n            .. code-block:: python\n\n                # Normal files like photos\n                await client.send_file(chat, '/my/photos/me.jpg', caption=\"It's me!\")\n                # or\n                await client.send_message(chat, \"It's me!\", file='/my/photos/me.jpg')\n\n                # Voice notes or round videos\n                await client.send_file(chat, '/my/songs/song.mp3', voice_note=True)\n                await client.send_file(chat, '/my/videos/video.mp4', video_note=True)\n\n                # Custom thumbnails\n                await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg')\n\n                # Only documents\n                await client.send_file(chat, '/my/photos/photo.png', force_document=True)\n\n                # Albums\n                await client.send_file(chat, [\n                    '/my/photos/holiday1.jpg',\n                    '/my/photos/holiday2.jpg',\n                    '/my/drawings/portrait.png'\n                ])\n\n                # Printing upload progress\n                def callback(current, total):\n                    print('Uploaded', current, 'out of', total,\n                          'bytes: {:.2%}'.format(current / total))\n\n                await client.send_file(chat, file, progress_callback=callback)\n\n                # Dices, including dart and other future emoji\n                from telethon.tl import types\n                await client.send_file(chat, types.InputMediaDice(''))\n                await client.send_file(chat, types.InputMediaDice('🎯'))\n\n                # Contacts\n                await client.send_file(chat, types.InputMediaContact(\n                    phone_number='+34 123 456 789',\n                    first_name='Example',\n                    last_name='',\n                    vcard=''\n                ))\n        \"\"\"\n        # TODO Properly implement allow_cache to reuse the sha256 of the file\n        # i.e. `None` was used\n        if not file:\n            raise TypeError('Cannot use {!r} as file'.format(file))\n\n        if not caption:\n            caption = ''\n\n        if not formatting_entities:\n            formatting_entities = []\n\n        entity = await self.get_input_entity(entity)\n        if comment_to is not None:\n            entity, reply_to = await self._get_comment_data(entity, comment_to)\n        else:\n            reply_to = utils.get_message_id(reply_to)\n\n        # First check if the user passed an iterable, in which case\n        # we may want to send grouped.\n        if utils.is_list_like(file):\n            sent_count = 0\n            used_callback = None if not progress_callback else (\n                lambda s, t: progress_callback(sent_count + s, len(file))\n            )\n\n            if utils.is_list_like(caption):\n                captions = caption\n            else:\n                captions = [caption]\n\n            # Check that formatting_entities list is valid\n            if all(utils.is_list_like(obj) for obj in formatting_entities):\n                formatting_entities = formatting_entities\n            elif utils.is_list_like(formatting_entities):\n                formatting_entities = [formatting_entities]\n            else:\n                raise TypeError('The formatting_entities argument must be a list or a sequence of lists')\n\n            # Check that all entities in all lists are of the correct type\n            if not all(isinstance(ent, types.TypeMessageEntity) for sublist in formatting_entities for ent in sublist):\n                raise TypeError('All entities must be instances of <types.TypeMessageEntity>')\n\n            result = []\n            while file:\n                result += await self._send_album(\n                    entity, file[:10], caption=captions[:10], formatting_entities=formatting_entities[:10],\n                    progress_callback=used_callback, reply_to=reply_to,\n                    parse_mode=parse_mode, silent=silent, schedule=schedule,\n                    supports_streaming=supports_streaming, clear_draft=clear_draft,\n                    force_document=force_document, background=background,\n                    send_as=send_as, message_effect_id=message_effect_id\n                )\n                file = file[10:]\n                captions = captions[10:]\n                formatting_entities = formatting_entities[10:]\n                sent_count += 10\n\n            return result\n\n        if formatting_entities:\n            msg_entities = formatting_entities\n        else:\n            caption, msg_entities =\\\n                await self._parse_message_text(caption, parse_mode)\n\n        file_handle, media, image = await self._file_to_media(\n            file, force_document=force_document,\n            mime_type=mime_type,\n            file_size=file_size,\n            progress_callback=progress_callback,\n            attributes=attributes, allow_cache=allow_cache, thumb=thumb,\n            voice_note=voice_note, video_note=video_note,\n            supports_streaming=supports_streaming, ttl=ttl,\n            nosound_video=nosound_video,\n        )\n\n        # e.g. invalid cast from :tl:`MessageMediaWebPage`\n        if not media:\n            raise TypeError('Cannot use {!r} as file'.format(file))\n\n        markup = self.build_reply_markup(buttons)\n        reply_to = None if reply_to is None else types.InputReplyToMessage(reply_to)\n        request = functions.messages.SendMediaRequest(\n            entity, media, reply_to=reply_to, message=caption,\n            entities=msg_entities, reply_markup=markup, silent=silent,\n            schedule_date=schedule, clear_draft=clear_draft,\n            background=background,\n            send_as=await self.get_input_entity(send_as) if send_as else None,\n            effect=message_effect_id\n        )\n        return self._get_response_message(request, await self(request), entity)\n\n    async def _send_album(self: 'TelegramClient', entity, files, caption='',\n                          formatting_entities=None,\n                          progress_callback=None, reply_to=None,\n                          parse_mode=(), silent=None, schedule=None,\n                          supports_streaming=None, clear_draft=None,\n                          force_document=False, background=None, ttl=None,\n                          send_as: typing.Optional['hints.EntityLike'] = None,\n                          message_effect_id: typing.Optional[int] = None):\n        \"\"\"Specialized version of .send_file for albums\"\"\"\n        # We don't care if the user wants to avoid cache, we will use it\n        # anyway. Why? The cached version will be exactly the same thing\n        # we need to produce right now to send albums (uploadMedia), and\n        # cache only makes a difference for documents where the user may\n        # want the attributes used on them to change.\n        #\n        # In theory documents can be sent inside the albums, but they appear\n        # as different messages (not inside the album), and the logic to set\n        # the attributes/avoid cache is already written in .send_file().\n        entity = await self.get_input_entity(entity)\n        if not utils.is_list_like(caption):\n            caption = (caption,)\n        if not all(isinstance(obj, list) for obj in formatting_entities):\n            formatting_entities = (formatting_entities,)\n\n        captions = []\n        # If the formatting_entities argument is provided, we don't use parse_mode\n        if formatting_entities:\n            # Pop from the end (so reverse)\n            capt_with_ent = itertools.zip_longest(reversed(caption), reversed(formatting_entities), fillvalue=None)\n            for msg_caption, msg_entities in capt_with_ent:\n                captions.append((msg_caption, msg_entities))\n        else:\n            for c in reversed(caption):  # Pop from the end (so reverse)\n                captions.append(await self._parse_message_text(c or '', parse_mode))\n\n        reply_to = utils.get_message_id(reply_to)\n\n        used_callback = None if not progress_callback else (\n            # use an integer when sent matches total, to easily determine a file has been fully sent\n            lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files))\n        )\n\n        # Need to upload the media first, but only if they're not cached yet\n        media = []\n        for sent_count, file in enumerate(files):\n            # Albums want :tl:`InputMedia` which, in theory, includes\n            # :tl:`InputMediaUploadedPhoto`. However, using that will\n            # make it `raise MediaInvalidError`, so we need to upload\n            # it as media and then convert that to :tl:`InputMediaPhoto`.\n            fh, fm, _ = await self._file_to_media(\n                file, supports_streaming=supports_streaming,\n                force_document=force_document, ttl=ttl,\n                progress_callback=used_callback, nosound_video=True)\n            if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):\n                r = await self(functions.messages.UploadMediaRequest(\n                    entity, media=fm\n                ))\n\n                fm = utils.get_input_media(r.photo)\n            elif isinstance(fm, (types.InputMediaUploadedDocument, types.InputMediaDocumentExternal)):\n                r = await self(functions.messages.UploadMediaRequest(\n                    entity, media=fm\n                ))\n\n                fm = utils.get_input_media(\n                    r.document, supports_streaming=supports_streaming)\n\n            if captions:\n                caption, msg_entities = captions.pop()\n            else:\n                caption, msg_entities = '', None\n            media.append(types.InputSingleMedia(\n                fm,\n                message=caption,\n                entities=msg_entities\n                # random_id is autogenerated\n            ))\n\n        # Now we can construct the multi-media request\n        request = functions.messages.SendMultiMediaRequest(\n            entity, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), multi_media=media,\n            silent=silent, schedule_date=schedule, clear_draft=clear_draft,\n            background=background,\n            send_as=await self.get_input_entity(send_as) if send_as else None,\n            effect=message_effect_id\n        )\n        result = await self(request)\n\n        random_ids = [m.random_id for m in media]\n        return self._get_response_message(random_ids, result, entity)\n\n    async def upload_file(\n            self: 'TelegramClient',\n            file: 'hints.FileLike',\n            *,\n            part_size_kb: float = None,\n            file_size: int = None,\n            file_name: str = None,\n            use_cache: type = None,\n            key: bytes = None,\n            iv: bytes = None,\n            progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile':\n        \"\"\"\n        Uploads a file to Telegram's servers, without sending it.\n\n        .. note::\n\n            Generally, you want to use `send_file` instead.\n\n        This method returns a handle (an instance of :tl:`InputFile` or\n        :tl:`InputFileBig`, as required) which can be later used before\n        it expires (they are usable during less than a day).\n\n        Uploading a file will simply return a \"handle\" to the file stored\n        remotely in the Telegram servers, which can be later used on. This\n        will **not** upload the file to your own chat or any chat at all.\n\n        Arguments\n            file (`str` | `bytes` | `file`):\n                The path of the file, byte array, or stream that will be sent.\n                Note that if a byte array or a stream is given, a filename\n                or its type won't be inferred, and it will be sent as an\n                \"unnamed application/octet-stream\".\n\n            part_size_kb (`int`, optional):\n                Chunk size when uploading files. The larger, the less\n                requests will be made (up to 512KB maximum).\n\n            file_size (`int`, optional):\n                The size of the file to be uploaded, which will be determined\n                automatically if not specified.\n\n                If the file size can't be determined beforehand, the entire\n                file will be read in-memory to find out how large it is.\n\n            file_name (`str`, optional):\n                The file name which will be used on the resulting InputFile.\n                If not specified, the name will be taken from the ``file``\n                and if this is not a `str`, it will be ``\"unnamed\"``.\n\n            use_cache (`type`, optional):\n                This parameter currently does nothing, but is kept for\n                backward-compatibility (and it may get its use back in\n                the future).\n\n            key ('bytes', optional):\n                In case of an encrypted upload (secret chats) a key is supplied\n\n            iv ('bytes', optional):\n                In case of an encrypted upload (secret chats) an iv is supplied\n\n            progress_callback (`callable`, optional):\n                A callback function accepting two parameters:\n                ``(sent bytes, total)``.\n\n                When sending an album, the callback will receive a number\n                between 0 and the amount of files as the \"sent\" parameter,\n                and the amount of files as the \"total\". Note that the first\n                parameter will be a floating point number to indicate progress\n                within a file (e.g. ``2.5`` means it has sent 50% of the third\n                file, because it's between 2 and 3).\n\n        Returns\n            :tl:`InputFileBig` if the file size is larger than 10MB,\n            `InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`\n            (subclass of :tl:`InputFile`) otherwise.\n\n        Example\n            .. code-block:: python\n\n                # Photos as photo and document\n                file = await client.upload_file('photo.jpg')\n                await client.send_file(chat, file)                       # sends as photo\n                await client.send_file(chat, file, force_document=True)  # sends as document\n\n                file.name = 'not a photo.jpg'\n                await client.send_file(chat, file, force_document=True)  # document, new name\n\n                # As song or as voice note\n                file = await client.upload_file('song.ogg')\n                await client.send_file(chat, file)                   # sends as song\n                await client.send_file(chat, file, voice_note=True)  # sends as voice note\n        \"\"\"\n        if isinstance(file, (types.InputFile, types.InputFileBig)):\n            return file  # Already uploaded\n\n        pos = 0\n        async with helpers._FileStream(file, file_size=file_size) as stream:\n            # Opening the stream will determine the correct file size\n            file_size = stream.file_size\n\n            if not part_size_kb:\n                part_size_kb = utils.get_appropriated_part_size(file_size)\n\n            if part_size_kb > 512:\n                raise ValueError('The part size must be less or equal to 512KB')\n\n            part_size = int(part_size_kb * 1024)\n            if part_size % 1024 != 0:\n                raise ValueError(\n                    'The part size must be evenly divisible by 1024')\n\n            # Set a default file name if None was specified\n            file_id = helpers.generate_random_long()\n            if not file_name:\n                file_name = stream.name or str(file_id)\n\n            # If the file name lacks extension, add it if possible.\n            # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR`\n            # even if the uploaded image is indeed a photo.\n            if not os.path.splitext(file_name)[-1]:\n                file_name += utils._get_extension(stream)\n\n            # Determine whether the file is too big (over 10MB) or not\n            # Telegram does make a distinction between smaller or larger files\n            is_big = file_size > 10 * 1024 * 1024\n            hash_md5 = hashlib.md5()\n\n            part_count = (file_size + part_size - 1) // part_size\n            self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',\n                                     file_size, part_count, part_size)\n\n            pos = 0\n            for part_index in range(part_count):\n                # Read the file by in chunks of size part_size\n                part = await helpers._maybe_await(stream.read(part_size))\n\n                if not isinstance(part, bytes):\n                    raise TypeError(\n                        'file descriptor returned {}, not bytes (you must '\n                        'open the file in bytes mode)'.format(type(part)))\n\n                # `file_size` could be wrong in which case `part` may not be\n                # `part_size` before reaching the end.\n                if len(part) != part_size and part_index < part_count - 1:\n                    raise ValueError(\n                        'read less than {} before reaching the end; either '\n                        '`file_size` or `read` are wrong'.format(part_size))\n\n                pos += len(part)\n\n                # Encryption part if needed\n                if key and iv:\n                    part = AES.encrypt_ige(part, key, iv)\n\n                if not is_big:\n                    # Bit odd that MD5 is only needed for small files and not\n                    # big ones with more chance for corruption, but that's\n                    # what Telegram wants.\n                    hash_md5.update(part)\n\n                # The SavePartRequest is different depending on whether\n                # the file is too large or not (over or less than 10MB)\n                if is_big:\n                    request = functions.upload.SaveBigFilePartRequest(\n                        file_id, part_index, part_count, part)\n                else:\n                    request = functions.upload.SaveFilePartRequest(\n                        file_id, part_index, part)\n\n                result = await self(request)\n                if result:\n                    self._log[__name__].debug('Uploaded %d/%d',\n                                              part_index + 1, part_count)\n                    if progress_callback:\n                        await helpers._maybe_await(progress_callback(pos, file_size))\n                else:\n                    raise RuntimeError(\n                        'Failed to upload file part {}.'.format(part_index))\n\n        if is_big:\n            return types.InputFileBig(file_id, part_count, file_name)\n        else:\n            return custom.InputSizedFile(\n                file_id, part_count, file_name, md5=hash_md5, size=file_size\n            )\n\n    # endregion\n\n    async def _file_to_media(\n            self, file, force_document=False, file_size=None,\n            progress_callback=None, attributes=None, thumb=None,\n            allow_cache=True, voice_note=False, video_note=False,\n            supports_streaming=False, mime_type=None, as_image=None,\n            ttl=None, nosound_video=None):\n        if not file:\n            return None, None, None\n\n        if isinstance(file, pathlib.Path):\n            file = str(file.absolute())\n\n        is_image = utils.is_image(file)\n        if as_image is None:\n            as_image = is_image and not force_document\n\n        # `aiofiles` do not base `io.IOBase` but do have `read`, so we\n        # just check for the read attribute to see if it's file-like.\n        if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig)) \\\n                and not hasattr(file, 'read'):\n            # The user may pass a Message containing media (or the media,\n            # or anything similar) that should be treated as a file. Try\n            # getting the input media for whatever they passed and send it.\n            #\n            # We pass all attributes since these will be used if the user\n            # passed :tl:`InputFile`, and all information may be relevant.\n            try:\n                return (None, utils.get_input_media(\n                    file,\n                    is_photo=as_image,\n                    attributes=attributes,\n                    force_document=force_document,\n                    voice_note=voice_note,\n                    video_note=video_note,\n                    supports_streaming=supports_streaming,\n                    ttl=ttl\n                ), as_image)\n            except TypeError:\n                # Can't turn whatever was given into media\n                return None, None, as_image\n\n        media = None\n        file_handle = None\n\n        if isinstance(file, (types.InputFile, types.InputFileBig)):\n            file_handle = file\n        elif not isinstance(file, str) or os.path.isfile(file):\n            file_handle = await self.upload_file(\n                _resize_photo_if_needed(file, as_image),\n                file_size=file_size,\n                progress_callback=progress_callback\n            )\n        elif re.match('https?://', file):\n            if as_image:\n                media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl)\n            else:\n                media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl)\n        else:\n            bot_file = utils.resolve_bot_file_id(file)\n            if bot_file:\n                media = utils.get_input_media(bot_file, ttl=ttl)\n\n        if media:\n            pass  # Already have media, don't check the rest\n        elif not file_handle:\n            raise ValueError(\n                'Failed to convert {} to media. Not an existing file, '\n                'an HTTP URL or a valid bot-API-like file ID'.format(file)\n            )\n        elif as_image:\n            media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl)\n        else:\n            attributes, mime_type = utils.get_attributes(\n                file,\n                mime_type=mime_type,\n                attributes=attributes,\n                force_document=force_document and not is_image,\n                voice_note=voice_note,\n                video_note=video_note,\n                supports_streaming=supports_streaming,\n                thumb=thumb\n            )\n\n            if not thumb:\n                thumb = None\n            else:\n                if isinstance(thumb, pathlib.Path):\n                    thumb = str(thumb.absolute())\n                thumb = await self.upload_file(thumb, file_size=file_size)\n\n            # setting `nosound_video` to `True` doesn't affect videos with sound\n            # instead it prevents sending silent videos as GIFs\n            nosound_video = nosound_video if mime_type.split(\"/\")[0] == 'video' else None\n\n            media = types.InputMediaUploadedDocument(\n                file=file_handle,\n                mime_type=mime_type,\n                attributes=attributes,\n                thumb=thumb,\n                force_file=force_document and not is_image,\n                ttl_seconds=ttl,\n                nosound_video=nosound_video\n            )\n        return file_handle, media, as_image\n\n    # endregion\n"
  },
  {
    "path": "telethon/client/users.py",
    "content": "import asyncio\nimport datetime\nimport itertools\nimport time\nimport typing\n\nfrom .. import errors, helpers, utils, hints\nfrom ..errors import MultiError, RPCError\nfrom ..helpers import retry_range\nfrom ..tl import TLRequest, types, functions\n\n_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!')\n\nif typing.TYPE_CHECKING:\n    from .telegramclient import TelegramClient\n\n\ndef _fmt_flood(delay, request, *, early=False, td=datetime.timedelta):\n    return (\n        'Sleeping%s for %ds (%s) on %s flood wait',\n        ' early' if early else '',\n        delay,\n        td(seconds=delay),\n        request.__class__.__name__\n    )\n\n\nclass UserMethods:\n    async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None):\n        return await self._call(self._sender, request, ordered=ordered)\n\n    async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None):\n        if self._loop is not None and self._loop != helpers.get_running_loop():\n            raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')\n        # if the loop is None it will fail with a connection error later on\n\n        if flood_sleep_threshold is None:\n            flood_sleep_threshold = self.flood_sleep_threshold\n        requests = list(request) if utils.is_list_like(request) else [request]\n        request = list(request) if utils.is_list_like(request) else request\n        for i, r in enumerate(requests):\n            if not isinstance(r, TLRequest):\n                raise _NOT_A_REQUEST()\n            await r.resolve(self, utils)\n\n            # Avoid making the request if it's already in a flood wait\n            if r.CONSTRUCTOR_ID in self._flood_waited_requests:\n                due = self._flood_waited_requests[r.CONSTRUCTOR_ID]\n                diff = round(due - time.time())\n                if diff <= 3:  # Flood waits below 3 seconds are \"ignored\"\n                    self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)\n                elif diff <= flood_sleep_threshold:\n                    self._log[__name__].info(*_fmt_flood(diff, r, early=True))\n                    await asyncio.sleep(diff)\n                    self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None)\n                else:\n                    raise errors.FloodWaitError(request=r, capture=diff)\n\n            if self._no_updates:\n                if utils.is_list_like(request):\n                    request[i] = functions.InvokeWithoutUpdatesRequest(r)\n                else:\n                    # This should only run once as requests should be a list of 1 item\n                    request = functions.InvokeWithoutUpdatesRequest(r)\n\n        request_index = 0\n        last_error = None\n        self._last_request = time.time()\n\n        for attempt in retry_range(self._request_retries):\n            try:\n                future = sender.send(request, ordered=ordered)\n                if isinstance(future, list):\n                    results = []\n                    exceptions = []\n                    for f in future:\n                        try:\n                            result = await f\n                        except RPCError as e:\n                            exceptions.append(e)\n                            results.append(None)\n                            continue\n                        await utils.maybe_async(self.session.process_entities(result))\n                        exceptions.append(None)\n                        results.append(result)\n                        request_index += 1\n                    if any(x is not None for x in exceptions):\n                        raise MultiError(exceptions, results, requests)\n                    else:\n                        return results\n                else:\n                    result = await future\n                    await utils.maybe_async(self.session.process_entities(result))\n                    return result\n            except (errors.ServerError, errors.RpcCallFailError,\n                    errors.RpcMcgetFailError, errors.InterdcCallErrorError,\n                    errors.TimedOutError,\n                    errors.InterdcCallRichErrorError) as e:\n                last_error = e\n                self._log[__name__].warning(\n                    'Telegram is having internal issues %s: %s',\n                    e.__class__.__name__, e)\n\n                await asyncio.sleep(2)\n            except (errors.FloodWaitError, errors.FloodPremiumWaitError,\n                    errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:\n                last_error = e\n                if utils.is_list_like(request):\n                    request = request[request_index]\n\n                # SLOW_MODE_WAIT is chat-specific, not request-specific\n                if not isinstance(e, errors.SlowModeWaitError):\n                    self._flood_waited_requests\\\n                        [request.CONSTRUCTOR_ID] = time.time() + e.seconds\n\n                # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for\n                # such a short amount will cause retries very fast leading to issues.\n                if e.seconds == 0:\n                    e.seconds = 1\n\n                if e.seconds <= self.flood_sleep_threshold:\n                    self._log[__name__].info(*_fmt_flood(e.seconds, request))\n                    await asyncio.sleep(e.seconds)\n                else:\n                    raise\n            except (errors.PhoneMigrateError, errors.NetworkMigrateError,\n                    errors.UserMigrateError) as e:\n                last_error = e\n                self._log[__name__].info('Phone migrated to %d', e.new_dc)\n                should_raise = isinstance(e, (\n                    errors.PhoneMigrateError, errors.NetworkMigrateError\n                ))\n                if should_raise and await self.is_user_authorized():\n                    raise\n                await self._switch_dc(e.new_dc)\n\n        if self._raise_last_call_error and last_error is not None:\n            raise last_error\n        raise ValueError('Request was unsuccessful {} time(s)'\n                         .format(attempt))\n\n    # region Public methods\n\n    async def get_me(self: 'TelegramClient', input_peer: bool = False) \\\n            -> 'typing.Union[types.User, types.InputPeerUser]':\n        \"\"\"\n        Gets \"me\", the current :tl:`User` who is logged in.\n\n        If the user has not logged in yet, this method returns `None`.\n\n        Arguments\n            input_peer (`bool`, optional):\n                Whether to return the :tl:`InputPeerUser` version or the normal\n                :tl:`User`. This can be useful if you just need to know the ID\n                of yourself.\n\n        Returns\n            Your own :tl:`User`.\n\n        Example\n            .. code-block:: python\n\n                me = await client.get_me()\n                print(me.username)\n        \"\"\"\n        if input_peer and self._mb_entity_cache.self_id:\n            return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer()\n\n        try:\n            me = (await self(\n                functions.users.GetUsersRequest([types.InputUserSelf()])))[0]\n\n            if not self._mb_entity_cache.self_id:\n                self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash)\n\n            return utils.get_input_peer(me, allow_self=False) if input_peer else me\n        except errors.UnauthorizedError:\n            return None\n\n    @property\n    def _self_id(self: 'TelegramClient') -> typing.Optional[int]:\n        \"\"\"\n        Returns the ID of the logged-in user, if known.\n\n        This property is used in every update, and some like `updateLoginToken`\n        occur prior to login, so it gracefully handles when no ID is known yet.\n        \"\"\"\n        return self._mb_entity_cache.self_id\n\n    async def is_bot(self: 'TelegramClient') -> bool:\n        \"\"\"\n        Return `True` if the signed-in user is a bot, `False` otherwise.\n\n        Example\n            .. code-block:: python\n\n                if await client.is_bot():\n                    print('Beep')\n                else:\n                    print('Hello')\n        \"\"\"\n        if self._mb_entity_cache.self_bot is None:\n            await self.get_me(input_peer=True)\n\n        return self._mb_entity_cache.self_bot\n\n    async def is_user_authorized(self: 'TelegramClient') -> bool:\n        \"\"\"\n        Returns `True` if the user is authorized (logged in).\n\n        Example\n            .. code-block:: python\n\n                if not await client.is_user_authorized():\n                    await client.send_code_request(phone)\n                    code = input('enter code: ')\n                    await client.sign_in(phone, code)\n        \"\"\"\n        if self._authorized is None:\n            try:\n                # Any request that requires authorization will work\n                await self(functions.updates.GetStateRequest())\n                self._authorized = True\n            except errors.RPCError:\n                self._authorized = False\n\n        return self._authorized\n\n    async def get_entity(\n            self: 'TelegramClient',\n            entity: 'hints.EntitiesLike') -> typing.Union['hints.Entity', typing.List['hints.Entity']]:\n        \"\"\"\n        Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`\n        or :tl:`Channel`. You can also pass a list or iterable of entities,\n        and they will be efficiently fetched from the network.\n\n        Arguments\n            entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):\n                If a username is given, **the username will be resolved** making\n                an API call every time. Resolving usernames is an expensive\n                operation and will start hitting flood waits around 50 usernames\n                in a short period of time.\n\n                If you want to get the entity for a *cached* username, you should\n                first `get_input_entity(username) <get_input_entity>` which will\n                use the cache), and then use `get_entity` with the result of the\n                previous call.\n\n                Similar limits apply to invite links, and you should use their\n                ID instead.\n\n                Using phone numbers (from people in your contact list), exact\n                names, integer IDs or :tl:`Peer` rely on a `get_input_entity`\n                first, which in turn needs the entity to be in cache, unless\n                a :tl:`InputPeer` was passed.\n\n                Unsupported types will raise ``TypeError``.\n\n                If the entity can't be found, ``ValueError`` will be raised.\n\n        Returns\n            :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the\n            input entity. A list will be returned if more than one was given.\n\n        Example\n            .. code-block:: python\n\n                from telethon import utils\n\n                me = await client.get_entity('me')\n                print(utils.get_display_name(me))\n\n                chat = await client.get_input_entity('username')\n                async for message in client.iter_messages(chat):\n                    ...\n\n                # Note that you could have used the username directly, but it's\n                # good to use get_input_entity if you will reuse it a lot.\n                async for message in client.iter_messages('username'):\n                    ...\n\n                # Note that for this to work the phone number must be in your contacts\n                some_id = await client.get_peer_id('+34123456789')\n        \"\"\"\n        single = not utils.is_list_like(entity)\n        if single:\n            entity = (entity,)\n\n        # Group input entities by string (resolve username),\n        # input users (get users), input chat (get chats) and\n        # input channels (get channels) to get the most entities\n        # in the less amount of calls possible.\n        inputs = []\n        for x in entity:\n            if isinstance(x, str):\n                inputs.append(x)\n            else:\n                inputs.append(await self.get_input_entity(x))\n\n        lists = {\n            helpers._EntityType.USER: [],\n            helpers._EntityType.CHAT: [],\n            helpers._EntityType.CHANNEL: [],\n        }\n        for x in inputs:\n            try:\n                lists[helpers._entity_type(x)].append(x)\n            except TypeError:\n                pass\n\n        users = lists[helpers._EntityType.USER]\n        chats = lists[helpers._EntityType.CHAT]\n        channels = lists[helpers._EntityType.CHANNEL]\n        if users:\n            # GetUsersRequest has a limit of 200 per call\n            tmp = []\n            while users:\n                curr, users = users[:200], users[200:]\n                tmp.extend(await self(functions.users.GetUsersRequest(curr)))\n            users = tmp\n        if chats:  # TODO Handle chats slice?\n            chats = (await self(\n                functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats\n        if channels:\n            channels = (await self(\n                functions.channels.GetChannelsRequest(channels))).chats\n\n        # Merge users, chats and channels into a single dictionary\n        id_entity = {\n            # `get_input_entity` might've guessed the type from a non-marked ID,\n            # so the only way to match that with the input is by not using marks here.\n            utils.get_peer_id(x, add_mark=False): x\n            for x in itertools.chain(users, chats, channels)\n        }\n\n        # We could check saved usernames and put them into the users,\n        # chats and channels list from before. While this would reduce\n        # the amount of ResolveUsername calls, it would fail to catch\n        # username changes.\n        result = []\n        for x in inputs:\n            if isinstance(x, str):\n                result.append(await self._get_entity_from_string(x))\n            elif not isinstance(x, types.InputPeerSelf):\n                result.append(id_entity[utils.get_peer_id(x, add_mark=False)])\n            else:\n                result.append(next(\n                    u for u in id_entity.values()\n                    if isinstance(u, types.User) and u.is_self\n                ))\n\n        return result[0] if single else result\n\n    async def get_input_entity(\n            self: 'TelegramClient',\n            peer: 'hints.EntityLike') -> 'types.TypeInputPeer':\n        \"\"\"\n        Turns the given entity into its input entity version.\n\n        Most requests use this kind of :tl:`InputPeer`, so this is the most\n        suitable call to make for those cases. **Generally you should let the\n        library do its job** and don't worry about getting the input entity\n        first, but if you're going to use an entity often, consider making the\n        call:\n\n        Arguments\n            entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):\n                If a username or invite link is given, **the library will\n                use the cache**. This means that it's possible to be using\n                a username that *changed* or an old invite link (this only\n                happens if an invite link for a small group chat is used\n                after it was upgraded to a mega-group).\n\n                If the username or ID from the invite link is not found in\n                the cache, it will be fetched. The same rules apply to phone\n                numbers (``'+34 123456789'``) from people in your contact list.\n\n                If an exact name is given, it must be in the cache too. This\n                is not reliable as different people can share the same name\n                and which entity is returned is arbitrary, and should be used\n                only for quick tests.\n\n                If a positive integer ID is given, the entity will be searched\n                in cached users, chats or channels, without making any call.\n\n                If a negative integer ID is given, the entity will be searched\n                exactly as either a chat (prefixed with ``-``) or as a channel\n                (prefixed with ``-100``).\n\n                If a :tl:`Peer` is given, it will be searched exactly in the\n                cache as either a user, chat or channel.\n\n                If the given object can be turned into an input entity directly,\n                said operation will be done.\n\n                Unsupported types will raise ``TypeError``.\n\n                If the entity can't be found, ``ValueError`` will be raised.\n\n        Returns\n            :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`\n            or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.\n\n            If you need to get the ID of yourself, you should use\n            `get_me` with ``input_peer=True``) instead.\n\n        Example\n            .. code-block:: python\n\n                # If you're going to use \"username\" often in your code\n                # (make a lot of calls), consider getting its input entity\n                # once, and then using the \"user\" everywhere instead.\n                user = await client.get_input_entity('username')\n\n                # The same applies to IDs, chats or channels.\n                chat = await client.get_input_entity(-123456789)\n        \"\"\"\n        # Short-circuit if the input parameter directly maps to an InputPeer\n        try:\n            return utils.get_input_peer(peer)\n        except TypeError:\n            pass\n\n        # Next in priority is having a peer (or its ID) cached in-memory\n        try:\n            # 0x2d45687 == crc32(b'Peer')\n            if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687:\n                return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer()\n        except AttributeError:\n            pass\n\n        # Then come known strings that take precedence\n        if peer in ('me', 'self'):\n            return types.InputPeerSelf()\n\n        # No InputPeer, cached peer, or known string. Fetch from disk cache\n        try:\n            input_entity = await utils.maybe_async(self.session.get_input_entity(peer))\n            return input_entity\n        except ValueError:\n            pass\n\n        # Only network left to try\n        if isinstance(peer, str):\n            return utils.get_input_peer(\n                await self._get_entity_from_string(peer))\n\n        # If we're a bot and the user has messaged us privately users.getUsers\n        # will work with access_hash = 0. Similar for channels.getChannels.\n        # If we're not a bot but the user is in our contacts, it seems to work\n        # regardless. These are the only two special-cased requests.\n        peer = utils.get_peer(peer)\n        if isinstance(peer, types.PeerUser):\n            users = await self(functions.users.GetUsersRequest([\n                types.InputUser(peer.user_id, access_hash=0)]))\n            if users and not isinstance(users[0], types.UserEmpty):\n                # If the user passed a valid ID they expect to work for\n                # channels but would be valid for users, we get UserEmpty.\n                # Avoid returning the invalid empty input peer for that.\n                #\n                # We *could* try to guess if it's a channel first, and if\n                # it's not, work as a chat and try to validate it through\n                # another request, but that becomes too much work.\n                return utils.get_input_peer(users[0])\n        elif isinstance(peer, types.PeerChat):\n            return types.InputPeerChat(peer.chat_id)\n        elif isinstance(peer, types.PeerChannel):\n            try:\n                channels = await self(functions.channels.GetChannelsRequest([\n                    types.InputChannel(peer.channel_id, access_hash=0)]))\n                return utils.get_input_peer(channels.chats[0])\n            except errors.ChannelInvalidError:\n                pass\n\n        raise ValueError(\n            'Could not find the input entity for {} ({}). Please read https://'\n            'docs.telethon.dev/en/stable/concepts/entities.html to'\n            ' find out more details.'\n            .format(peer, type(peer).__name__)\n        )\n\n    async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'):\n        i, cls = utils.resolve_id(await self.get_peer_id(peer))\n        return cls(i)\n\n    async def get_peer_id(\n            self: 'TelegramClient',\n            peer: 'hints.EntityLike',\n            add_mark: bool = True) -> int:\n        \"\"\"\n        Gets the ID for the given entity.\n\n        This method needs to be ``async`` because `peer` supports usernames,\n        invite-links, phone numbers (from people in your contact list), etc.\n\n        If ``add_mark is False``, then a positive ID will be returned\n        instead. By default, bot-API style IDs (signed) are returned.\n\n        Example\n            .. code-block:: python\n\n                print(await client.get_peer_id('me'))\n        \"\"\"\n        if isinstance(peer, int):\n            return utils.get_peer_id(peer, add_mark=add_mark)\n\n        try:\n            if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6):\n                # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer'\n                peer = await self.get_input_entity(peer)\n        except AttributeError:\n            peer = await self.get_input_entity(peer)\n\n        if isinstance(peer, types.InputPeerSelf):\n            peer = await self.get_me(input_peer=True)\n\n        return utils.get_peer_id(peer, add_mark=add_mark)\n\n    # endregion\n\n    # region Private methods\n\n    async def _get_entity_from_string(self: 'TelegramClient', string):\n        \"\"\"\n        Gets a full entity from the given string, which may be a phone or\n        a username, and processes all the found entities on the session.\n        The string may also be a user link, or a channel/chat invite link.\n\n        This method has the side effect of adding the found users to the\n        session database, so it can be queried later without API calls,\n        if this option is enabled on the session.\n\n        Returns the found entity, or raises TypeError if not found.\n        \"\"\"\n        phone = utils.parse_phone(string)\n        if phone:\n            try:\n                for user in (await self(\n                        functions.contacts.GetContactsRequest(0))).users:\n                    if user.phone == phone:\n                        return user\n            except errors.BotMethodInvalidError:\n                raise ValueError('Cannot get entity by phone number as a '\n                                 'bot (try using integer IDs, not strings)')\n        elif string.lower() in ('me', 'self'):\n            return await self.get_me()\n        else:\n            username, is_join_chat = utils.parse_username(string)\n            if is_join_chat:\n                invite = await self(\n                    functions.messages.CheckChatInviteRequest(username))\n\n                if isinstance(invite, types.ChatInvite):\n                    raise ValueError(\n                        'Cannot get entity from a channel (or group) '\n                        'that you are not part of. Join the group and retry'\n                    )\n                elif isinstance(invite, types.ChatInviteAlready):\n                    return invite.chat\n            elif username:\n                try:\n                    result = await self(\n                        functions.contacts.ResolveUsernameRequest(username))\n                except errors.UsernameNotOccupiedError as e:\n                    raise ValueError('No user has \"{}\" as username'\n                                     .format(username)) from e\n\n                try:\n                    pid = utils.get_peer_id(result.peer, add_mark=False)\n                    if isinstance(result.peer, types.PeerUser):\n                        return next(x for x in result.users if x.id == pid)\n                    else:\n                        return next(x for x in result.chats if x.id == pid)\n                except StopIteration:\n                    pass\n            try:\n                # Nobody with this username, maybe it's an exact name/title\n                input_entity = await utils.maybe_async(self.session.get_input_entity(string))\n                return await self.get_entity(input_entity)\n            except ValueError:\n                pass\n\n        raise ValueError(\n            'Cannot find any entity corresponding to \"{}\"'.format(string)\n        )\n\n    async def _get_input_dialog(self: 'TelegramClient', dialog):\n        \"\"\"\n        Returns a :tl:`InputDialogPeer`. This is a bit tricky because\n        it may or not need access to the client to convert what's given\n        into an input entity.\n        \"\"\"\n        try:\n            if dialog.SUBCLASS_OF_ID == 0xa21c9795:  # crc32(b'InputDialogPeer')\n                dialog.peer = await self.get_input_entity(dialog.peer)\n                return dialog\n            elif dialog.SUBCLASS_OF_ID == 0xc91c90b6:  # crc32(b'InputPeer')\n                return types.InputDialogPeer(dialog)\n        except AttributeError:\n            pass\n\n        return types.InputDialogPeer(await self.get_input_entity(dialog))\n\n    async def _get_input_notify(self: 'TelegramClient', notify):\n        \"\"\"\n        Returns a :tl:`InputNotifyPeer`. This is a bit tricky because\n        it may or not need access to the client to convert what's given\n        into an input entity.\n        \"\"\"\n        try:\n            if notify.SUBCLASS_OF_ID == 0x58981615:\n                if isinstance(notify, types.InputNotifyPeer):\n                    notify.peer = await self.get_input_entity(notify.peer)\n                return notify\n        except AttributeError:\n            pass\n\n        return types.InputNotifyPeer(await self.get_input_entity(notify))\n\n    # endregion\n"
  },
  {
    "path": "telethon/crypto/__init__.py",
    "content": "\"\"\"\nThis module contains several utilities regarding cryptographic purposes,\nsuch as the AES IGE mode used by Telegram, the authorization key bound with\ntheir data centers, and so on.\n\"\"\"\nfrom .aes import AES\nfrom .aesctr import AESModeCTR\nfrom .authkey import AuthKey\nfrom .factorization import Factorization\nfrom .cdndecrypter import CdnDecrypter\n"
  },
  {
    "path": "telethon/crypto/aes.py",
    "content": "\"\"\"\nAES IGE implementation in Python.\n\nIf available, cryptg will be used instead, otherwise\nif available, libssl will be used instead, otherwise\nthe Python implementation will be used.\n\"\"\"\nimport os\nimport pyaes\nimport logging\nfrom . import libssl\n\n\n__log__ = logging.getLogger(__name__)\n\n\ntry:\n    import cryptg\n    __log__.info('cryptg detected, it will be used for encryption')\nexcept ImportError:\n    cryptg = None\n    if libssl.encrypt_ige and libssl.decrypt_ige:\n        __log__.info('libssl detected, it will be used for encryption')\n    else:\n        __log__.info('cryptg module not installed and libssl not found, '\n                     'falling back to (slower) Python encryption')\n\n\nclass AES:\n    \"\"\"\n    Class that servers as an interface to encrypt and decrypt\n    text through the AES IGE mode.\n    \"\"\"\n    @staticmethod\n    def decrypt_ige(cipher_text, key, iv):\n        \"\"\"\n        Decrypts the given text in 16-bytes blocks by using the\n        given key and 32-bytes initialization vector.\n        \"\"\"\n        if cryptg:\n            return cryptg.decrypt_ige(cipher_text, key, iv)\n        if libssl.decrypt_ige:\n            return libssl.decrypt_ige(cipher_text, key, iv)\n\n        iv1 = iv[:len(iv) // 2]\n        iv2 = iv[len(iv) // 2:]\n\n        aes = pyaes.AES(key)\n\n        plain_text = []\n        blocks_count = len(cipher_text) // 16\n\n        cipher_text_block = [0] * 16\n        for block_index in range(blocks_count):\n            for i in range(16):\n                cipher_text_block[i] = \\\n                    cipher_text[block_index * 16 + i] ^ iv2[i]\n\n            plain_text_block = aes.decrypt(cipher_text_block)\n\n            for i in range(16):\n                plain_text_block[i] ^= iv1[i]\n\n            iv1 = cipher_text[block_index * 16:block_index * 16 + 16]\n            iv2 = plain_text_block\n\n            plain_text.extend(plain_text_block)\n\n        return bytes(plain_text)\n\n    @staticmethod\n    def encrypt_ige(plain_text, key, iv):\n        \"\"\"\n        Encrypts the given text in 16-bytes blocks by using the\n        given key and 32-bytes initialization vector.\n        \"\"\"\n        padding = len(plain_text) % 16\n        if padding:\n            plain_text += os.urandom(16 - padding)\n\n        if cryptg:\n            return cryptg.encrypt_ige(plain_text, key, iv)\n        if libssl.encrypt_ige:\n            return libssl.encrypt_ige(plain_text, key, iv)\n\n        iv1 = iv[:len(iv) // 2]\n        iv2 = iv[len(iv) // 2:]\n\n        aes = pyaes.AES(key)\n\n        cipher_text = []\n        blocks_count = len(plain_text) // 16\n\n        for block_index in range(blocks_count):\n            plain_text_block = list(\n                plain_text[block_index * 16:block_index * 16 + 16]\n            )\n            for i in range(16):\n                plain_text_block[i] ^= iv1[i]\n\n            cipher_text_block = aes.encrypt(plain_text_block)\n\n            for i in range(16):\n                cipher_text_block[i] ^= iv2[i]\n\n            iv1 = cipher_text_block\n            iv2 = plain_text[block_index * 16:block_index * 16 + 16]\n\n            cipher_text.extend(cipher_text_block)\n\n        return bytes(cipher_text)\n"
  },
  {
    "path": "telethon/crypto/aesctr.py",
    "content": "\"\"\"\nThis module holds the AESModeCTR wrapper class.\n\"\"\"\nimport pyaes\n\n\nclass AESModeCTR:\n    \"\"\"Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV\"\"\"\n    # TODO Maybe make a pull request to pyaes to support iv on CTR\n\n    def __init__(self, key, iv):\n        \"\"\"\n        Initializes the AES CTR mode with the given key/iv pair.\n\n        :param key: the key to be used as bytes.\n        :param iv: the bytes initialization vector. Must have a length of 16.\n        \"\"\"\n        # TODO Use libssl if available\n        assert isinstance(key, bytes)\n        self._aes = pyaes.AESModeOfOperationCTR(key)\n\n        assert isinstance(iv, bytes)\n        assert len(iv) == 16\n        self._aes._counter._counter = list(iv)\n\n    def encrypt(self, data):\n        \"\"\"\n        Encrypts the given plain text through AES CTR.\n\n        :param data: the plain text to be encrypted.\n        :return: the encrypted cipher text.\n        \"\"\"\n        return self._aes.encrypt(data)\n\n    def decrypt(self, data):\n        \"\"\"\n        Decrypts the given cipher text through AES CTR\n\n        :param data: the cipher text to be decrypted.\n        :return: the decrypted plain text.\n        \"\"\"\n        return self._aes.decrypt(data)\n"
  },
  {
    "path": "telethon/crypto/authkey.py",
    "content": "\"\"\"\nThis module holds the AuthKey class.\n\"\"\"\nimport struct\nfrom hashlib import sha1\n\nfrom ..extensions import BinaryReader\n\n\nclass AuthKey:\n    \"\"\"\n    Represents an authorization key, used to encrypt and decrypt\n    messages sent to Telegram's data centers.\n    \"\"\"\n    def __init__(self, data):\n        \"\"\"\n        Initializes a new authorization key.\n\n        :param data: the data in bytes that represent this auth key.\n        \"\"\"\n        self.key = data\n\n    @property\n    def key(self):\n        return self._key\n\n    @key.setter\n    def key(self, value):\n        if not value:\n            self._key = self.aux_hash = self.key_id = None\n            return\n\n        if isinstance(value, type(self)):\n            self._key, self.aux_hash, self.key_id = \\\n                value._key, value.aux_hash, value.key_id\n            return\n\n        self._key = value\n        with BinaryReader(sha1(self._key).digest()) as reader:\n            self.aux_hash = reader.read_long(signed=False)\n            reader.read(4)\n            self.key_id = reader.read_long(signed=False)\n\n    # TODO This doesn't really fit here, it's only used in authentication\n    def calc_new_nonce_hash(self, new_nonce, number):\n        \"\"\"\n        Calculates the new nonce hash based on the current attributes.\n\n        :param new_nonce: the new nonce to be hashed.\n        :param number: number to prepend before the hash.\n        :return: the hash for the given new nonce.\n        \"\"\"\n        new_nonce = new_nonce.to_bytes(32, 'little', signed=True)\n        data = new_nonce + struct.pack('<BQ', number, self.aux_hash)\n\n        # Calculates the message key from the given data\n        return int.from_bytes(sha1(data).digest()[4:20], 'little', signed=True)\n\n    def __bool__(self):\n        return bool(self._key)\n\n    def __eq__(self, other):\n        return isinstance(other, type(self)) and other.key == self._key\n"
  },
  {
    "path": "telethon/crypto/cdndecrypter.py",
    "content": "\"\"\"\nThis module holds the CdnDecrypter utility class.\n\"\"\"\nfrom hashlib import sha256\n\nfrom ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest\nfrom ..tl.types.upload import CdnFileReuploadNeeded, CdnFile\nfrom ..crypto import AESModeCTR\nfrom ..errors import CdnFileTamperedError\n\n\nclass CdnDecrypter:\n    \"\"\"\n    Used when downloading a file results in a 'FileCdnRedirect' to\n    both prepare the redirect, decrypt the file as it downloads, and\n    ensure the file hasn't been tampered. https://core.telegram.org/cdn\n    \"\"\"\n    def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):\n        \"\"\"\n        Initializes the CDN decrypter.\n\n        :param cdn_client: a client connected to a CDN.\n        :param file_token: the token of the file to be used.\n        :param cdn_aes: the AES CTR used to decrypt the file.\n        :param cdn_file_hashes: the hashes the decrypted file must match.\n        \"\"\"\n        self.client = cdn_client\n        self.file_token = file_token\n        self.cdn_aes = cdn_aes\n        self.cdn_file_hashes = cdn_file_hashes\n\n    @staticmethod\n    async def prepare_decrypter(client, cdn_client, cdn_redirect):\n        \"\"\"\n        Prepares a new CDN decrypter.\n\n        :param client: a TelegramClient connected to the main servers.\n        :param cdn_client: a new client connected to the CDN.\n        :param cdn_redirect: the redirect file object that caused this call.\n        :return: (CdnDecrypter, first chunk file data)\n        \"\"\"\n        cdn_aes = AESModeCTR(\n            key=cdn_redirect.encryption_key,\n            # 12 first bytes of the IV..4 bytes of the offset (0, big endian)\n            iv=cdn_redirect.encryption_iv[:12] + bytes(4)\n        )\n\n        # We assume that cdn_redirect.cdn_file_hashes are ordered by offset,\n        # and that there will be enough of these to retrieve the whole file.\n        decrypter = CdnDecrypter(\n            cdn_client, cdn_redirect.file_token,\n            cdn_aes, cdn_redirect.cdn_file_hashes\n        )\n\n        cdn_file = await cdn_client(GetCdnFileRequest(\n            file_token=cdn_redirect.file_token,\n            offset=cdn_redirect.cdn_file_hashes[0].offset,\n            limit=cdn_redirect.cdn_file_hashes[0].limit\n        ))\n        if isinstance(cdn_file, CdnFileReuploadNeeded):\n            # We need to use the original client here\n            await client(ReuploadCdnFileRequest(\n                file_token=cdn_redirect.file_token,\n                request_token=cdn_file.request_token\n            ))\n\n            # We want to always return a valid upload.CdnFile\n            cdn_file = decrypter.get_file()\n        else:\n            cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes)\n            cdn_hash = decrypter.cdn_file_hashes.pop(0)\n            decrypter.check(cdn_file.bytes, cdn_hash)\n\n        return decrypter, cdn_file\n\n    def get_file(self):\n        \"\"\"\n        Calls GetCdnFileRequest and decrypts its bytes.\n        Also ensures that the file hasn't been tampered.\n\n        :return: the CdnFile result.\n        \"\"\"\n        if self.cdn_file_hashes:\n            cdn_hash = self.cdn_file_hashes.pop(0)\n            cdn_file = self.client(GetCdnFileRequest(\n                self.file_token, cdn_hash.offset, cdn_hash.limit\n            ))\n            cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes)\n            self.check(cdn_file.bytes, cdn_hash)\n        else:\n            cdn_file = CdnFile(bytes(0))\n\n        return cdn_file\n\n    @staticmethod\n    def check(data, cdn_hash):\n        \"\"\"\n        Checks the integrity of the given data.\n        Raises CdnFileTamperedError if the integrity check fails.\n\n        :param data: the data to be hashed.\n        :param cdn_hash: the expected hash.\n        \"\"\"\n        if sha256(data).digest() != cdn_hash.hash:\n            raise CdnFileTamperedError()\n"
  },
  {
    "path": "telethon/crypto/factorization.py",
    "content": "\"\"\"\nThis module holds a fast Factorization class.\n\"\"\"\nfrom random import randint\n\n\nclass Factorization:\n    \"\"\"\n    Simple module to factorize large numbers really quickly.\n    \"\"\"\n    @classmethod\n    def factorize(cls, pq):\n        \"\"\"\n        Factorizes the given large integer.\n\n        Implementation from https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/.\n\n        :param pq: the prime pair pq.\n        :return: a tuple containing the two factors p and q.\n        \"\"\"\n        if pq % 2 == 0:\n            return 2, pq // 2\n\n        y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1)\n        g = r = q = 1\n        x = ys = 0\n\n        while g == 1:\n            x = y\n            for i in range(r):\n                y = (pow(y, 2, pq) + c) % pq\n\n            k = 0\n            while k < r and g == 1:\n                ys = y\n                for i in range(min(m, r - k)):\n                    y = (pow(y, 2, pq) + c) % pq\n                    q = q * (abs(x - y)) % pq\n\n                g = cls.gcd(q, pq)\n                k += m\n\n            r *= 2\n\n        if g == pq:\n            while True:\n                ys = (pow(ys, 2, pq) + c) % pq\n                g = cls.gcd(abs(x - ys), pq)\n                if g > 1:\n                    break\n\n        p, q = g, pq // g\n        return (p, q) if p < q else (q, p)\n\n    @staticmethod\n    def gcd(a, b):\n        \"\"\"\n        Calculates the Greatest Common Divisor.\n\n        :param a: the first number.\n        :param b: the second number.\n        :return: GCD(a, b)\n        \"\"\"\n        while b:\n            a, b = b, a % b\n\n        return a\n"
  },
  {
    "path": "telethon/crypto/libssl.py",
    "content": "\"\"\"\nHelper module around the system's libssl library if available for IGE mode.\n\"\"\"\nimport ctypes\nimport ctypes.util\nimport platform\nimport sys\ntry:\n    import ctypes.macholib.dyld\nexcept ImportError:\n    pass\nimport logging\nimport os\n\n__log__ = logging.getLogger(__name__)\n\n\ndef _find_ssl_lib():\n    lib = ctypes.util.find_library('ssl')\n    # macOS 10.15 segfaults on  unversioned crypto libraries.\n    # We therefore pin the current stable version here\n    # Credit for fix goes to Sarah Harvey (@worldwise001)\n    # https://www.shh.sh/2020/01/04/python-abort-trap-6.html\n    if sys.platform == 'darwin':\n        release, _version_info, _machine = platform.mac_ver()\n        ver, major, *_ = release.split('.')\n        # macOS 10.14 \"mojave\" is the last known major release\n        # to support unversioned libssl.dylib. Anything above\n        # needs specific versions\n        if int(ver) > 10 or int(ver) == 10 and int(major) > 14:\n            lib = (\n                ctypes.util.find_library('libssl.46') or\n                ctypes.util.find_library('libssl.44') or\n                ctypes.util.find_library('libssl.42')\n            )\n    if not lib:\n        raise OSError('no library called \"ssl\" found')\n\n    # First, let ctypes try to handle it itself.\n    try:\n        libssl = ctypes.cdll.LoadLibrary(lib)\n    except OSError:\n        pass\n    else:\n        return libssl\n\n    # This is a best-effort attempt at finding the full real path of lib.\n    #\n    # Unfortunately ctypes doesn't tell us *where* it finds the library,\n    # so we have to do that ourselves.\n    try:\n        # This is not documented, so it could fail. Be on the safe side.\n        paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK\n    except AttributeError:\n        paths = [\n            os.path.expanduser(\"~/lib\"),\n            \"/usr/local/lib\",\n            \"/lib\",\n            \"/usr/lib\",\n        ]\n\n    for path in paths:\n        if os.path.isdir(path):\n            for root, _, files in os.walk(path):\n                if lib in files:\n                    # Manually follow symbolic links on *nix systems.\n                    # Fix for https://github.com/LonamiWebs/Telethon/issues/1167\n                    lib = os.path.realpath(os.path.join(root, lib))\n                    return ctypes.cdll.LoadLibrary(lib)\n    else:\n        raise OSError('no absolute path for \"%s\" and cannot load by name' % lib)\n\n\ntry:\n    _libssl = _find_ssl_lib()\nexcept OSError as e:\n    # See https://github.com/LonamiWebs/Telethon/issues/1167\n    # Sometimes `find_library` returns improper filenames.\n    __log__.info('Failed to load SSL library: %s (%s)', type(e), e)\n    _libssl = None\n\nif not _libssl:\n    decrypt_ige = None\n    encrypt_ige = None\nelse:\n    # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h\n    AES_ENCRYPT = ctypes.c_int(1)\n    AES_DECRYPT = ctypes.c_int(0)\n    AES_MAXNR = 14\n\n    class AES_KEY(ctypes.Structure):\n        \"\"\"Helper class representing an AES key\"\"\"\n        _fields_ = [\n            ('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))),\n            ('rounds', ctypes.c_uint),\n        ]\n\n    def decrypt_ige(cipher_text, key, iv):\n        aes_key = AES_KEY()\n        key_len = ctypes.c_int(8 * len(key))\n        key = (ctypes.c_ubyte * len(key))(*key)\n        iv = (ctypes.c_ubyte * len(iv))(*iv)\n\n        in_len = ctypes.c_size_t(len(cipher_text))\n        in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text)\n        out_ptr = (ctypes.c_ubyte * len(cipher_text))()\n\n        _libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key))\n        _libssl.AES_ige_encrypt(\n            ctypes.byref(in_ptr),\n            ctypes.byref(out_ptr),\n            in_len,\n            ctypes.byref(aes_key),\n            ctypes.byref(iv),\n            AES_DECRYPT\n        )\n\n        return bytes(out_ptr)\n\n    def encrypt_ige(plain_text, key, iv):\n        aes_key = AES_KEY()\n        key_len = ctypes.c_int(8 * len(key))\n        key = (ctypes.c_ubyte * len(key))(*key)\n        iv = (ctypes.c_ubyte * len(iv))(*iv)\n\n        in_len = ctypes.c_size_t(len(plain_text))\n        in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text)\n        out_ptr = (ctypes.c_ubyte * len(plain_text))()\n\n        _libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key))\n        _libssl.AES_ige_encrypt(\n            ctypes.byref(in_ptr),\n            ctypes.byref(out_ptr),\n            in_len,\n            ctypes.byref(aes_key),\n            ctypes.byref(iv),\n            AES_ENCRYPT\n        )\n\n        return bytes(out_ptr)\n"
  },
  {
    "path": "telethon/crypto/rsa.py",
    "content": "\"\"\"\nThis module holds several utilities regarding RSA and server fingerprints.\n\"\"\"\nimport os\nimport struct\nfrom hashlib import sha1\ntry:\n    import rsa\n    import rsa.core\nexcept ImportError:\n    rsa = None\n    raise ImportError('Missing module \"rsa\", please install via pip.')\n\nfrom ..tl import TLObject\n\n\n# {fingerprint: (Crypto.PublicKey.RSA._RSAobj, old)} dictionary\n_server_keys = {}\n\n\ndef get_byte_array(integer):\n    \"\"\"Return the variable length bytes corresponding to the given int\"\"\"\n    # Operate in big endian (unlike most of Telegram API) since:\n    # > \"...pq is a representation of a natural number\n    #    (in binary *big endian* format)...\"\n    # > \"...current value of dh_prime equals\n    #    (in *big-endian* byte order)...\"\n    # Reference: https://core.telegram.org/mtproto/auth_key\n    return int.to_bytes(\n        integer,\n        (integer.bit_length() + 8 - 1) // 8,  # 8 bits per byte,\n        byteorder='big',\n        signed=False\n    )\n\n\ndef _compute_fingerprint(key):\n    \"\"\"\n    Given a RSA key, computes its fingerprint like Telegram does.\n\n    :param key: the Crypto.RSA key.\n    :return: its 8-bytes-long fingerprint.\n    \"\"\"\n    n = TLObject.serialize_bytes(get_byte_array(key.n))\n    e = TLObject.serialize_bytes(get_byte_array(key.e))\n    # Telegram uses the last 8 bytes as the fingerprint\n    return struct.unpack('<q', sha1(n + e).digest()[-8:])[0]\n\n\ndef add_key(pub, *, old):\n    \"\"\"Adds a new public key to be used when encrypting new data is needed\"\"\"\n    global _server_keys\n    key = rsa.PublicKey.load_pkcs1(pub)\n    _server_keys[_compute_fingerprint(key)] = (key, old)\n\n\ndef encrypt(fingerprint, data, *, use_old=False):\n    \"\"\"\n    Encrypts the given data known the fingerprint to be used\n    in the way Telegram requires us to do so (sha1(data) + data + padding)\n\n    :param fingerprint: the fingerprint of the RSA key.\n    :param data: the data to be encrypted.\n    :param use_old: whether old keys should be used.\n    :return:\n        the cipher text, or None if no key matching this fingerprint is found.\n    \"\"\"\n    global _server_keys\n    key, old = _server_keys.get(fingerprint, [None, None])\n    if (not key) or (old and not use_old):\n        return None\n\n    # len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding\n    to_encrypt = sha1(data).digest() + data + os.urandom(235 - len(data))\n\n    # rsa module rsa.encrypt adds 11 bits for padding which we don't want\n    # rsa module uses rsa.transform.bytes2int(to_encrypt), easier way:\n    payload = int.from_bytes(to_encrypt, 'big')\n    encrypted = rsa.core.encrypt_int(payload, key.e, key.n)\n    # rsa module uses transform.int2bytes(encrypted, keylength), easier:\n    block = encrypted.to_bytes(256, 'big')\n    return block\n\n\n# Add default keys\n# https://github.com/DrKLO/Telegram/blob/a724d96e9c008b609fe188d122aa2922e40de5fc/TMessagesProj/jni/tgnet/Handshake.cpp#L356-L436\nfor pub in (\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAruw2yP/BCcsJliRoW5eBVBVle9dtjJw+OYED160Wybum9SXtBBLX\nriwt4rROd9csv0t0OHCaTmRqBcQ0J8fxhN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/\nj1dnVa/gVbCjdSxpbrfY2g2L4frzjJvdl84Kd9ORYjDEAyFnEA7dD556OptgLQQ2\ne2iVNq8NZLYTzLp5YpOdO1doK+ttrltggTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnS\nLj16yE5HvJQn0CNpRdENvRUXe6tBP78O39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wF\nXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5JwIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAvfLHfYH2r9R70w8prHblWt/nDkh+XkgpflqQVcnAfSuTtO05lNPs\npQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOOKPi0OfJXoRVylFzAQG/j83u5K3kRLbae\n7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ3TDS2pQOCtovG4eDl9wacrXOJTG2990V\njgnIKNA0UMoP+KF03qzryqIt3oTvZq03DyWdGK+AZjgBLaDKSnC6qD2cFY81UryR\nWOab8zKkWAnhw2kFpcqhI0jdV5QaSCExvnsjVaX0Y1N0870931/5Jb9ICe4nweZ9\nkSDF/gip3kWLG0o8XQpChDfyvsqB9OLV/wIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAs/ditzm+mPND6xkhzwFIz6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGr\nzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+\nth6knSU0yLtNKuQVP6voMrnt9MV1X92LGZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvS\nUwwc+yi1/gGaybwlzZwqXYoPOhwMebzKUk0xW14htcJrRrq+PXXQbRzTMynseCoP\nIoke0dtCodbA3qQxQovE16q9zz4Otv2k4j63cz53J+mhkVWAeWxVGI0lltJmWtEY\nK6er8VqqWot3nqmWMXogrgRLggv/NbbooQIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q05shjg8/4p6047bn6/m8yPy1RBsvIyvuD\nuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xbnfxL5BXHplJhMtADXKM9bWB11PU1Eioc\n3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvi\nfRLJbY08/Gp66KpQvy7g8w7VB8wlgePexW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqe\nPji9NP3tJUFQjcECqcm0yV7/2d0t/pbCm+ZH1sadZspQCEPPrtbkQBlvHb4OLiIW\nPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6MAQIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n):\n    add_key(pub, old=False)\n\n\nfor pub in (\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6\nlyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS\nan9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw\nEfzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+\n8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n\nSlv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAxq7aeLAqJR20tkQQMfRn+ocfrtMlJsQ2Uksfs7Xcoo77jAid0bRt\nksiVmT2HEIJUlRxfABoPBV8wY9zRTUMaMA654pUX41mhyVN+XoerGxFvrs9dF1Ru\nvCHbI02dM2ppPvyytvvMoefRoL5BTcpAihFgm5xCaakgsJ/tH5oVl74CdhQw8J5L\nxI/K++KJBUyZ26Uba1632cOiq05JBUW0Z2vWIOk4BLysk7+U9z+SxynKiZR3/xdi\nXvFKk01R3BHV+GUKM2RYazpS/P8v7eyKhAbKxOdRcFpHLlVwfjyM1VlDQrEZxsMp\nNTLYXb6Sce1Uov0YtNx5wEowlREH1WOTlwIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAsQZnSWVZNfClk29RcDTJQ76n8zZaiTGuUsi8sUhW8AS4PSbPKDm+\nDyJgdHDWdIF3HBzl7DHeFrILuqTs0vfS7Pa2NW8nUBwiaYQmPtwEa4n7bTmBVGsB\n1700/tz8wQWOLUlL2nMv+BPlDhxq4kmJCyJfgrIrHlX8sGPcPA4Y6Rwo0MSqYn3s\ng1Pu5gOKlaT9HKmE6wn5Sut6IiBjWozrRQ6n5h2RXNtO7O2qCDqjgB2vBxhV7B+z\nhRbLbCmW0tYMDsvPpX5M8fsO05svN+lKtCAuz1leFns8piZpptpSCFn7bWxiA9/f\nx5x17D7pfah3Sy2pA+NDXyzSlGcKdaUmwQIDAQAB\n-----END RSA PUBLIC KEY-----''',\n\n        '''-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAwqjFW0pi4reKGbkc9pK83Eunwj/k0G8ZTioMMPbZmW99GivMibwa\nxDM9RDWabEMyUtGoQC2ZcDeLWRK3W8jMP6dnEKAlvLkDLfC4fXYHzFO5KHEqF06i\nqAqBdmI1iBGdQv/OQCBcbXIWCGDY2AsiqLhlGQfPOI7/vvKc188rTriocgUtoTUc\n/n/sIUzkgwTqRyvWYynWARWzQg0I9olLBBC2q5RQJJlnYXZwyTL3y9tdb7zOHkks\nWV9IMQmZmyZh/N7sMbGWQpt4NMchGpPGeJ2e5gHBjDnlIf2p1yZOYeUYrdbwcS0t\nUiggS4UeE8TzIuXFQxw7fzEIlmhIaq3FnwIDAQAB\n-----END RSA PUBLIC KEY-----''',\n):\n    add_key(pub, old=True)\n"
  },
  {
    "path": "telethon/custom.py",
    "content": "from .tl.custom import *\n"
  },
  {
    "path": "telethon/errors/__init__.py",
    "content": "\"\"\"\nThis module holds all the base and automatically generated errors that the\nTelegram API has. See telethon_generator/errors.json for more.\n\"\"\"\nimport re\n\nfrom .common import (\n    ReadCancelledError, TypeNotFoundError, InvalidChecksumError,\n    InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError,\n    AlreadyInConversationError, BadMessageError, MultiError\n)\n\n# This imports the base errors too, as they're imported there\nfrom .rpcbaseerrors import *\nfrom .rpcerrorlist import *\n\n\ndef rpc_message_to_error(rpc_error, request):\n    \"\"\"\n    Converts a Telegram's RPC Error to a Python error.\n\n    :param rpc_error: the RpcError instance.\n    :param request: the request that caused this error.\n    :return: the RPCError as a Python exception that represents this error.\n    \"\"\"\n    # Try to get the error by direct look-up, otherwise regex\n    # Case-insensitive, for things like \"timeout\" which don't conform.\n    cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None)\n    if cls:\n        return cls(request=request)\n\n    for msg_regex, cls in rpc_errors_re:\n        m = re.match(msg_regex, rpc_error.error_message)\n        if m:\n            capture = int(m.group(1)) if m.groups() else None\n            return cls(request=request, capture=capture)\n\n    # Some errors are negative:\n    # * -500 for \"No workers running\",\n    # * -503 for \"Timeout\"\n    #\n    # We treat them as if they were positive, so -500 will be treated\n    # as a `ServerError`, etc.\n    cls = base_errors.get(abs(rpc_error.error_code), RPCError)\n    return cls(request=request, message=rpc_error.error_message,\n               code=rpc_error.error_code)\n"
  },
  {
    "path": "telethon/errors/common.py",
    "content": "\"\"\"Errors not related to the Telegram API itself\"\"\"\nimport struct\nimport textwrap\n\nfrom ..tl import TLRequest\n\n\nclass ReadCancelledError(Exception):\n    \"\"\"Occurs when a read operation was cancelled.\"\"\"\n    def __init__(self):\n        super().__init__('The read operation was cancelled.')\n\n\nclass TypeNotFoundError(Exception):\n    \"\"\"\n    Occurs when a type is not found, for example,\n    when trying to read a TLObject with an invalid constructor code.\n    \"\"\"\n    def __init__(self, invalid_constructor_id, remaining):\n        super().__init__(\n            'Could not find a matching Constructor ID for the TLObject '\n            'that was supposed to be read with ID {:08x}. See the FAQ '\n            'for more details. '\n            'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))\n\n        self.invalid_constructor_id = invalid_constructor_id\n        self.remaining = remaining\n\n\nclass InvalidChecksumError(Exception):\n    \"\"\"\n    Occurs when using the TCP full mode and the checksum of a received\n    packet doesn't match the expected checksum.\n    \"\"\"\n    def __init__(self, checksum, valid_checksum):\n        super().__init__(\n            'Invalid checksum ({} when {} was expected). '\n            'This packet should be skipped.'\n            .format(checksum, valid_checksum))\n\n        self.checksum = checksum\n        self.valid_checksum = valid_checksum\n\n\nclass InvalidBufferError(BufferError):\n    \"\"\"\n    Occurs when the buffer is invalid, and may contain an HTTP error code.\n    For instance, 404 means \"forgotten/broken authorization key\", while\n    \"\"\"\n    def __init__(self, payload):\n        self.payload = payload\n        if len(payload) == 4:\n            self.code = -struct.unpack('<i', payload)[0]\n            super().__init__(\n                'Invalid response buffer (HTTP code {})'.format(self.code))\n        else:\n            self.code = None\n            super().__init__(\n                'Invalid response buffer (too short {})'.format(self.payload))\n\n\nclass AuthKeyNotFound(Exception):\n    \"\"\"\n    The server claims it doesn't know about the authorization key (session\n    file) currently being used. This might be because it either has never\n    seen this authorization key, or it used to know about the authorization\n    key but has forgotten it, either temporarily or permanently (possibly\n    due to server errors).\n\n    If the issue persists, you may need to recreate the session file and login\n    again. This is not done automatically because it is not possible to know\n    if the issue is temporary or permanent.\n    \"\"\"\n    def __init__(self):\n        super().__init__(textwrap.dedent(self.__class__.__doc__))\n\n\nclass SecurityError(Exception):\n    \"\"\"\n    Generic security error, mostly used when generating a new AuthKey.\n    \"\"\"\n    def __init__(self, *args):\n        if not args:\n            args = ['A security check failed.']\n        super().__init__(*args)\n\n\nclass CdnFileTamperedError(SecurityError):\n    \"\"\"\n    Occurs when there's a hash mismatch between the decrypted CDN file\n    and its expected hash.\n    \"\"\"\n    def __init__(self):\n        super().__init__(\n            'The CDN file has been altered and its download cancelled.'\n        )\n\n\nclass AlreadyInConversationError(Exception):\n    \"\"\"\n    Occurs when another exclusive conversation is opened in the same chat.\n    \"\"\"\n    def __init__(self):\n        super().__init__(\n            'Cannot open exclusive conversation in a '\n            'chat that already has one open conversation'\n        )\n\n\nclass BadMessageError(Exception):\n    \"\"\"Occurs when handling a bad_message_notification.\"\"\"\n    ErrorMessages = {\n        16:\n        'msg_id too low (most likely, client time is wrong it would be '\n        'worthwhile to synchronize it using msg_id notifications and re-send '\n        'the original message with the \"correct\" msg_id or wrap it in a '\n        'container with a new msg_id if the original message had waited too '\n        'long on the client to be transmitted).',\n        17:\n        'msg_id too high (similar to the previous case, the client time has '\n        'to be synchronized, and the message re-sent with the correct msg_id).',\n        18:\n        'Incorrect two lower order msg_id bits (the server expects client '\n        'message msg_id to be divisible by 4).',\n        19:\n        'Container msg_id is the same as msg_id of a previously received '\n        'message (this must never happen).',\n        20:\n        'Message too old, and it cannot be verified whether the server has '\n        'received a message with this msg_id or not.',\n        32:\n        'msg_seqno too low (the server has already received a message with a '\n        'lower msg_id but with either a higher or an equal and odd seqno).',\n        33:\n        'msg_seqno too high (similarly, there is a message with a higher '\n        'msg_id but with either a lower or an equal and odd seqno).',\n        34:\n        'An even msg_seqno expected (irrelevant message), but odd received.',\n        35:\n        'Odd msg_seqno expected (relevant message), but even received.',\n        48:\n        'Incorrect server salt (in this case, the bad_server_salt response '\n        'is received with the correct salt, and the message is to be re-sent '\n        'with it).',\n        64:\n        'Invalid container.'\n    }\n\n    def __init__(self, request, code):\n        super().__init__(request, self.ErrorMessages.get(\n            code,\n            'Unknown error code (this should not happen): {}.'.format(code)))\n\n        self.code = code\n\n\nclass MultiError(Exception):\n    \"\"\"Exception container for multiple `TLRequest`'s.\"\"\"\n\n    def __new__(cls, exceptions, result, requests):\n        if len(result) != len(exceptions) != len(requests):\n            raise ValueError(\n                'Need result, exception and request for each error')\n        for e, req in zip(exceptions, requests):\n            if not isinstance(e, BaseException) and e is not None:\n                raise TypeError(\n                    \"Expected an exception object, not '%r'\" % e\n                )\n            if not isinstance(req, TLRequest):\n                raise TypeError(\n                    \"Expected TLRequest object, not '%r'\" % req\n                )\n\n        if len(exceptions) == 1:\n            return exceptions[0]\n        self = BaseException.__new__(cls)\n        self.exceptions = list(exceptions)\n        self.results = list(result)\n        self.requests = list(requests)\n        return self\n"
  },
  {
    "path": "telethon/errors/rpcbaseerrors.py",
    "content": "from ..tl import functions\n\n_NESTS_QUERY = (\n    functions.InvokeAfterMsgRequest,\n    functions.InvokeAfterMsgsRequest,\n    functions.InitConnectionRequest,\n    functions.InvokeWithLayerRequest,\n    functions.InvokeWithoutUpdatesRequest,\n    functions.InvokeWithMessagesRangeRequest,\n    functions.InvokeWithTakeoutRequest,\n)\n\nclass RPCError(Exception):\n    \"\"\"Base class for all Remote Procedure Call errors.\"\"\"\n    code = None\n    message = None\n\n    def __init__(self, request, message, code=None):\n        super().__init__('RPCError {}: {}{}'.format(\n            code or self.code, message, self._fmt_request(request)))\n\n        self.request = request\n        self.code = code\n        self.message = message\n\n    @staticmethod\n    def _fmt_request(request):\n        n = 0\n        reason = ''\n        while isinstance(request, _NESTS_QUERY):\n            n += 1\n            reason += request.__class__.__name__ + '('\n            request = request.query\n        reason += request.__class__.__name__ + ')' * n\n\n        return ' (caused by {})'.format(reason)\n\n    def __reduce__(self):\n        return type(self), (self.request, self.message, self.code)\n\n\nclass InvalidDCError(RPCError):\n    \"\"\"\n    The request must be repeated, but directed to a different data center.\n    \"\"\"\n    code = 303\n    message = 'ERROR_SEE_OTHER'\n\n\nclass BadRequestError(RPCError):\n    \"\"\"\n    The query contains errors. In the event that a request was created\n    using a form and contains user generated data, the user should be\n    notified that the data must be corrected before the query is repeated.\n    \"\"\"\n    code = 400\n    message = 'BAD_REQUEST'\n\n\nclass UnauthorizedError(RPCError):\n    \"\"\"\n    There was an unauthorized attempt to use functionality available only\n    to authorized users.\n    \"\"\"\n    code = 401\n    message = 'UNAUTHORIZED'\n\n\nclass ForbiddenError(RPCError):\n    \"\"\"\n    Privacy violation. For example, an attempt to write a message to\n    someone who has blacklisted the current user.\n    \"\"\"\n    code = 403\n    message = 'FORBIDDEN'\n\n\nclass NotFoundError(RPCError):\n    \"\"\"\n    An attempt to invoke a non-existent object, such as a method.\n    \"\"\"\n    code = 404\n    message = 'NOT_FOUND'\n\n\nclass AuthKeyError(RPCError):\n    \"\"\"\n    Errors related to invalid authorization key, like\n    AUTH_KEY_DUPLICATED which can cause the connection to fail.\n    \"\"\"\n    code = 406\n    message = 'AUTH_KEY'\n\n\nclass FloodError(RPCError):\n    \"\"\"\n    The maximum allowed number of attempts to invoke the given method\n    with the given input parameters has been exceeded. For example, in an\n    attempt to request a large number of text messages (SMS) for the same\n    phone number.\n    \"\"\"\n    code = 420\n    message = 'FLOOD'\n\n\nclass ServerError(RPCError):\n    \"\"\"\n    An internal server error occurred while a request was being processed\n    for example, there was a disruption while accessing a database or file\n    storage.\n    \"\"\"\n    code = 500  # Also witnessed as -500\n    message = 'INTERNAL'\n\n\nclass TimedOutError(RPCError):\n    \"\"\"\n    Clicking the inline buttons of bots that never (or take to long to)\n    call ``answerCallbackQuery`` will result in this \"special\" RPCError.\n    \"\"\"\n    code = 503  # Only witnessed as -503\n    message = 'Timeout'\n\n\nBotTimeout = TimedOutError\n\n\nbase_errors = {x.code: x for x in (\n    InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError,\n    NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError\n)}\n"
  },
  {
    "path": "telethon/events/__init__.py",
    "content": "from .raw import Raw\nfrom .album import Album\nfrom .chataction import ChatAction\nfrom .messagedeleted import MessageDeleted\nfrom .messageedited import MessageEdited\nfrom .messageread import MessageRead\nfrom .newmessage import NewMessage\nfrom .userupdate import UserUpdate\nfrom .callbackquery import CallbackQuery\nfrom .inlinequery import InlineQuery\n\n\n_HANDLERS_ATTRIBUTE = '__tl.handlers'\n\n\nclass StopPropagation(Exception):\n    \"\"\"\n    If this exception is raised in any of the handlers for a given event,\n    it will stop the execution of all other registered event handlers.\n    It can be seen as the ``StopIteration`` in a for loop but for events.\n\n    Example usage:\n\n        >>> from telethon import TelegramClient, events\n        >>> client = TelegramClient(...)\n        >>>\n        >>> @client.on(events.NewMessage)\n        ... async def delete(event):\n        ...     await event.delete()\n        ...     # No other event handler will have a chance to handle this event\n        ...     raise StopPropagation\n        ...\n        >>> @client.on(events.NewMessage)\n        ... async def _(event):\n        ...     # Will never be reached, because it is the second handler\n        ...     pass\n    \"\"\"\n    # For some reason Sphinx wants the silly >>> or\n    # it will show warnings and look bad when generated.\n    pass\n\n\ndef register(event=None):\n    \"\"\"\n    Decorator method to *register* event handlers. This is the client-less\n    `add_event_handler()\n    <telethon.client.updates.UpdateMethods.add_event_handler>` variant.\n\n    Note that this method only registers callbacks as handlers,\n    and does not attach them to any client. This is useful for\n    external modules that don't have access to the client, but\n    still want to define themselves as a handler. Example:\n\n    >>> from telethon import events\n    >>> @events.register(events.NewMessage)\n    ... async def handler(event):\n    ...     ...\n    ...\n    >>> # (somewhere else)\n    ...\n    >>> from telethon import TelegramClient\n    >>> client = TelegramClient(...)\n    >>> client.add_event_handler(handler)\n\n    Remember that you can use this as a non-decorator\n    through ``register(event)(callback)``.\n\n    Args:\n        event (`_EventBuilder` | `type`):\n            The event builder class or instance to be used,\n            for instance ``events.NewMessage``.\n    \"\"\"\n    if isinstance(event, type):\n        event = event()\n    elif not event:\n        event = Raw()\n\n    def decorator(callback):\n        handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])\n        handlers.append(event)\n        setattr(callback, _HANDLERS_ATTRIBUTE, handlers)\n        return callback\n\n    return decorator\n\n\ndef unregister(callback, event=None):\n    \"\"\"\n    Inverse operation of `register` (though not a decorator). Client-less\n    `remove_event_handler\n    <telethon.client.updates.UpdateMethods.remove_event_handler>`\n    variant. **Note that this won't remove handlers from the client**,\n    because it simply can't, so you would generally use this before\n    adding the handlers to the client.\n\n    This method is here for symmetry. You will rarely need to\n    unregister events, since you can simply just not add them\n    to any client.\n\n    If no event is given, all events for this callback are removed.\n    Returns how many callbacks were removed.\n    \"\"\"\n    found = 0\n    if event and not isinstance(event, type):\n        event = type(event)\n\n    handlers = getattr(callback, _HANDLERS_ATTRIBUTE, [])\n    handlers.append((event, callback))\n    i = len(handlers)\n    while i:\n        i -= 1\n        ev = handlers[i]\n        if not event or isinstance(ev, event):\n            del handlers[i]\n            found += 1\n\n    return found\n\n\ndef is_handler(callback):\n    \"\"\"\n    Returns `True` if the given callback is an\n    event handler (i.e. you used `register` on it).\n    \"\"\"\n    return hasattr(callback, _HANDLERS_ATTRIBUTE)\n\n\ndef list(callback):\n    \"\"\"\n    Returns a list containing the registered event\n    builders inside the specified callback handler.\n    \"\"\"\n    return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:]\n\n\ndef _get_handlers(callback):\n    \"\"\"\n    Like ``list`` but returns `None` if the callback was never registered.\n    \"\"\"\n    return getattr(callback, _HANDLERS_ATTRIBUTE, None)\n"
  },
  {
    "path": "telethon/events/album.py",
    "content": "import asyncio\nimport time\nimport weakref\n\nfrom .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils\nfrom ..tl import types\nfrom ..tl.custom.sendergetter import SenderGetter\n\n_IGNORE_MAX_SIZE = 100  # len()\n_IGNORE_MAX_AGE = 5  # seconds\n\n# IDs to ignore, and when they were added. If it grows too large, we will\n# remove old entries. Although it should generally not be bigger than 10,\n# it may be possible some updates are not processed and thus not removed.\n_IGNORE_DICT = {}\n\n\n_HACK_DELAY = 0.5\n\n\nclass AlbumHack:\n    \"\"\"\n    When receiving an album from a different data-center, they will come in\n    separate `Updates`, so we need to temporarily remember them for a while\n    and only after produce the event.\n\n    Of course events are not designed for this kind of wizardy, so this is\n    a dirty hack that gets the job done.\n\n    When cleaning up the code base we may want to figure out a better way\n    to do this, or just leave the album problem to the users; the update\n    handling code is bad enough as it is.\n    \"\"\"\n    def __init__(self, client, event):\n        # It's probably silly to use a weakref here because this object is\n        # very short-lived but might as well try to do \"the right thing\".\n        self._client = weakref.ref(client)\n        self._event = event  # parent event\n        self._due = client.loop.time() + _HACK_DELAY\n\n        client.loop.create_task(self.deliver_event())\n\n    def extend(self, messages):\n        client = self._client()\n        if client:  # weakref may be dead\n            self._event.messages.extend(messages)\n            self._due = client.loop.time() + _HACK_DELAY\n\n    async def deliver_event(self):\n        while True:\n            client = self._client()\n            if client is None:\n                return  # weakref is dead, nothing to deliver\n\n            diff = self._due - client.loop.time()\n            if diff <= 0:\n                # We've hit our due time, deliver event. It won't respect\n                # sequential updates but fixing that would just worsen this.\n                await client._dispatch_event(self._event)\n                return\n\n            del client  # Clear ref and sleep until our due time\n            await asyncio.sleep(diff)\n\n\n@name_inner_event\nclass Album(EventBuilder):\n    \"\"\"\n    Occurs whenever you receive an album. This event only exists\n    to ease dealing with an unknown amount of messages that belong\n    to the same album.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.Album)\n            async def handler(event):\n                # Counting how many photos or videos the album has\n                print('Got an album with', len(event), 'items')\n\n                # Forwarding the album as a whole to some chat\n                event.forward_to(chat)\n\n                # Printing the caption\n                print(event.text)\n\n                # Replying to the fifth item in the album\n                await event.messages[4].reply('Cool!')\n    \"\"\"\n\n    def __init__(\n            self, chats=None, *, blacklist_chats=False, func=None):\n        super().__init__(chats, blacklist_chats=blacklist_chats, func=func)\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        # TODO normally we'd only check updates if they come with other updates\n        # but MessageBox is not designed for this so others will always be None.\n        # In essence we always rely on AlbumHack rather than returning early if not others.\n        others = [update]\n\n        if isinstance(update,\n                      (types.UpdateNewMessage, types.UpdateNewChannelMessage)):\n            if not isinstance(update.message, types.Message):\n                return  # We don't care about MessageService's here\n\n            group = update.message.grouped_id\n            if group is None:\n                return  # It must be grouped\n\n            # Check whether we are supposed to skip this update, and\n            # if we do also remove it from the ignore list since we\n            # won't need to check against it again.\n            if _IGNORE_DICT.pop(id(update), None):\n                return\n\n            # Check if the ignore list is too big, and if it is clean it\n            # TODO time could technically go backwards; time is not monotonic\n            now = time.time()\n            if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE:\n                for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]:\n                    del _IGNORE_DICT[i]\n\n            # Add the other updates to the ignore list\n            for u in others:\n                if u is not update:\n                    _IGNORE_DICT[id(u)] = now\n\n            # Figure out which updates share the same group and use those\n            return cls.Event([\n                u.message for u in others\n                if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage))\n                    and isinstance(u.message, types.Message)\n                    and u.message.grouped_id == group)\n            ])\n\n    def filter(self, event):\n        # Albums with less than two messages require a few hacks to work.\n        if len(event.messages) > 1:\n            return super().filter(event)\n\n    class Event(EventCommon, SenderGetter):\n        \"\"\"\n        Represents the event of a new album.\n\n        Members:\n            messages (Sequence[`Message <telethon.tl.custom.message.Message>`]):\n                The list of messages belonging to the same album.\n        \"\"\"\n        def __init__(self, messages):\n            message = messages[0]\n            super().__init__(chat_peer=message.peer_id,\n                             msg_id=message.id, broadcast=bool(message.post))\n            SenderGetter.__init__(self, message.sender_id)\n            self.messages = messages\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            self._sender, self._input_sender = utils._get_entity_pair(\n                self.sender_id, self._entities, client._mb_entity_cache)\n\n            for msg in self.messages:\n                msg._finish_init(client, self._entities, None)\n\n            if len(self.messages) == 1:\n                # This will require hacks to be a proper album event\n                hack = client._albums.get(self.grouped_id)\n                if hack is None:\n                    client._albums[self.grouped_id] = AlbumHack(client, self)\n                else:\n                    hack.extend(self.messages)\n\n        @property\n        def grouped_id(self):\n            \"\"\"\n            The shared ``grouped_id`` between all the messages.\n            \"\"\"\n            return self.messages[0].grouped_id\n\n        @property\n        def text(self):\n            \"\"\"\n            The message text of the first photo with a caption,\n            formatted using the client's default parse mode.\n            \"\"\"\n            return next((m.text for m in self.messages if m.text), '')\n\n        @property\n        def raw_text(self):\n            \"\"\"\n            The raw message text of the first photo\n            with a caption, ignoring any formatting.\n            \"\"\"\n            return next((m.raw_text for m in self.messages if m.raw_text), '')\n\n        @property\n        def is_reply(self):\n            \"\"\"\n            `True` if the album is a reply to some other message.\n\n            Remember that you can access the ID of the message\n            this one is replying to through `reply_to_msg_id`,\n            and the `Message` object with `get_reply_message()`.\n            \"\"\"\n            # Each individual message in an album all reply to the same message\n            return self.messages[0].is_reply\n\n        @property\n        def forward(self):\n            \"\"\"\n            The `Forward <telethon.tl.custom.forward.Forward>`\n            information for the first message in the album if it was forwarded.\n            \"\"\"\n            # Each individual message in an album all reply to the same message\n            return self.messages[0].forward\n\n        # endregion Public Properties\n\n        # region Public Methods\n\n        async def get_reply_message(self):\n            \"\"\"\n            The `Message <telethon.tl.custom.message.Message>`\n            that this album is replying to, or `None`.\n\n            The result will be cached after its first use.\n            \"\"\"\n            return await self.messages[0].get_reply_message()\n\n        async def respond(self, *args, **kwargs):\n            \"\"\"\n            Responds to the album (not as a reply). Shorthand for\n            `telethon.client.messages.MessageMethods.send_message`\n            with ``entity`` already set.\n            \"\"\"\n            return await self.messages[0].respond(*args, **kwargs)\n\n        async def reply(self, *args, **kwargs):\n            \"\"\"\n            Replies to the first photo in the album (as a reply). Shorthand\n            for `telethon.client.messages.MessageMethods.send_message`\n            with both ``entity`` and ``reply_to`` already set.\n            \"\"\"\n            return await self.messages[0].reply(*args, **kwargs)\n\n        async def forward_to(self, *args, **kwargs):\n            \"\"\"\n            Forwards the entire album. Shorthand for\n            `telethon.client.messages.MessageMethods.forward_messages`\n            with both ``messages`` and ``from_peer`` already set.\n            \"\"\"\n            if self._client:\n                kwargs['messages'] = self.messages\n                kwargs['from_peer'] = await self.get_input_chat()\n                return await self._client.forward_messages(*args, **kwargs)\n\n        async def edit(self, *args, **kwargs):\n            \"\"\"\n            Edits the first caption or the message, or the first messages'\n            caption if no caption is set, iff it's outgoing. Shorthand for\n            `telethon.client.messages.MessageMethods.edit_message`\n            with both ``entity`` and ``message`` already set.\n\n            Returns `None` if the message was incoming,\n            or the edited `Message` otherwise.\n\n            .. note::\n\n                This is different from `client.edit_message\n                <telethon.client.messages.MessageMethods.edit_message>`\n                and **will respect** the previous state of the message.\n                For example, if the message didn't have a link preview,\n                the edit won't add one by default, and you should force\n                it by setting it to `True` if you want it.\n\n                This is generally the most desired and convenient behaviour,\n                and will work for link previews and message buttons.\n            \"\"\"\n            for msg in self.messages:\n                if msg.raw_text:\n                    return await msg.edit(*args, **kwargs)\n\n            return await self.messages[0].edit(*args, **kwargs)\n\n        async def delete(self, *args, **kwargs):\n            \"\"\"\n            Deletes the entire album. You're responsible for checking whether\n            you have the permission to do so, or to except the error otherwise.\n            Shorthand for\n            `telethon.client.messages.MessageMethods.delete_messages` with\n            ``entity`` and ``message_ids`` already set.\n            \"\"\"\n            if self._client:\n                return await self._client.delete_messages(\n                    await self.get_input_chat(), self.messages,\n                    *args, **kwargs\n                )\n\n        async def mark_read(self):\n            \"\"\"\n            Marks the entire album as read. Shorthand for\n            `client.send_read_acknowledge()\n            <telethon.client.messages.MessageMethods.send_read_acknowledge>`\n            with both ``entity`` and ``message`` already set.\n            \"\"\"\n            if self._client:\n                await self._client.send_read_acknowledge(\n                    await self.get_input_chat(), max_id=self.messages[-1].id)\n\n        async def pin(self, *, notify=False):\n            \"\"\"\n            Pins the first photo in the album. Shorthand for\n            `telethon.client.messages.MessageMethods.pin_message`\n            with both ``entity`` and ``message`` already set.\n            \"\"\"\n            return await self.messages[0].pin(notify=notify)\n\n        def __len__(self):\n            \"\"\"\n            Return the amount of messages in the album.\n\n            Equivalent to ``len(self.messages)``.\n            \"\"\"\n            return len(self.messages)\n\n        def __iter__(self):\n            \"\"\"\n            Iterate over the messages in the album.\n\n            Equivalent to ``iter(self.messages)``.\n            \"\"\"\n            return iter(self.messages)\n\n        def __getitem__(self, n):\n            \"\"\"\n            Access the n'th message in the album.\n\n            Equivalent to ``event.messages[n]``.\n            \"\"\"\n            return self.messages[n]\n"
  },
  {
    "path": "telethon/events/callbackquery.py",
    "content": "import re\nimport struct\n\nfrom .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils\nfrom ..tl import types, functions\nfrom ..tl.custom.sendergetter import SenderGetter\n\n\n@name_inner_event\nclass CallbackQuery(EventBuilder):\n    \"\"\"\n    Occurs whenever you sign in as a bot and a user\n    clicks one of the inline buttons on your messages.\n\n    Note that the `chats` parameter will **not** work with normal\n    IDs or peers if the clicked inline button comes from a \"via bot\"\n    message. The `chats` parameter also supports checking against the\n    `chat_instance` which should be used for inline callbacks.\n\n    Args:\n        data (`bytes`, `str`, `callable`, optional):\n            If set, the inline button payload data must match this data.\n            A UTF-8 string can also be given, a regex or a callable. For\n            instance, to check against ``'data_1'`` and ``'data_2'`` you\n            can use ``re.compile(b'data_')``.\n\n        pattern (`bytes`, `str`, `callable`, `Pattern`, optional):\n            If set, only buttons with payload matching this pattern will be handled.\n            You can specify a regex-like string which will be matched\n            against the payload data, a callable function that returns `True`\n            if a the payload data is acceptable, or a compiled regex pattern.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events, Button\n\n            # Handle all callback queries and check data inside the handler\n            @client.on(events.CallbackQuery)\n            async def handler(event):\n                if event.data == b'yes':\n                    await event.answer('Correct answer!')\n\n            # Handle only callback queries with data being b'no'\n            @client.on(events.CallbackQuery(data=b'no'))\n            async def handler(event):\n                # Pop-up message with alert\n                await event.answer('Wrong answer!', alert=True)\n\n            # Send a message with buttons users can click\n            async def main():\n                await client.send_message(user, 'Yes or no?', buttons=[\n                    Button.inline('Yes!', b'yes'),\n                    Button.inline('Nope', b'no')\n                ])\n    \"\"\"\n    def __init__(\n            self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None):\n        super().__init__(chats, blacklist_chats=blacklist_chats, func=func)\n\n        if data and pattern:\n            raise ValueError(\"Only pass either data or pattern not both.\")\n\n        if isinstance(data, str):\n            data = data.encode('utf-8')\n        if isinstance(pattern, str):\n            pattern = pattern.encode('utf-8')\n\n        match = data if data else pattern\n\n        if isinstance(match, bytes):\n            self.match = data if data else re.compile(pattern).match\n        elif not match or callable(match):\n            self.match = match\n        elif hasattr(match, 'match') and callable(match.match):\n            if not isinstance(getattr(match, 'pattern', b''), bytes):\n                match = re.compile(match.pattern.encode('utf-8'),\n                                   match.flags & (~re.UNICODE))\n\n            self.match = match.match\n        else:\n            raise TypeError('Invalid data or pattern type given')\n\n        self._no_check = all(x is None for x in (\n            self.chats, self.func, self.match,\n        ))\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, types.UpdateBotCallbackQuery):\n            return cls.Event(update, update.peer, update.msg_id)\n        elif isinstance(update, types.UpdateInlineBotCallbackQuery):\n            # See https://github.com/LonamiWebs/Telethon/pull/1005\n            # The long message ID is actually just msg_id + peer_id\n            mid, pid = struct.unpack('<ii', struct.pack('<q', update.msg_id.id))\n            peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid)\n            return cls.Event(update, peer, mid)\n\n    def filter(self, event):\n        # We can't call super().filter(...) because it ignores chat_instance\n        if self._no_check:\n            return event\n\n        if self.chats is not None:\n            inside = event.query.chat_instance in self.chats\n            if event.chat_id:\n                inside |= event.chat_id in self.chats\n\n            if inside == self.blacklist_chats:\n                return\n\n        if self.match:\n            if callable(self.match):\n                event.data_match = event.pattern_match = self.match(event.query.data)\n                if not event.data_match:\n                    return\n            elif event.query.data != self.match:\n                return\n\n        if self.func:\n            # Return the result of func directly as it may need to be awaited\n            return self.func(event)\n        return True\n\n    class Event(EventCommon, SenderGetter):\n        \"\"\"\n        Represents the event of a new callback query.\n\n        Members:\n            query (:tl:`UpdateBotCallbackQuery`):\n                The original :tl:`UpdateBotCallbackQuery`.\n\n            data_match (`obj`, optional):\n                The object returned by the ``data=`` parameter\n                when creating the event builder, if any. Similar\n                to ``pattern_match`` for the new message event.\n\n            pattern_match (`obj`, optional):\n                Alias for ``data_match``.\n        \"\"\"\n        def __init__(self, query, peer, msg_id):\n            super().__init__(peer, msg_id=msg_id)\n            SenderGetter.__init__(self, query.user_id)\n            self.query = query\n            self.data_match = None\n            self.pattern_match = None\n            self._message = None\n            self._answered = False\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            self._sender, self._input_sender = utils._get_entity_pair(\n                self.sender_id, self._entities, client._mb_entity_cache)\n\n        @property\n        def id(self):\n            \"\"\"\n            Returns the query ID. The user clicking the inline\n            button is the one who generated this random ID.\n            \"\"\"\n            return self.query.query_id\n\n        @property\n        def message_id(self):\n            \"\"\"\n            Returns the message ID to which the clicked inline button belongs.\n            \"\"\"\n            return self._message_id\n\n        @property\n        def data(self):\n            \"\"\"\n            Returns the data payload from the original inline button.\n            \"\"\"\n            return self.query.data\n\n        @property\n        def chat_instance(self):\n            \"\"\"\n            Unique identifier for the chat where the callback occurred.\n            Useful for high scores in games.\n            \"\"\"\n            return self.query.chat_instance\n\n        async def get_message(self):\n            \"\"\"\n            Returns the message to which the clicked inline button belongs.\n            \"\"\"\n            if self._message is not None:\n                return self._message\n\n            try:\n                chat = await self.get_input_chat() if self.is_channel else None\n                self._message = await self._client.get_messages(\n                    chat, ids=self._message_id)\n            except ValueError:\n                return\n\n            return self._message\n\n        async def _refetch_sender(self):\n            self._sender = self._entities.get(self.sender_id)\n            if not self._sender:\n                return\n\n            self._input_sender = utils.get_input_peer(self._chat)\n            if not getattr(self._input_sender, 'access_hash', True):\n                # getattr with True to handle the InputPeerSelf() case\n                try:\n                    self._input_sender = self._client._mb_entity_cache.get(\n                        utils.resolve_id(self._sender_id)[0])._as_input_peer()\n                except AttributeError:\n                    m = await self.get_message()\n                    if m:\n                        self._sender = m._sender\n                        self._input_sender = m._input_sender\n\n        async def answer(\n                self, message=None, cache_time=0, *, url=None, alert=False):\n            \"\"\"\n            Answers the callback query (and stops the loading circle).\n\n            Args:\n                message (`str`, optional):\n                    The toast message to show feedback to the user.\n\n                cache_time (`int`, optional):\n                    For how long this result should be cached on\n                    the user's client. Defaults to 0 for no cache.\n\n                url (`str`, optional):\n                    The URL to be opened in the user's client. Note that\n                    the only valid URLs are those of games your bot has,\n                    or alternatively a 't.me/your_bot?start=xyz' parameter.\n\n                alert (`bool`, optional):\n                    Whether an alert (a pop-up dialog) should be used\n                    instead of showing a toast. Defaults to `False`.\n            \"\"\"\n            if self._answered:\n                return\n\n            self._answered = True\n            return await self._client(\n                functions.messages.SetBotCallbackAnswerRequest(\n                    query_id=self.query.query_id,\n                    cache_time=cache_time,\n                    alert=alert,\n                    message=message,\n                    url=url\n                )\n            )\n\n        @property\n        def via_inline(self):\n            \"\"\"\n            Whether this callback was generated from an inline button sent\n            via an inline query or not. If the bot sent the message itself\n            with buttons, and one of those is clicked, this will be `False`.\n            If a user sent the message coming from an inline query to the\n            bot, and one of those is clicked, this will be `True`.\n\n            If it's `True`, it's likely that the bot is **not** in the\n            chat, so methods like `respond` or `delete` won't work (but\n            `edit` will always work).\n            \"\"\"\n            return isinstance(self.query, types.UpdateInlineBotCallbackQuery)\n\n        async def respond(self, *args, **kwargs):\n            \"\"\"\n            Responds to the message (not as a reply). Shorthand for\n            `telethon.client.messages.MessageMethods.send_message` with\n            ``entity`` already set.\n\n            This method also creates a task to `answer` the callback.\n\n            This method will likely fail if `via_inline` is `True`.\n            \"\"\"\n            self._client.loop.create_task(self.answer())\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n        async def reply(self, *args, **kwargs):\n            \"\"\"\n            Replies to the message (as a reply). Shorthand for\n            `telethon.client.messages.MessageMethods.send_message` with\n            both ``entity`` and ``reply_to`` already set.\n\n            This method also creates a task to `answer` the callback.\n\n            This method will likely fail if `via_inline` is `True`.\n            \"\"\"\n            self._client.loop.create_task(self.answer())\n            kwargs['reply_to'] = self.query.msg_id\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n        async def edit(self, *args, **kwargs):\n            \"\"\"\n            Edits the message. Shorthand for\n            `telethon.client.messages.MessageMethods.edit_message` with\n            the ``entity`` set to the correct :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`.\n\n            Returns `True` if the edit was successful.\n\n            This method also creates a task to `answer` the callback.\n\n            .. note::\n\n                This method won't respect the previous message unlike\n                `Message.edit <telethon.tl.custom.message.Message.edit>`,\n                since the message object is normally not present.\n            \"\"\"\n            self._client.loop.create_task(self.answer())\n            if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):\n                return await self._client.edit_message(\n                    self.query.msg_id, *args, **kwargs\n                )\n            else:\n                return await self._client.edit_message(\n                    await self.get_input_chat(), self.query.msg_id,\n                    *args, **kwargs\n                )\n\n        async def delete(self, *args, **kwargs):\n            \"\"\"\n            Deletes the message. Shorthand for\n            `telethon.client.messages.MessageMethods.delete_messages` with\n            ``entity`` and ``message_ids`` already set.\n\n            If you need to delete more than one message at once, don't use\n            this `delete` method. Use a\n            `telethon.client.telegramclient.TelegramClient` instance directly.\n\n            This method also creates a task to `answer` the callback.\n\n            This method will likely fail if `via_inline` is `True`.\n            \"\"\"\n            self._client.loop.create_task(self.answer())\n            if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):\n                raise TypeError('Inline messages cannot be deleted as there is no API request available to do so')\n            return await self._client.delete_messages(\n                await self.get_input_chat(), [self.query.msg_id],\n                *args, **kwargs\n            )\n"
  },
  {
    "path": "telethon/events/chataction.py",
    "content": "from .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils\nfrom ..tl import types\n\n\n@name_inner_event\nclass ChatAction(EventBuilder):\n    \"\"\"\n    Occurs on certain chat actions:\n\n    * Whenever a new chat is created.\n    * Whenever a chat's title or photo is changed or removed.\n    * Whenever a new message is pinned.\n    * Whenever a user scores in a game.\n    * Whenever a user joins or is added to the group.\n    * Whenever a user is removed or leaves a group if it has\n      less than 50 members or the removed user was a bot.\n\n    Note that \"chat\" refers to \"small group, megagroup and broadcast\n    channel\", whereas \"group\" refers to \"small group and megagroup\" only.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.ChatAction)\n            async def handler(event):\n                # Welcome every new user\n                if event.user_joined:\n                    await event.reply('Welcome to the group!')\n    \"\"\"\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        # Rely on specific pin updates for unpins, but otherwise ignore them\n        # for new pins (we'd rather handle the new service message with pin,\n        # so that we can act on that message').\n        if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned:\n            return cls.Event(types.PeerChannel(update.channel_id),\n                             pin_ids=update.messages,\n                             pin=update.pinned)\n\n        elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned:\n            return cls.Event(update.peer,\n                             pin_ids=update.messages,\n                             pin=update.pinned)\n\n        elif isinstance(update, types.UpdateChatParticipantAdd):\n            return cls.Event(types.PeerChat(update.chat_id),\n                             added_by=update.inviter_id or True,\n                             users=update.user_id)\n\n        elif isinstance(update, types.UpdateChatParticipantDelete):\n            return cls.Event(types.PeerChat(update.chat_id),\n                             kicked_by=True,\n                             users=update.user_id)\n\n        # UpdateChannel is sent if we leave a channel, and the update._entities\n        # set by _process_update would let us make some guesses. However it's\n        # better not to rely on this. Rely only in MessageActionChatDeleteUser.\n\n        elif (isinstance(update, (\n                types.UpdateNewMessage, types.UpdateNewChannelMessage))\n              and isinstance(update.message, types.MessageService)):\n            msg = update.message\n            action = update.message.action\n            if isinstance(action, types.MessageActionChatJoinedByLink):\n                return cls.Event(msg,\n                                 added_by=True,\n                                 users=msg.from_id)\n            elif isinstance(action, types.MessageActionChatAddUser):\n                # If a user adds itself, it means they joined via the public chat username\n                added_by = ([msg.sender_id] == action.users) or msg.from_id\n                return cls.Event(msg,\n                                 added_by=added_by,\n                                 users=action.users)\n            elif isinstance(action, types.MessageActionChatDeleteUser):\n                return cls.Event(msg,\n                                 kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True,\n                                 users=action.user_id)\n            elif isinstance(action, types.MessageActionChatCreate):\n                return cls.Event(msg,\n                                 users=action.users,\n                                 created=True,\n                                 new_title=action.title)\n            elif isinstance(action, types.MessageActionChannelCreate):\n                return cls.Event(msg,\n                                 created=True,\n                                 users=msg.from_id,\n                                 new_title=action.title)\n            elif isinstance(action, types.MessageActionChatEditTitle):\n                return cls.Event(msg,\n                                 users=msg.from_id,\n                                 new_title=action.title)\n            elif isinstance(action, types.MessageActionChatEditPhoto):\n                return cls.Event(msg,\n                                 users=msg.from_id,\n                                 new_photo=action.photo)\n            elif isinstance(action, types.MessageActionChatDeletePhoto):\n                return cls.Event(msg,\n                                 users=msg.from_id,\n                                 new_photo=True)\n            elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to:\n                return cls.Event(msg,\n                                 pin_ids=[msg.reply_to_msg_id])\n            elif isinstance(action, types.MessageActionGameScore):\n                return cls.Event(msg,\n                                 new_score=action.score)\n\n        elif isinstance(update, types.UpdateChannelParticipant) \\\n                and bool(update.new_participant) != bool(update.prev_participant):\n            # If members are hidden, bots will receive this update instead,\n            # as there won't be a service message. Promotions and demotions\n            # seem to have both new and prev participant, which are ignored\n            # by this event.\n            return cls.Event(types.PeerChannel(update.channel_id),\n                             users=update.user_id,\n                             added_by=update.actor_id if update.new_participant else None,\n                             kicked_by=update.actor_id if update.prev_participant else None)\n\n    class Event(EventCommon):\n        \"\"\"\n        Represents the event of a new chat action.\n\n        Members:\n            action_message  (`MessageAction <https://tl.telethon.dev/types/message_action.html>`_):\n                The message invoked by this Chat Action.\n\n            new_pin (`bool`):\n                `True` if there is a new pin.\n\n            new_photo (`bool`):\n                `True` if there's a new chat photo (or it was removed).\n\n            photo (:tl:`Photo`, optional):\n                The new photo (or `None` if it was removed).\n\n            user_added (`bool`):\n                `True` if the user was added by some other.\n\n            user_joined (`bool`):\n                `True` if the user joined on their own.\n\n            user_left (`bool`):\n                `True` if the user left on their own.\n\n            user_kicked (`bool`):\n                `True` if the user was kicked by some other.\n\n            created (`bool`, optional):\n                `True` if this chat was just created.\n\n            new_title (`str`, optional):\n                The new title string for the chat, if applicable.\n\n            new_score (`str`, optional):\n                The new score string for the game, if applicable.\n\n            unpin (`bool`):\n                `True` if the existing pin gets unpinned.\n        \"\"\"\n\n        def __init__(self, where, new_photo=None,\n                     added_by=None, kicked_by=None, created=None,\n                     users=None, new_title=None, pin_ids=None, pin=None, new_score=None):\n            if isinstance(where, types.MessageService):\n                self.action_message = where\n                where = where.peer_id\n            else:\n                self.action_message = None\n\n            # TODO needs some testing (can there be more than one id, and do they follow pin order?)\n            #      same in get_pinned_message\n            super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None)\n\n            self.new_pin = pin_ids is not None\n            self._pin_ids = pin_ids\n            self._pinned_messages = None\n\n            self.new_photo = new_photo is not None\n            self.photo = \\\n                new_photo if isinstance(new_photo, types.Photo) else None\n\n            self._added_by = None\n            self._kicked_by = None\n            self.user_added = self.user_joined = self.user_left = \\\n                self.user_kicked = self.unpin = False\n\n            if added_by is True:\n                self.user_joined = True\n            elif added_by:\n                self.user_added = True\n                self._added_by = added_by\n\n            # If `from_id` was not present (it's `True`) or the affected\n            # user was \"kicked by itself\", then it left. Else it was kicked.\n            if kicked_by is True or (users is not None and kicked_by == users):\n                self.user_left = True\n            elif kicked_by:\n                self.user_kicked = True\n                self._kicked_by = kicked_by\n\n            self.created = bool(created)\n\n            if isinstance(users, list):\n                self._user_ids = [utils.get_peer_id(u) for u in users]\n            elif users:\n                self._user_ids = [utils.get_peer_id(users)]\n            else:\n                self._user_ids = []\n\n            self._users = None\n            self._input_users = None\n            self.new_title = new_title\n            self.new_score = new_score\n            self.unpin = not pin\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            if self.action_message:\n                self.action_message._finish_init(client, self._entities, None)\n\n        async def respond(self, *args, **kwargs):\n            \"\"\"\n            Responds to the chat action message (not as a reply). Shorthand for\n            `telethon.client.messages.MessageMethods.send_message` with\n            ``entity`` already set.\n            \"\"\"\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n        async def reply(self, *args, **kwargs):\n            \"\"\"\n            Replies to the chat action message (as a reply). Shorthand for\n            `telethon.client.messages.MessageMethods.send_message` with\n            both ``entity`` and ``reply_to`` already set.\n\n            Has the same effect as `respond` if there is no message.\n            \"\"\"\n            if not self.action_message:\n                return await self.respond(*args, **kwargs)\n\n            kwargs['reply_to'] = self.action_message.id\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n        async def delete(self, *args, **kwargs):\n            \"\"\"\n            Deletes the chat action message. You're responsible for checking\n            whether you have the permission to do so, or to except the error\n            otherwise. Shorthand for\n            `telethon.client.messages.MessageMethods.delete_messages` with\n            ``entity`` and ``message_ids`` already set.\n\n            Does nothing if no message action triggered this event.\n            \"\"\"\n            if not self.action_message:\n                return\n\n            return await self._client.delete_messages(\n                await self.get_input_chat(), [self.action_message],\n                *args, **kwargs\n            )\n\n        async def get_pinned_message(self):\n            \"\"\"\n            If ``new_pin`` is `True`, this returns the `Message\n            <telethon.tl.custom.message.Message>` object that was pinned.\n            \"\"\"\n            if self._pinned_messages is None:\n                await self.get_pinned_messages()\n\n            if self._pinned_messages:\n                return self._pinned_messages[0]\n\n        async def get_pinned_messages(self):\n            \"\"\"\n            If ``new_pin`` is `True`, this returns a `list` of `Message\n            <telethon.tl.custom.message.Message>` objects that were pinned.\n            \"\"\"\n            if not self._pin_ids:\n                return self._pin_ids  # either None or empty list\n\n            chat = await self.get_input_chat()\n            if chat:\n                self._pinned_messages = await self._client.get_messages(\n                    self._input_chat, ids=self._pin_ids)\n\n            return self._pinned_messages\n\n        @property\n        def added_by(self):\n            \"\"\"\n            The user who added ``users``, if applicable (`None` otherwise).\n            \"\"\"\n            if self._added_by and not isinstance(self._added_by, types.User):\n                aby = self._entities.get(utils.get_peer_id(self._added_by))\n                if aby:\n                    self._added_by = aby\n\n            return self._added_by\n\n        async def get_added_by(self):\n            \"\"\"\n            Returns `added_by` but will make an API call if necessary.\n            \"\"\"\n            if not self.added_by and self._added_by:\n                self._added_by = await self._client.get_entity(self._added_by)\n\n            return self._added_by\n\n        @property\n        def kicked_by(self):\n            \"\"\"\n            The user who kicked ``users``, if applicable (`None` otherwise).\n            \"\"\"\n            if self._kicked_by and not isinstance(self._kicked_by, types.User):\n                kby = self._entities.get(utils.get_peer_id(self._kicked_by))\n                if kby:\n                    self._kicked_by = kby\n\n            return self._kicked_by\n\n        async def get_kicked_by(self):\n            \"\"\"\n            Returns `kicked_by` but will make an API call if necessary.\n            \"\"\"\n            if not self.kicked_by and self._kicked_by:\n                self._kicked_by = await self._client.get_entity(self._kicked_by)\n\n            return self._kicked_by\n\n        @property\n        def user(self):\n            \"\"\"\n            The first user that takes part in this action. For example, who joined.\n\n            Might be `None` if the information can't be retrieved or\n            there is no user taking part.\n            \"\"\"\n            if self.users:\n                return self._users[0]\n\n        async def get_user(self):\n            \"\"\"\n            Returns `user` but will make an API call if necessary.\n            \"\"\"\n            if self.users or await self.get_users():\n                return self._users[0]\n\n        @property\n        def input_user(self):\n            \"\"\"\n            Input version of the ``self.user`` property.\n            \"\"\"\n            if self.input_users:\n                return self._input_users[0]\n\n        async def get_input_user(self):\n            \"\"\"\n            Returns `input_user` but will make an API call if necessary.\n            \"\"\"\n            if self.input_users or await self.get_input_users():\n                return self._input_users[0]\n\n        @property\n        def user_id(self):\n            \"\"\"\n            Returns the marked signed ID of the first user, if any.\n            \"\"\"\n            if self._user_ids:\n                return self._user_ids[0]\n\n        @property\n        def users(self):\n            \"\"\"\n            A list of users that take part in this action. For example, who joined.\n\n            Might be empty if the information can't be retrieved or there\n            are no users taking part.\n            \"\"\"\n            if not self._user_ids:\n                return []\n\n            if self._users is None:\n                self._users = [\n                    self._entities[user_id]\n                    for user_id in self._user_ids\n                    if user_id in self._entities\n                ]\n\n            return self._users\n\n        async def get_users(self):\n            \"\"\"\n            Returns `users` but will make an API call if necessary.\n            \"\"\"\n            if not self._user_ids:\n                return []\n\n            # Note: we access the property first so that it fills if needed\n            if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message:\n                await self.action_message._reload_message()\n                self._users = [\n                    u for u in self.action_message.action_entities\n                    if isinstance(u, (types.User, types.UserEmpty))]\n\n            return self._users\n\n        @property\n        def input_users(self):\n            \"\"\"\n            Input version of the ``self.users`` property.\n            \"\"\"\n            if self._input_users is None and self._user_ids:\n                self._input_users = []\n                for user_id in self._user_ids:\n                    # First try to get it from our entities\n                    try:\n                        self._input_users.append(utils.get_input_peer(self._entities[user_id]))\n                        continue\n                    except (KeyError, TypeError):\n                        pass\n\n                    # If missing, try from the entity cache\n                    try:\n                        self._input_users.append(self._client._mb_entity_cache.get(\n                            utils.resolve_id(user_id)[0])._as_input_peer())\n                        continue\n                    except AttributeError:\n                        pass\n\n            return self._input_users or []\n\n        async def get_input_users(self):\n            \"\"\"\n            Returns `input_users` but will make an API call if necessary.\n            \"\"\"\n            if not self._user_ids:\n                return []\n\n            # Note: we access the property first so that it fills if needed\n            if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message:\n                self._input_users = [\n                    utils.get_input_peer(u)\n                    for u in self.action_message.action_entities\n                    if isinstance(u, (types.User, types.UserEmpty))]\n\n            return self._input_users or []\n\n        @property\n        def user_ids(self):\n            \"\"\"\n            Returns the marked signed ID of the users, if any.\n            \"\"\"\n            if self._user_ids:\n                return self._user_ids[:]\n"
  },
  {
    "path": "telethon/events/common.py",
    "content": "import abc\nimport asyncio\nimport warnings\n\nfrom .. import utils\nfrom ..tl import TLObject, types\nfrom ..tl.custom.chatgetter import ChatGetter\n\n\nasync def _into_id_set(client, chats):\n    \"\"\"Helper util to turn the input chat or chats into a set of IDs.\"\"\"\n    if chats is None:\n        return None\n\n    if not utils.is_list_like(chats):\n        chats = (chats,)\n\n    result = set()\n    for chat in chats:\n        if isinstance(chat, int):\n            if chat < 0:\n                result.add(chat)  # Explicitly marked IDs are negative\n            else:\n                result.update({  # Support all valid types of peers\n                    utils.get_peer_id(types.PeerUser(chat)),\n                    utils.get_peer_id(types.PeerChat(chat)),\n                    utils.get_peer_id(types.PeerChannel(chat)),\n                })\n        elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687:\n            # 0x2d45687 == crc32(b'Peer')\n            result.add(utils.get_peer_id(chat))\n        else:\n            chat = await client.get_input_entity(chat)\n            if isinstance(chat, types.InputPeerSelf):\n                chat = await client.get_me(input_peer=True)\n            result.add(utils.get_peer_id(chat))\n\n    return result\n\n\nclass EventBuilder(abc.ABC):\n    \"\"\"\n    The common event builder, with builtin support to filter per chat.\n\n    Args:\n        chats (`entity`, optional):\n            May be one or more entities (username/peer/etc.), preferably IDs.\n            By default, only matching chats will be handled.\n\n        blacklist_chats (`bool`, optional):\n            Whether to treat the chats as a blacklist instead of\n            as a whitelist (default). This means that every chat\n            will be handled *except* those specified in ``chats``\n            which will be ignored if ``blacklist_chats=True``.\n\n        func (`callable`, optional):\n            A callable (async or not) function that should accept the event as input\n            parameter, and return a value indicating whether the event\n            should be dispatched or not (any truthy value will do, it\n            does not need to be a `bool`). It works like a custom filter:\n\n            .. code-block:: python\n\n                @client.on(events.NewMessage(func=lambda e: e.is_private))\n                async def handler(event):\n                    pass  # code here\n    \"\"\"\n    def __init__(self, chats=None, *, blacklist_chats=False, func=None):\n        self.chats = chats\n        self.blacklist_chats = bool(blacklist_chats)\n        self.resolved = False\n        self.func = func\n        self._resolve_lock = None\n\n    @classmethod\n    @abc.abstractmethod\n    def build(cls, update, others=None, self_id=None):\n        \"\"\"\n        Builds an event for the given update if possible, or returns None.\n\n        `others` are the rest of updates that came in the same container\n        as the current `update`.\n\n        `self_id` should be the current user's ID, since it is required\n        for some events which lack this information but still need it.\n        \"\"\"\n        # TODO So many parameters specific to only some update types seems dirty\n\n    async def resolve(self, client):\n        \"\"\"Helper method to allow event builders to be resolved before usage\"\"\"\n        if self.resolved:\n            return\n\n        if not self._resolve_lock:\n            self._resolve_lock = asyncio.Lock()\n\n        async with self._resolve_lock:\n            if not self.resolved:\n                await self._resolve(client)\n                self.resolved = True\n\n    async def _resolve(self, client):\n        self.chats = await _into_id_set(client, self.chats)\n\n    def filter(self, event):\n        \"\"\"\n        Returns a truthy value if the event passed the filter and should be\n        used, or falsy otherwise. The return value may need to be awaited.\n\n        The events must have been resolved before this can be called.\n        \"\"\"\n        if not self.resolved:\n            return\n\n        if self.chats is not None:\n            # Note: the `event.chat_id` property checks if it's `None` for us\n            inside = event.chat_id in self.chats\n            if inside == self.blacklist_chats:\n                # If this chat matches but it's a blacklist ignore.\n                # If it doesn't match but it's a whitelist ignore.\n                return\n\n        if not self.func:\n            return True\n\n        # Return the result of func directly as it may need to be awaited\n        return self.func(event)\n\n\nclass EventCommon(ChatGetter, abc.ABC):\n    \"\"\"\n    Intermediate class with common things to all events.\n\n    Remember that this class implements `ChatGetter\n    <telethon.tl.custom.chatgetter.ChatGetter>` which\n    means you have access to all chat properties and methods.\n\n    In addition, you can access the `original_update`\n    field which contains the original :tl:`Update`.\n    \"\"\"\n    _event_name = 'Event'\n\n    def __init__(self, chat_peer=None, msg_id=None, broadcast=None):\n        super().__init__(chat_peer, broadcast=broadcast)\n        self._entities = {}\n        self._client = None\n        self._message_id = msg_id\n        self.original_update = None\n\n    def _set_client(self, client):\n        \"\"\"\n        Setter so subclasses can act accordingly when the client is set.\n        \"\"\"\n        self._client = client\n        if self._chat_peer:\n            self._chat, self._input_chat = utils._get_entity_pair(\n                self.chat_id, self._entities, client._mb_entity_cache)\n        else:\n            self._chat = self._input_chat = None\n\n    @property\n    def client(self):\n        \"\"\"\n        The `telethon.TelegramClient` that created this event.\n        \"\"\"\n        return self._client\n\n    def __str__(self):\n        return TLObject.pretty_format(self.to_dict())\n\n    def stringify(self):\n        return TLObject.pretty_format(self.to_dict(), indent=0)\n\n    def to_dict(self):\n        d = {k: v for k, v in self.__dict__.items() if k[0] != '_'}\n        d['_'] = self._event_name\n        return d\n\n\ndef name_inner_event(cls):\n    \"\"\"Decorator to rename cls.Event 'Event' as 'cls.Event'\"\"\"\n    if hasattr(cls, 'Event'):\n        cls.Event._event_name = '{}.Event'.format(cls.__name__)\n    else:\n        warnings.warn('Class {} does not have a inner Event'.format(cls))\n    return cls\n"
  },
  {
    "path": "telethon/events/inlinequery.py",
    "content": "import inspect\nimport re\n\nimport asyncio\n\nfrom .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils, helpers\nfrom ..tl import types, functions, custom\nfrom ..tl.custom.sendergetter import SenderGetter\n\n\n@name_inner_event\nclass InlineQuery(EventBuilder):\n    \"\"\"\n    Occurs whenever you sign in as a bot and a user\n    sends an inline query such as ``@bot query``.\n\n    Args:\n        users (`entity`, optional):\n            May be one or more entities (username/peer/etc.), preferably IDs.\n            By default, only inline queries from these users will be handled.\n\n        blacklist_users (`bool`, optional):\n            Whether to treat the users as a blacklist instead of\n            as a whitelist (default). This means that every chat\n            will be handled *except* those specified in ``users``\n            which will be ignored if ``blacklist_users=True``.\n\n        pattern (`str`, `callable`, `Pattern`, optional):\n            If set, only queries matching this pattern will be handled.\n            You can specify a regex-like string which will be matched\n            against the message, a callable function that returns `True`\n            if a message is acceptable, or a compiled regex pattern.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.InlineQuery)\n            async def handler(event):\n                builder = event.builder\n\n                # Two options (convert user text to UPPERCASE or lowercase)\n                await event.answer([\n                    builder.article('UPPERCASE', text=event.text.upper()),\n                    builder.article('lowercase', text=event.text.lower()),\n                ])\n    \"\"\"\n    def __init__(\n            self, users=None, *, blacklist_users=False, func=None, pattern=None):\n        super().__init__(users, blacklist_chats=blacklist_users, func=func)\n\n        if isinstance(pattern, str):\n            self.pattern = re.compile(pattern).match\n        elif not pattern or callable(pattern):\n            self.pattern = pattern\n        elif hasattr(pattern, 'match') and callable(pattern.match):\n            self.pattern = pattern.match\n        else:\n            raise TypeError('Invalid pattern type given')\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, types.UpdateBotInlineQuery):\n            return cls.Event(update)\n\n    def filter(self, event):\n        if self.pattern:\n            match = self.pattern(event.text)\n            if not match:\n                return\n            event.pattern_match = match\n\n        return super().filter(event)\n\n    class Event(EventCommon, SenderGetter):\n        \"\"\"\n        Represents the event of a new callback query.\n\n        Members:\n            query (:tl:`UpdateBotInlineQuery`):\n                The original :tl:`UpdateBotInlineQuery`.\n\n                Make sure to access the `text` property of the query if\n                you want the text rather than the actual query object.\n\n            pattern_match (`obj`, optional):\n                The resulting object from calling the passed ``pattern``\n                function, which is ``re.compile(...).match`` by default.\n        \"\"\"\n        def __init__(self, query):\n            super().__init__(chat_peer=types.PeerUser(query.user_id))\n            SenderGetter.__init__(self, query.user_id)\n            self.query = query\n            self.pattern_match = None\n            self._answered = False\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            self._sender, self._input_sender = utils._get_entity_pair(\n                self.sender_id, self._entities, client._mb_entity_cache)\n\n        @property\n        def id(self):\n            \"\"\"\n            Returns the unique identifier for the query ID.\n            \"\"\"\n            return self.query.query_id\n\n        @property\n        def text(self):\n            \"\"\"\n            Returns the text the user used to make the inline query.\n            \"\"\"\n            return self.query.query\n\n        @property\n        def offset(self):\n            \"\"\"\n            The string the user's client used as an offset for the query.\n            This will either be empty or equal to offsets passed to `answer`.\n            \"\"\"\n            return self.query.offset\n\n        @property\n        def geo(self):\n            \"\"\"\n            If the user location is requested when using inline mode\n            and the user's device is able to send it, this will return\n            the :tl:`GeoPoint` with the position of the user.\n            \"\"\"\n            return self.query.geo\n\n        @property\n        def builder(self):\n            \"\"\"\n            Returns a new `InlineBuilder\n            <telethon.tl.custom.inlinebuilder.InlineBuilder>` instance.\n            \"\"\"\n            return custom.InlineBuilder(self._client)\n\n        async def answer(\n                self, results=None, cache_time=0, *,\n                gallery=False, next_offset=None, private=False,\n                switch_pm=None, switch_pm_param=''):\n            \"\"\"\n            Answers the inline query with the given results.\n\n            See the documentation for `builder` to know what kind of answers\n            can be given.\n\n            Args:\n                results (`list`, optional):\n                    A list of :tl:`InputBotInlineResult` to use.\n                    You should use `builder` to create these:\n\n                    .. code-block:: python\n\n                        builder = inline.builder\n                        r1 = builder.article('Be nice', text='Have a nice day')\n                        r2 = builder.article('Be bad', text=\"I don't like you\")\n                        await inline.answer([r1, r2])\n\n                    You can send up to 50 results as documented in\n                    https://core.telegram.org/bots/api#answerinlinequery.\n                    Sending more will raise ``ResultsTooMuchError``,\n                    and you should consider using `next_offset` to\n                    paginate them.\n\n                cache_time (`int`, optional):\n                    For how long this result should be cached on\n                    the user's client. Defaults to 0 for no cache.\n\n                gallery (`bool`, optional):\n                    Whether the results should show as a gallery (grid) or not.\n\n                next_offset (`str`, optional):\n                    The offset the client will send when the user scrolls the\n                    results and it repeats the request.\n\n                private (`bool`, optional):\n                    Whether the results should be cached by Telegram\n                    (not private) or by the user's client (private).\n\n                switch_pm (`str`, optional):\n                    If set, this text will be shown in the results\n                    to allow the user to switch to private messages.\n\n                switch_pm_param (`str`, optional):\n                    Optional parameter to start the bot with if\n                    `switch_pm` was used.\n\n            Example:\n\n                .. code-block:: python\n\n                    @bot.on(events.InlineQuery)\n                    async def handler(event):\n                        builder = event.builder\n\n                        rev_text = event.text[::-1]\n                        await event.answer([\n                            builder.article('Reverse text', text=rev_text),\n                            builder.photo('/path/to/photo.jpg')\n                        ])\n            \"\"\"\n            if self._answered:\n                return\n\n            if results:\n                futures = [self._as_future(x) for x in results]\n\n                await asyncio.wait(futures)\n\n                # All futures will be in the `done` *set* that `wait` returns.\n                #\n                # Precisely because it's a `set` and not a `list`, it\n                # will not preserve the order, but since all futures\n                # completed we can use our original, ordered `list`.\n                results = [x.result() for x in futures]\n            else:\n                results = []\n\n            if switch_pm:\n                switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param)\n\n            return await self._client(\n                functions.messages.SetInlineBotResultsRequest(\n                    query_id=self.query.query_id,\n                    results=results,\n                    cache_time=cache_time,\n                    gallery=gallery,\n                    next_offset=next_offset,\n                    private=private,\n                    switch_pm=switch_pm\n                )\n            )\n\n        @staticmethod\n        def _as_future(obj):\n            if inspect.isawaitable(obj):\n                return asyncio.ensure_future(obj)\n\n            f = helpers.get_running_loop().create_future()\n            f.set_result(obj)\n            return f\n"
  },
  {
    "path": "telethon/events/messagedeleted.py",
    "content": "from .common import EventBuilder, EventCommon, name_inner_event\nfrom ..tl import types\n\n\n@name_inner_event\nclass MessageDeleted(EventBuilder):\n    \"\"\"\n    Occurs whenever a message is deleted. Note that this event isn't 100%\n    reliable, since Telegram doesn't always notify the clients that a message\n    was deleted.\n\n    .. important::\n\n        Telegram **does not** send information about *where* a message\n        was deleted if it occurs in private conversations with other users\n        or in small group chats, because message IDs are *unique* and you\n        can identify the chat with the message ID alone if you saved it\n        previously.\n\n        Telethon **does not** save information of where messages occur,\n        so it cannot know in which chat a message was deleted (this will\n        only work in channels, where the channel ID *is* present).\n\n        This means that the ``chats=`` parameter will not work reliably,\n        unless you intend on working with channels and super-groups only.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.MessageDeleted)\n            async def handler(event):\n                # Log all deleted message IDs\n                for msg_id in event.deleted_ids:\n                    print('Message', msg_id, 'was deleted in', event.chat_id)\n    \"\"\"\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, types.UpdateDeleteMessages):\n            return cls.Event(\n                deleted_ids=update.messages,\n                peer=None\n            )\n        elif isinstance(update, types.UpdateDeleteChannelMessages):\n            return cls.Event(\n                deleted_ids=update.messages,\n                peer=types.PeerChannel(update.channel_id)\n            )\n\n    class Event(EventCommon):\n        def __init__(self, deleted_ids, peer):\n            super().__init__(\n                chat_peer=peer, msg_id=(deleted_ids or [0])[0]\n            )\n            self.deleted_id = None if not deleted_ids else deleted_ids[0]\n            self.deleted_ids = deleted_ids\n"
  },
  {
    "path": "telethon/events/messageedited.py",
    "content": "from .common import name_inner_event\nfrom .newmessage import NewMessage\nfrom ..tl import types\n\n\n@name_inner_event\nclass MessageEdited(NewMessage):\n    \"\"\"\n    Occurs whenever a message is edited. Just like `NewMessage\n    <telethon.events.newmessage.NewMessage>`, you should treat\n    this event as a `Message <telethon.tl.custom.message.Message>`.\n\n    .. warning::\n\n        On channels, `Message.out <telethon.tl.custom.message.Message>`\n        will be `True` if you sent the message originally, **not if\n        you edited it**! This can be dangerous if you run outgoing\n        commands on edits.\n\n        Some examples follow:\n\n        * You send a message \"A\", ``out is True``.\n        * You edit \"A\" to \"B\", ``out is True``.\n        * Someone else edits \"B\" to \"C\", ``out is True`` (**be careful!**).\n        * Someone sends \"X\", ``out is False``.\n        * Someone edits \"X\" to \"Y\", ``out is False``.\n        * You edit \"Y\" to \"Z\", ``out is False``.\n\n        Since there are useful cases where you need the right ``out``\n        value, the library cannot do anything automatically to help you.\n        Instead, consider using ``from_users='me'`` (it won't work in\n        broadcast channels at all since the sender is the channel and\n        not you).\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.MessageEdited)\n            async def handler(event):\n                # Log the date of new edits\n                print('Message', event.id, 'changed at', event.date)\n    \"\"\"\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, (types.UpdateEditMessage,\n                               types.UpdateEditChannelMessage)):\n            return cls.Event(update.message)\n\n    class Event(NewMessage.Event):\n        pass  # Required if we want a different name for it\n"
  },
  {
    "path": "telethon/events/messageread.py",
    "content": "from .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils\nfrom ..tl import types\n\n\n@name_inner_event\nclass MessageRead(EventBuilder):\n    \"\"\"\n    Occurs whenever one or more messages are read in a chat.\n\n    Args:\n        inbox (`bool`, optional):\n            If this argument is `True`, then when you read someone else's\n            messages the event will be fired. By default (`False`) only\n            when messages you sent are read by someone else will fire it.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.MessageRead)\n            async def handler(event):\n                # Log when someone reads your messages\n                print('Someone has read all your messages until', event.max_id)\n\n            @client.on(events.MessageRead(inbox=True))\n            async def handler(event):\n                # Log when you read message in a chat (from your \"inbox\")\n                print('You have read messages until', event.max_id)\n    \"\"\"\n    def __init__(\n            self, chats=None, *, blacklist_chats=False, func=None, inbox=False):\n        super().__init__(chats, blacklist_chats=blacklist_chats, func=func)\n        self.inbox = inbox\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, types.UpdateReadHistoryInbox):\n            return cls.Event(update.peer, update.max_id, False)\n        elif isinstance(update, types.UpdateReadHistoryOutbox):\n            return cls.Event(update.peer, update.max_id, True)\n        elif isinstance(update, types.UpdateReadChannelInbox):\n            return cls.Event(types.PeerChannel(update.channel_id),\n                             update.max_id, False)\n        elif isinstance(update, types.UpdateReadChannelOutbox):\n            return cls.Event(types.PeerChannel(update.channel_id),\n                             update.max_id, True)\n        elif isinstance(update, types.UpdateReadMessagesContents):\n            return cls.Event(message_ids=update.messages,\n                             contents=True)\n        elif isinstance(update, types.UpdateChannelReadMessagesContents):\n            return cls.Event(types.PeerChannel(update.channel_id),\n                             message_ids=update.messages,\n                             contents=True)\n\n    def filter(self, event):\n        if self.inbox == event.outbox:\n            return\n\n        return super().filter(event)\n\n    class Event(EventCommon):\n        \"\"\"\n        Represents the event of one or more messages being read.\n\n        Members:\n            max_id (`int`):\n                Up to which message ID has been read. Every message\n                with an ID equal or lower to it have been read.\n\n            outbox (`bool`):\n                `True` if someone else has read your messages.\n\n            contents (`bool`):\n                `True` if what was read were the contents of a message.\n                This will be the case when e.g. you play a voice note.\n                It may only be set on ``inbox`` events.\n        \"\"\"\n        def __init__(self, peer=None, max_id=None, out=False, contents=False,\n                     message_ids=None):\n            self.outbox = out\n            self.contents = contents\n            self._message_ids = message_ids or []\n            self._messages = None\n            self.max_id = max_id or max(message_ids or [], default=None)\n            super().__init__(peer, self.max_id)\n\n        @property\n        def inbox(self):\n            \"\"\"\n            `True` if you have read someone else's messages.\n            \"\"\"\n            return not self.outbox\n\n        @property\n        def message_ids(self):\n            \"\"\"\n            The IDs of the messages **which contents'** were read.\n\n            Use :meth:`is_read` if you need to check whether a message\n            was read instead checking if it's in here.\n            \"\"\"\n            return self._message_ids\n\n        async def get_messages(self):\n            \"\"\"\n            Returns the list of `Message <telethon.tl.custom.message.Message>`\n            **which contents'** were read.\n\n            Use :meth:`is_read` if you need to check whether a message\n            was read instead checking if it's in here.\n            \"\"\"\n            if self._messages is None:\n                chat = await self.get_input_chat()\n                if not chat:\n                    self._messages = []\n                else:\n                    self._messages = await self._client.get_messages(\n                        chat, ids=self._message_ids)\n\n            return self._messages\n\n        def is_read(self, message):\n            \"\"\"\n            Returns `True` if the given message (or its ID) has been read.\n\n            If a list-like argument is provided, this method will return a\n            list of booleans indicating which messages have been read.\n            \"\"\"\n            if utils.is_list_like(message):\n                return [(m if isinstance(m, int) else m.id) <= self.max_id\n                        for m in message]\n            else:\n                return (message if isinstance(message, int)\n                        else message.id) <= self.max_id\n\n        def __contains__(self, message):\n            \"\"\"`True` if the message(s) are read message.\"\"\"\n            if utils.is_list_like(message):\n                return all(self.is_read(message))\n            else:\n                return self.is_read(message)\n"
  },
  {
    "path": "telethon/events/newmessage.py",
    "content": "import re\n\nfrom .common import EventBuilder, EventCommon, name_inner_event, _into_id_set\nfrom .. import utils\nfrom ..tl import types\n\n\n@name_inner_event\nclass NewMessage(EventBuilder):\n    \"\"\"\n    Occurs whenever a new text message or a message with media arrives.\n\n    Args:\n        incoming (`bool`, optional):\n            If set to `True`, only **incoming** messages will be handled.\n            Mutually exclusive with ``outgoing`` (can only set one of either).\n\n        outgoing (`bool`, optional):\n            If set to `True`, only **outgoing** messages will be handled.\n            Mutually exclusive with ``incoming`` (can only set one of either).\n\n        from_users (`entity`, optional):\n            Unlike `chats`, this parameter filters the *senders* of the\n            message. That is, only messages *sent by these users* will be\n            handled. Use `chats` if you want private messages with this/these\n            users. `from_users` lets you filter by messages sent by *one or\n            more* users across the desired chats (doesn't need a list).\n\n        forwards (`bool`, optional):\n            Whether forwarded messages should be handled or not. By default,\n            both forwarded and normal messages are included. If it's `True`\n            *only* forwards will be handled. If it's `False` only messages\n            that are *not* forwards will be handled.\n\n        pattern (`str`, `callable`, `Pattern`, optional):\n            If set, only messages matching this pattern will be handled.\n            You can specify a regex-like string which will be matched\n            against the message, a callable function that returns `True`\n            if a message is acceptable, or a compiled regex pattern.\n\n    Example\n        .. code-block:: python\n\n            import asyncio\n            from telethon import events\n\n            @client.on(events.NewMessage(pattern='(?i)hello.+'))\n            async def handler(event):\n                # Respond whenever someone says \"Hello\" and something else\n                await event.reply('Hey!')\n\n            @client.on(events.NewMessage(outgoing=True, pattern='!ping'))\n            async def handler(event):\n                # Say \"!pong\" whenever you send \"!ping\", then delete both messages\n                m = await event.respond('!pong')\n                await asyncio.sleep(5)\n                await client.delete_messages(event.chat_id, [event.id, m.id])\n    \"\"\"\n    def __init__(self, chats=None, *, blacklist_chats=False, func=None,\n                 incoming=None, outgoing=None,\n                 from_users=None, forwards=None, pattern=None):\n        if incoming and outgoing:\n            incoming = outgoing = None  # Same as no filter\n        elif incoming is not None and outgoing is None:\n            outgoing = not incoming\n        elif outgoing is not None and incoming is None:\n            incoming = not outgoing\n        elif all(x is not None and not x for x in (incoming, outgoing)):\n            raise ValueError(\"Don't create an event handler if you \"\n                             \"don't want neither incoming nor outgoing!\")\n\n        super().__init__(chats, blacklist_chats=blacklist_chats, func=func)\n        self.incoming = incoming\n        self.outgoing = outgoing\n        self.from_users = from_users\n        self.forwards = forwards\n        if isinstance(pattern, str):\n            self.pattern = re.compile(pattern).match\n        elif not pattern or callable(pattern):\n            self.pattern = pattern\n        elif hasattr(pattern, 'match') and callable(pattern.match):\n            self.pattern = pattern.match\n        else:\n            raise TypeError('Invalid pattern type given')\n\n        # Should we short-circuit? E.g. perform no check at all\n        self._no_check = all(x is None for x in (\n            self.chats, self.incoming, self.outgoing, self.pattern,\n            self.from_users, self.forwards, self.from_users, self.func\n        ))\n\n    async def _resolve(self, client):\n        await super()._resolve(client)\n        self.from_users = await _into_id_set(client, self.from_users)\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update,\n                      (types.UpdateNewMessage, types.UpdateNewChannelMessage)):\n            if not isinstance(update.message, types.Message):\n                return  # We don't care about MessageService's here\n            event = cls.Event(update.message)\n        elif isinstance(update, types.UpdateShortMessage):\n            event = cls.Event(types.Message(\n                out=update.out,\n                mentioned=update.mentioned,\n                media_unread=update.media_unread,\n                silent=update.silent,\n                id=update.id,\n                peer_id=types.PeerUser(update.user_id),\n                from_id=types.PeerUser(self_id if update.out else update.user_id),\n                message=update.message,\n                date=update.date,\n                fwd_from=update.fwd_from,\n                via_bot_id=update.via_bot_id,\n                reply_to=update.reply_to,\n                entities=update.entities,\n                ttl_period=update.ttl_period\n            ))\n        elif isinstance(update, types.UpdateShortChatMessage):\n            event = cls.Event(types.Message(\n                out=update.out,\n                mentioned=update.mentioned,\n                media_unread=update.media_unread,\n                silent=update.silent,\n                id=update.id,\n                from_id=types.PeerUser(self_id if update.out else update.from_id),\n                peer_id=types.PeerChat(update.chat_id),\n                message=update.message,\n                date=update.date,\n                fwd_from=update.fwd_from,\n                via_bot_id=update.via_bot_id,\n                reply_to=update.reply_to,\n                entities=update.entities,\n                ttl_period=update.ttl_period\n            ))\n        else:\n            return\n\n        return event\n\n    def filter(self, event):\n        if self._no_check:\n            return event\n\n        if self.incoming and event.message.out:\n            return\n        if self.outgoing and not event.message.out:\n            return\n        if self.forwards is not None:\n            if bool(self.forwards) != bool(event.message.fwd_from):\n                return\n\n        if self.from_users is not None:\n            if event.message.sender_id not in self.from_users:\n                return\n\n        if self.pattern:\n            match = self.pattern(event.message.message or '')\n            if not match:\n                return\n            event.pattern_match = match\n\n        return super().filter(event)\n\n    class Event(EventCommon):\n        \"\"\"\n        Represents the event of a new message. This event can be treated\n        to all effects as a `Message <telethon.tl.custom.message.Message>`,\n        so please **refer to its documentation** to know what you can do\n        with this event.\n\n        Members:\n            message (`Message <telethon.tl.custom.message.Message>`):\n                This is the only difference with the received\n                `Message <telethon.tl.custom.message.Message>`, and will\n                return the `telethon.tl.custom.message.Message` itself,\n                not the text.\n\n                See `Message <telethon.tl.custom.message.Message>` for\n                the rest of available members and methods.\n\n            pattern_match (`obj`):\n                The resulting object from calling the passed ``pattern`` function.\n                Here's an example using a string (defaults to regex match):\n\n                >>> from telethon import TelegramClient, events\n                >>> client = TelegramClient(...)\n                >>>\n                >>> @client.on(events.NewMessage(pattern=r'hi (\\\\w+)!'))\n                ... async def handler(event):\n                ...     # In this case, the result is a ``Match`` object\n                ...     # since the `str` pattern was converted into\n                ...     # the ``re.compile(pattern).match`` function.\n                ...     print('Welcomed', event.pattern_match.group(1))\n                ...\n                >>>\n        \"\"\"\n        def __init__(self, message):\n            self.__dict__['_init'] = False\n            super().__init__(chat_peer=message.peer_id,\n                             msg_id=message.id, broadcast=bool(message.post))\n\n            self.pattern_match = None\n            self.message = message\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            m = self.message\n            m._finish_init(client, self._entities, None)\n            self.__dict__['_init'] = True  # No new attributes can be set\n\n        def __getattr__(self, item):\n            if item in self.__dict__:\n                return self.__dict__[item]\n            else:\n                return getattr(self.message, item)\n\n        def __setattr__(self, name, value):\n            if not self.__dict__['_init'] or name in self.__dict__:\n                self.__dict__[name] = value\n            else:\n                setattr(self.message, name, value)\n"
  },
  {
    "path": "telethon/events/raw.py",
    "content": "from .common import EventBuilder\nfrom .. import utils\n\n\nclass Raw(EventBuilder):\n    \"\"\"\n    Raw events are not actual events. Instead, they are the raw\n    :tl:`Update` object that Telegram sends. You normally shouldn't\n    need these.\n\n    Args:\n        types (`list` | `tuple` | `type`, optional):\n            The type or types that the :tl:`Update` instance must be.\n            Equivalent to ``if not isinstance(update, types): return``.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.Raw)\n            async def handler(update):\n                # Print all incoming updates\n                print(update.stringify())\n    \"\"\"\n    def __init__(self, types=None, *, func=None):\n        super().__init__(func=func)\n        if not types:\n            self.types = None\n        elif not utils.is_list_like(types):\n            if not isinstance(types, type):\n                raise TypeError('Invalid input type given: {}'.format(types))\n\n            self.types = types\n        else:\n            if not all(isinstance(x, type) for x in types):\n                raise TypeError('Invalid input types given: {}'.format(types))\n\n            self.types = tuple(types)\n\n    async def resolve(self, client):\n        self.resolved = True\n\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        return update\n\n    def filter(self, event):\n        if not self.types or isinstance(event, self.types):\n            if self.func:\n                # Return the result of func directly as it may need to be awaited\n                return self.func(event)\n            return event\n"
  },
  {
    "path": "telethon/events/userupdate.py",
    "content": "import datetime\nimport functools\n\nfrom .common import EventBuilder, EventCommon, name_inner_event\nfrom .. import utils\nfrom ..tl import types\nfrom ..tl.custom.sendergetter import SenderGetter\n\n\n# TODO Either the properties are poorly named or they should be\n#      different events, but that would be a breaking change.\n#\n# TODO There are more \"user updates\", but bundling them all up\n#      in a single place will make it annoying to use (since\n#      the user needs to check for the existence of `None`).\n#\n# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser\n\ndef _requires_action(function):\n    @functools.wraps(function)\n    def wrapped(self):\n        return None if self.action is None else function(self)\n\n    return wrapped\n\n\ndef _requires_status(function):\n    @functools.wraps(function)\n    def wrapped(self):\n        return None if self.status is None else function(self)\n\n    return wrapped\n\n\n@name_inner_event\nclass UserUpdate(EventBuilder):\n    \"\"\"\n    Occurs whenever a user goes online, starts typing, etc.\n\n    Example\n        .. code-block:: python\n\n            from telethon import events\n\n            @client.on(events.UserUpdate)\n            async def handler(event):\n                # If someone is uploading, say something\n                if event.uploading:\n                    await client.send_message(event.user_id, 'What are you sending?')\n    \"\"\"\n    @classmethod\n    def build(cls, update, others=None, self_id=None):\n        if isinstance(update, types.UpdateUserStatus):\n            return cls.Event(types.PeerUser(update.user_id),\n                             status=update.status)\n        elif isinstance(update, types.UpdateChannelUserTyping):\n            return cls.Event(update.from_id,\n                             chat_peer=types.PeerChannel(update.channel_id),\n                             typing=update.action)\n        elif isinstance(update, types.UpdateChatUserTyping):\n            return cls.Event(update.from_id,\n                             chat_peer=types.PeerChat(update.chat_id),\n                             typing=update.action)\n        elif isinstance(update, types.UpdateUserTyping):\n            return cls.Event(update.user_id,\n                             typing=update.action)\n\n    class Event(EventCommon, SenderGetter):\n        \"\"\"\n        Represents the event of a user update\n        such as gone online, started typing, etc.\n\n        Members:\n            status (:tl:`UserStatus`, optional):\n                The user status if the update is about going online or offline.\n\n                You should check this attribute first before checking any\n                of the seen within properties, since they will all be `None`\n                if the status is not set.\n\n            action (:tl:`SendMessageAction`, optional):\n                The \"typing\" action if any the user is performing if any.\n\n                You should check this attribute first before checking any\n                of the typing properties, since they will all be `None`\n                if the action is not set.\n        \"\"\"\n        def __init__(self, peer, *, status=None, chat_peer=None, typing=None):\n            super().__init__(chat_peer or peer)\n            SenderGetter.__init__(self, utils.get_peer_id(peer))\n\n            self.status = status\n            self.action = typing\n\n        def _set_client(self, client):\n            super()._set_client(client)\n            self._sender, self._input_sender = utils._get_entity_pair(\n                self.sender_id, self._entities, client._mb_entity_cache)\n\n        @property\n        def user(self):\n            \"\"\"Alias for `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`.\"\"\"\n            return self.sender\n\n        async def get_user(self):\n            \"\"\"Alias for `get_sender <telethon.tl.custom.sendergetter.SenderGetter.get_sender>`.\"\"\"\n            return await self.get_sender()\n\n        @property\n        def input_user(self):\n            \"\"\"Alias for `input_sender <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`.\"\"\"\n            return self.input_sender\n\n        async def get_input_user(self):\n            \"\"\"Alias for `get_input_sender <telethon.tl.custom.sendergetter.SenderGetter.get_input_sender>`.\"\"\"\n            return await self.get_input_sender()\n\n        @property\n        def user_id(self):\n            \"\"\"Alias for `sender_id <telethon.tl.custom.sendergetter.SenderGetter.sender_id>`.\"\"\"\n            return self.sender_id\n\n        @property\n        @_requires_action\n        def typing(self):\n            \"\"\"\n            `True` if the action is typing a message.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageTypingAction)\n\n        @property\n        @_requires_action\n        def uploading(self):\n            \"\"\"\n            `True` if the action is uploading something.\n            \"\"\"\n            return isinstance(self.action, (\n                types.SendMessageChooseContactAction,\n                types.SendMessageChooseStickerAction,\n                types.SendMessageUploadAudioAction,\n                types.SendMessageUploadDocumentAction,\n                types.SendMessageUploadPhotoAction,\n                types.SendMessageUploadRoundAction,\n                types.SendMessageUploadVideoAction\n            ))\n\n        @property\n        @_requires_action\n        def recording(self):\n            \"\"\"\n            `True` if the action is recording something.\n            \"\"\"\n            return isinstance(self.action, (\n                types.SendMessageRecordAudioAction,\n                types.SendMessageRecordRoundAction,\n                types.SendMessageRecordVideoAction\n            ))\n\n        @property\n        @_requires_action\n        def playing(self):\n            \"\"\"\n            `True` if the action is playing a game.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageGamePlayAction)\n\n        @property\n        @_requires_action\n        def cancel(self):\n            \"\"\"\n            `True` if the action was cancelling other actions.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageCancelAction)\n\n        @property\n        @_requires_action\n        def geo(self):\n            \"\"\"\n            `True` if what's being uploaded is a geo.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageGeoLocationAction)\n\n        @property\n        @_requires_action\n        def audio(self):\n            \"\"\"\n            `True` if what's being recorded/uploaded is an audio.\n            \"\"\"\n            return isinstance(self.action, (\n                types.SendMessageRecordAudioAction,\n                types.SendMessageUploadAudioAction\n            ))\n\n        @property\n        @_requires_action\n        def round(self):\n            \"\"\"\n            `True` if what's being recorded/uploaded is a round video.\n            \"\"\"\n            return isinstance(self.action, (\n                types.SendMessageRecordRoundAction,\n                types.SendMessageUploadRoundAction\n            ))\n\n        @property\n        @_requires_action\n        def video(self):\n            \"\"\"\n            `True` if what's being recorded/uploaded is an video.\n            \"\"\"\n            return isinstance(self.action, (\n                types.SendMessageRecordVideoAction,\n                types.SendMessageUploadVideoAction\n            ))\n\n        @property\n        @_requires_action\n        def contact(self):\n            \"\"\"\n            `True` if what's being uploaded (selected) is a contact.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageChooseContactAction)\n\n        @property\n        @_requires_action\n        def document(self):\n            \"\"\"\n            `True` if what's being uploaded is document.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageUploadDocumentAction)\n\n        @property\n        @_requires_action\n        def sticker(self):\n            \"\"\"\n            `True` if what's being uploaded is a sticker.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageChooseStickerAction)\n\n        @property\n        @_requires_action\n        def photo(self):\n            \"\"\"\n            `True` if what's being uploaded is a photo.\n            \"\"\"\n            return isinstance(self.action, types.SendMessageUploadPhotoAction)\n\n        @property\n        @_requires_status\n        def last_seen(self):\n            \"\"\"\n            Exact `datetime.datetime` when the user was last seen if known.\n            \"\"\"\n            if isinstance(self.status, types.UserStatusOffline):\n                return self.status.was_online\n\n        @property\n        @_requires_status\n        def until(self):\n            \"\"\"\n            The `datetime.datetime` until when the user should appear online.\n            \"\"\"\n            if isinstance(self.status, types.UserStatusOnline):\n                return self.status.expires\n\n        def _last_seen_delta(self):\n            if isinstance(self.status, types.UserStatusOffline):\n                return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online\n            elif isinstance(self.status, types.UserStatusOnline):\n                return datetime.timedelta(days=0)\n            elif isinstance(self.status, types.UserStatusRecently):\n                return datetime.timedelta(days=1)\n            elif isinstance(self.status, types.UserStatusLastWeek):\n                return datetime.timedelta(days=7)\n            elif isinstance(self.status, types.UserStatusLastMonth):\n                return datetime.timedelta(days=30)\n            else:\n                return datetime.timedelta(days=365)\n\n        @property\n        @_requires_status\n        def online(self):\n            \"\"\"\n            `True` if the user is currently online,\n            \"\"\"\n            return self._last_seen_delta() <= datetime.timedelta(days=0)\n\n        @property\n        @_requires_status\n        def recently(self):\n            \"\"\"\n            `True` if the user was seen within a day.\n            \"\"\"\n            return self._last_seen_delta() <= datetime.timedelta(days=1)\n\n        @property\n        @_requires_status\n        def within_weeks(self):\n            \"\"\"\n            `True` if the user was seen within 7 days.\n            \"\"\"\n            return self._last_seen_delta() <= datetime.timedelta(days=7)\n\n        @property\n        @_requires_status\n        def within_months(self):\n            \"\"\"\n            `True` if the user was seen within 30 days.\n            \"\"\"\n            return self._last_seen_delta() <= datetime.timedelta(days=30)\n"
  },
  {
    "path": "telethon/extensions/__init__.py",
    "content": "\"\"\"\nSeveral extensions Python is missing, such as a proper class to handle a TCP\ncommunication with support for cancelling the operation, and a utility class\nto read arbitrary binary data in a more comfortable way, with int/strings/etc.\n\"\"\"\nfrom .binaryreader import BinaryReader\n"
  },
  {
    "path": "telethon/extensions/binaryreader.py",
    "content": "\"\"\"\nThis module contains the BinaryReader utility class.\n\"\"\"\nimport struct\nimport time\nfrom datetime import datetime, timedelta, timezone\n\nfrom ..errors import TypeNotFoundError\nfrom ..tl.alltlobjects import tlobjects\nfrom ..tl.core import core_objects\n\n_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6])\n_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc)\n\n\nclass BinaryReader:\n    \"\"\"\n    Small utility class to read binary data.\n    \"\"\"\n\n    def __init__(self, data):\n        self.stream = data or b''\n        self.position = 0\n        self._last = None  # Should come in handy to spot -404 errors\n\n    # region Reading\n\n    # \"All numbers are written as little endian.\"\n    # https://core.telegram.org/mtproto\n    def read_byte(self):\n        \"\"\"Reads a single byte value.\"\"\"\n        value, = struct.unpack_from(\"<B\", self.stream, self.position)\n        self.position += 1\n        return value\n\n    def read_int(self, signed=True):\n        \"\"\"Reads an integer (4 bytes) value.\"\"\"\n        fmt = '<i' if signed else '<I'\n        value, = struct.unpack_from(fmt, self.stream, self.position)\n        self.position += 4\n        return value\n\n    def read_long(self, signed=True):\n        \"\"\"Reads a long integer (8 bytes) value.\"\"\"\n        fmt = '<q' if signed else '<Q'\n        value, = struct.unpack_from(fmt, self.stream, self.position)\n        self.position += 8\n        return value\n\n    def read_float(self):\n        \"\"\"Reads a real floating point (4 bytes) value.\"\"\"\n        value, = struct.unpack_from(\"<f\", self.stream, self.position)\n        self.position += 4\n        return value\n\n    def read_double(self):\n        \"\"\"Reads a real floating point (8 bytes) value.\"\"\"\n        value, = struct.unpack_from(\"<d\", self.stream, self.position)\n        self.position += 8\n        return value\n\n    def read_large_int(self, bits, signed=True):\n        \"\"\"Reads a n-bits long integer value.\"\"\"\n        return int.from_bytes(\n            self.read(bits // 8), byteorder='little', signed=signed)\n\n    def read(self, length=-1):\n        \"\"\"Read the given amount of bytes, or -1 to read all remaining.\"\"\"\n        if length >= 0:\n            result = self.stream[self.position:self.position + length]\n            self.position += length\n        else:\n            result = self.stream[self.position:]\n            self.position += len(result)\n        if (length >= 0) and (len(result) != length):\n            raise BufferError(\n                'No more data left to read (need {}, got {}: {}); last read {}'\n                .format(length, len(result), repr(result), repr(self._last))\n            )\n\n        self._last = result\n        return result\n\n    def get_bytes(self):\n        \"\"\"Gets the byte array representing the current buffer as a whole.\"\"\"\n        return self.stream\n\n    # endregion\n\n    # region Telegram custom reading\n\n    def tgread_bytes(self):\n        \"\"\"\n        Reads a Telegram-encoded byte array, without the need of\n        specifying its length.\n        \"\"\"\n        first_byte = self.read_byte()\n        if first_byte == 254:\n            length = self.read_byte() | (self.read_byte() << 8) | (\n                self.read_byte() << 16)\n            padding = length % 4\n        else:\n            length = first_byte\n            padding = (length + 1) % 4\n\n        data = self.read(length)\n        if padding > 0:\n            padding = 4 - padding\n            self.read(padding)\n\n        return data\n\n    def tgread_string(self):\n        \"\"\"Reads a Telegram-encoded string.\"\"\"\n        return str(self.tgread_bytes(), encoding='utf-8', errors='replace')\n\n    def tgread_bool(self):\n        \"\"\"Reads a Telegram boolean value.\"\"\"\n        value = self.read_int(signed=False)\n        if value == 0x997275b5:  # boolTrue\n            return True\n        elif value == 0xbc799737:  # boolFalse\n            return False\n        else:\n            raise RuntimeError('Invalid boolean code {}'.format(hex(value)))\n\n    def tgread_date(self):\n        \"\"\"Reads and converts Unix time (used by Telegram)\n           into a Python datetime object.\n        \"\"\"\n        value = self.read_int()\n        return _EPOCH + timedelta(seconds=value)\n\n    def tgread_object(self):\n        \"\"\"Reads a Telegram object.\"\"\"\n        constructor_id = self.read_int(signed=False)\n        clazz = tlobjects.get(constructor_id, None)\n        if clazz is None:\n            # The class was None, but there's still a\n            # chance of it being a manually parsed value like bool!\n            value = constructor_id\n            if value == 0x997275b5:  # boolTrue\n                return True\n            elif value == 0xbc799737:  # boolFalse\n                return False\n            elif value == 0x1cb5c415:  # Vector\n                return [self.tgread_object() for _ in range(self.read_int())]\n\n            clazz = core_objects.get(constructor_id, None)\n            if clazz is None:\n                # If there was still no luck, give up\n                self.seek(-4)  # Go back\n                pos = self.tell_position()\n                error = TypeNotFoundError(constructor_id, self.read())\n                self.set_position(pos)\n                raise error\n\n        return clazz.from_reader(self)\n\n    def tgread_vector(self):\n        \"\"\"Reads a vector (a list) of Telegram objects.\"\"\"\n        if 0x1cb5c415 != self.read_int(signed=False):\n            raise RuntimeError('Invalid constructor code, vector was expected')\n\n        count = self.read_int()\n        return [self.tgread_object() for _ in range(count)]\n\n    # endregion\n\n    def close(self):\n        \"\"\"Closes the reader, freeing the BytesIO stream.\"\"\"\n        self.stream = b''\n\n    # region Position related\n\n    def tell_position(self):\n        \"\"\"Tells the current position on the stream.\"\"\"\n        return self.position\n\n    def set_position(self, position):\n        \"\"\"Sets the current position on the stream.\"\"\"\n        self.position = position\n\n    def seek(self, offset):\n        \"\"\"\n        Seeks the stream position given an offset from the current position.\n        The offset may be negative.\n        \"\"\"\n        self.position += offset\n\n    # endregion\n\n    # region with block\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.close()\n\n    # endregion\n"
  },
  {
    "path": "telethon/extensions/html.py",
    "content": "\"\"\"\nSimple HTML -> Telegram entity parser.\n\"\"\"\nfrom collections import deque\nfrom html import escape\nfrom html.parser import HTMLParser\nfrom typing import Iterable, Tuple, List\n\nfrom ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text\nfrom ..tl import TLObject\nfrom ..tl.types import (\n    MessageEntityBold, MessageEntityItalic, MessageEntityCode,\n    MessageEntityPre, MessageEntityEmail, MessageEntityUrl,\n    MessageEntityTextUrl, MessageEntityMentionName,\n    MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote,\n    MessageEntityCustomEmoji, TypeMessageEntity\n)\n\n\nclass HTMLToTelegramParser(HTMLParser):\n    def __init__(self):\n        super().__init__()\n        self.text = ''\n        self.entities = []\n        self._building_entities = {}\n        self._open_tags = deque()\n        self._open_tags_meta = deque()\n\n    def handle_starttag(self, tag, attrs):\n        self._open_tags.appendleft(tag)\n        self._open_tags_meta.appendleft(None)\n\n        attrs = dict(attrs)\n        EntityType = None\n        args = {}\n        if tag == 'strong' or tag == 'b':\n            EntityType = MessageEntityBold\n        elif tag == 'em' or tag == 'i':\n            EntityType = MessageEntityItalic\n        elif tag == 'u':\n            EntityType = MessageEntityUnderline\n        elif tag == 'del' or tag == 's':\n            EntityType = MessageEntityStrike\n        elif tag == 'blockquote':\n            EntityType = MessageEntityBlockquote\n        elif tag == 'code':\n            try:\n                # If we're in the middle of a <pre> tag, this <code> tag is\n                # probably intended for syntax highlighting.\n                #\n                # Syntax highlighting is set with\n                #     <code class='language-...'>codeblock</code>\n                # inside <pre> tags\n                pre = self._building_entities['pre']\n                try:\n                    pre.language = attrs['class'][len('language-'):]\n                except KeyError:\n                    pass\n            except KeyError:\n                EntityType = MessageEntityCode\n        elif tag == 'pre':\n            EntityType = MessageEntityPre\n            args['language'] = ''\n        elif tag == 'a':\n            try:\n                url = attrs['href']\n            except KeyError:\n                return\n            if url.startswith('mailto:'):\n                url = url[len('mailto:'):]\n                EntityType = MessageEntityEmail\n            else:\n                if self.get_starttag_text() == url:\n                    EntityType = MessageEntityUrl\n                else:\n                    EntityType = MessageEntityTextUrl\n                    args['url'] = del_surrogate(url)\n                    url = None\n            self._open_tags_meta.popleft()\n            self._open_tags_meta.appendleft(url)\n        elif tag == 'tg-emoji':\n            try:\n                emoji_id = int(attrs['emoji-id'])\n            except (KeyError, ValueError):\n                return\n\n            EntityType = MessageEntityCustomEmoji\n            args['document_id'] = emoji_id\n            \n        if EntityType and tag not in self._building_entities:\n            self._building_entities[tag] = EntityType(\n                offset=len(self.text),\n                # The length will be determined when closing the tag.\n                length=0,\n                **args)\n\n    def handle_data(self, text):\n        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''\n        if previous_tag == 'a':\n            url = self._open_tags_meta[0]\n            if url:\n                text = url\n\n        for tag, entity in self._building_entities.items():\n            entity.length += len(text)\n\n        self.text += text\n\n    def handle_endtag(self, tag):\n        try:\n            self._open_tags.popleft()\n            self._open_tags_meta.popleft()\n        except IndexError:\n            pass\n        entity = self._building_entities.pop(tag, None)\n        if entity:\n            self.entities.append(entity)\n\n\ndef parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:\n    \"\"\"\n    Parses the given HTML message and returns its stripped representation\n    plus a list of the MessageEntity's that were found.\n\n    :param html: the message with HTML to be parsed.\n    :return: a tuple consisting of (clean message, [message entities]).\n    \"\"\"\n    if not html:\n        return html, []\n\n    parser = HTMLToTelegramParser()\n    parser.feed(add_surrogate(html))\n    text = strip_text(parser.text, parser.entities)\n    parser.entities.reverse()\n    parser.entities.sort(key=lambda entity: entity.offset)\n    return del_surrogate(text), parser.entities\n\n\nENTITY_TO_FORMATTER = {\n    MessageEntityBold: ('<strong>', '</strong>'),\n    MessageEntityItalic: ('<em>', '</em>'),\n    MessageEntityCode: ('<code>', '</code>'),\n    MessageEntityUnderline: ('<u>', '</u>'),\n    MessageEntityStrike: ('<del>', '</del>'),\n    MessageEntityBlockquote: ('<blockquote>', '</blockquote>'),\n    MessageEntityPre: lambda e, _: (\n        \"<pre>\\n\"\n        \"    <code class='language-{}'>\\n\"\n        \"        \".format(e.language), \"{}\\n\"\n        \"    </code>\\n\"\n        \"</pre>\"\n    ),\n    MessageEntityEmail: lambda _, t: ('<a href=\"mailto:{}\">'.format(t), '</a>'),\n    MessageEntityUrl: lambda _, t: ('<a href=\"{}\">'.format(t), '</a>'),\n    MessageEntityTextUrl: lambda e, _: ('<a href=\"{}\">'.format(escape(e.url)), '</a>'),\n    MessageEntityMentionName: lambda e, _: ('<a href=\"tg://user?id={}\">'.format(e.user_id), '</a>'),\n    MessageEntityCustomEmoji: lambda e, _: ('<tg-emoji emoji-id=\"{}\">'.format(e.document_id), '</tg-emoji>'),\n}\n\n\ndef unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str:\n    \"\"\"\n    Performs the reverse operation to .parse(), effectively returning HTML\n    given a normal text and its MessageEntity's.\n\n    :param text: the text to be reconverted into HTML.\n    :param entities: the MessageEntity's applied to the text.\n    :return: a HTML representation of the combination of both inputs.\n    \"\"\"\n    if not text:\n        return text\n    elif not entities:\n        return escape(text)\n\n    if isinstance(entities, TLObject):\n        entities = (entities,)\n\n    text = add_surrogate(text)\n    insert_at = []\n    for i, entity in enumerate(entities):\n        s = entity.offset\n        e = entity.offset + entity.length\n        delimiter = ENTITY_TO_FORMATTER.get(type(entity), None)\n        if delimiter:\n            if callable(delimiter):\n                delimiter = delimiter(entity, text[s:e])\n            insert_at.append((s, i, delimiter[0]))\n            insert_at.append((e, -i, delimiter[1]))\n\n    insert_at.sort(key=lambda t: (t[0], t[1]))\n    next_escape_bound = len(text)\n    while insert_at:\n        # Same logic as markdown.py\n        at, _, what = insert_at.pop()\n        while within_surrogate(text, at):\n            at += 1\n\n        text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:]\n        next_escape_bound = at\n\n    text = escape(text[:next_escape_bound]) + text[next_escape_bound:]\n\n    return del_surrogate(text)\n"
  },
  {
    "path": "telethon/extensions/markdown.py",
    "content": "\"\"\"\nSimple markdown parser which does not support nesting. Intended primarily\nfor use within the library, which attempts to handle emojies correctly,\nsince they seem to count as two characters and it's a bit strange.\n\"\"\"\nimport re\nimport warnings\n\nfrom ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text\nfrom ..tl import TLObject\nfrom ..tl.types import (\n    MessageEntityBold, MessageEntityItalic, MessageEntityCode,\n    MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName,\n    MessageEntityStrike\n)\n\nDEFAULT_DELIMITERS = {\n    '**': MessageEntityBold,\n    '__': MessageEntityItalic,\n    '~~': MessageEntityStrike,\n    '`': MessageEntityCode,\n    '```': MessageEntityPre\n}\n\nDEFAULT_URL_RE = re.compile(r'\\[([^]]*?)\\]\\(([\\s\\S]*?)\\)')\nDEFAULT_URL_FORMAT = '[{0}]({1})'\n\n\ndef parse(message, delimiters=None, url_re=None):\n    \"\"\"\n    Parses the given markdown message and returns its stripped representation\n    plus a list of the MessageEntity's that were found.\n\n    :param message: the message with markdown-like syntax to be parsed.\n    :param delimiters: the delimiters to be used, {delimiter: type}.\n    :param url_re: the URL bytes regex to be used. Must have two groups.\n    :return: a tuple consisting of (clean message, [message entities]).\n    \"\"\"\n    if not message:\n        return message, []\n\n    if url_re is None:\n        url_re = DEFAULT_URL_RE\n    elif isinstance(url_re, str):\n        url_re = re.compile(url_re)\n\n    if not delimiters:\n        if delimiters is not None:\n            return message, []\n        delimiters = DEFAULT_DELIMITERS\n\n    # Build a regex to efficiently test all delimiters at once.\n    # Note that the largest delimiter should go first, we don't\n    # want ``` to be interpreted as a single back-tick in a code block.\n    delim_re = re.compile('|'.join('({})'.format(re.escape(k))\n                                   for k in sorted(delimiters, key=len, reverse=True)))\n\n    # Cannot use a for loop because we need to skip some indices\n    i = 0\n    result = []\n\n    # Work on byte level with the utf-16le encoding to get the offsets right.\n    # The offset will just be half the index we're at.\n    message = add_surrogate(message)\n    while i < len(message):\n        m = delim_re.match(message, pos=i)\n\n        # Did we find some delimiter here at `i`?\n        if m:\n            delim = next(filter(None, m.groups()))\n\n            # +1 to avoid matching right after (e.g. \"****\")\n            end = message.find(delim, i + len(delim) + 1)\n\n            # Did we find the earliest closing tag?\n            if end != -1:\n\n                # Remove the delimiter from the string\n                message = ''.join((\n                        message[:i],\n                        message[i + len(delim):end],\n                        message[end + len(delim):]\n                ))\n\n                # Check other affected entities\n                for ent in result:\n                    # If the end is after our start, it is affected\n                    if ent.offset + ent.length > i:\n                        # If the old start is before ours and the old end is after ours, we are fully enclosed\n                        if ent.offset <= i and ent.offset + ent.length >= end + len(delim):\n                            ent.length -= len(delim) * 2\n                        else:\n                            ent.length -= len(delim)\n\n                # Append the found entity\n                ent = delimiters[delim]\n                if ent == MessageEntityPre:\n                    result.append(ent(i, end - i - len(delim), ''))  # has 'lang'\n                else:\n                    result.append(ent(i, end - i - len(delim)))\n\n                # No nested entities inside code blocks\n                if ent in (MessageEntityCode, MessageEntityPre):\n                    i = end - len(delim)\n\n                continue\n\n        elif url_re:\n            m = url_re.match(message, pos=i)\n            if m:\n                # Replace the whole match with only the inline URL text.\n                message = ''.join((\n                    message[:m.start()],\n                    m.group(1),\n                    message[m.end():]\n                ))\n\n                delim_size = m.end() - m.start() - len(m.group(1))\n                for ent in result:\n                    # If the end is after our start, it is affected\n                    if ent.offset + ent.length > m.start():\n                        ent.length -= delim_size\n\n                result.append(MessageEntityTextUrl(\n                    offset=m.start(), length=len(m.group(1)),\n                    url=del_surrogate(m.group(2))\n                ))\n                i += len(m.group(1))\n                continue\n\n        i += 1\n\n    message = strip_text(message, result)\n    return del_surrogate(message), result\n\n\ndef unparse(text, entities, delimiters=None, url_fmt=None):\n    \"\"\"\n    Performs the reverse operation to .parse(), effectively returning\n    markdown-like syntax given a normal text and its MessageEntity's.\n\n    :param text: the text to be reconverted into markdown.\n    :param entities: the MessageEntity's applied to the text.\n    :return: a markdown-like text representing the combination of both inputs.\n    \"\"\"\n    if not text or not entities:\n        return text\n\n    if not delimiters:\n        if delimiters is not None:\n            return text\n        delimiters = DEFAULT_DELIMITERS\n\n    if url_fmt is not None:\n        warnings.warn('url_fmt is deprecated')  # since it complicates everything *a lot*\n\n    if isinstance(entities, TLObject):\n        entities = (entities,)\n\n    text = add_surrogate(text)\n    delimiters = {v: k for k, v in delimiters.items()}\n    insert_at = []\n    for i, entity in enumerate(entities):\n        s = entity.offset\n        e = entity.offset + entity.length\n        delimiter = delimiters.get(type(entity), None)\n        if delimiter:\n            insert_at.append((s, i, delimiter))\n            insert_at.append((e, -i, delimiter))\n        else:\n            url = None\n            if isinstance(entity, MessageEntityTextUrl):\n                url = entity.url\n            elif isinstance(entity, MessageEntityMentionName):\n                url = 'tg://user?id={}'.format(entity.user_id)\n            if url:\n                insert_at.append((s, i, '['))\n                insert_at.append((e, -i, ']({})'.format(url)))\n\n    insert_at.sort(key=lambda t: (t[0], t[1]))\n    while insert_at:\n        at, _, what = insert_at.pop()\n\n        # If we are in the middle of a surrogate nudge the position by -1.\n        # Otherwise we would end up with malformed text and fail to encode.\n        # For example of bad input: \"Hi \\ud83d\\ude1c\"\n        # https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF\n        while within_surrogate(text, at):\n            at += 1\n\n        text = text[:at] + what + text[at:]\n\n    return del_surrogate(text)\n"
  },
  {
    "path": "telethon/extensions/messagepacker.py",
    "content": "import asyncio\nimport collections\nimport io\nimport struct\n\nfrom ..tl import TLRequest\nfrom ..tl.core.messagecontainer import MessageContainer\nfrom ..tl.core.tlmessage import TLMessage\n\n\nclass MessagePacker:\n    \"\"\"\n    This class packs `RequestState` as outgoing `TLMessages`.\n\n    The purpose of this class is to support putting N `RequestState` into a\n    queue, and then awaiting for \"packed\" `TLMessage` in the other end. The\n    simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but\n    for efficiency purposes it's ``States -> Container`` (N-to-1).\n\n    This addresses several needs: outgoing messages will be smaller, so the\n    encryption and network overhead also is smaller. It's also a central\n    point where outgoing requests are put, and where ready-messages are get.\n    \"\"\"\n\n    def __init__(self, state, loggers):\n        self._state = state\n        self._deque = collections.deque()\n        self._ready = asyncio.Event()\n        self._log = loggers[__name__]\n\n    def append(self, state):\n        self._deque.append(state)\n        self._ready.set()\n\n    def extend(self, states):\n        self._deque.extend(states)\n        self._ready.set()\n\n    async def get(self):\n        \"\"\"\n        Returns (batch, data) if one or more items could be retrieved.\n\n        If the cancellation occurs or only invalid items were in the\n        queue, (None, None) will be returned instead.\n        \"\"\"\n        if not self._deque:\n            self._ready.clear()\n            await self._ready.wait()\n\n        buffer = io.BytesIO()\n        batch = []\n        size = 0\n\n        # Fill a new batch to return while the size is small enough,\n        # as long as we don't exceed the maximum length of messages.\n        while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH:\n            state = self._deque.popleft()\n            size += len(state.data) + TLMessage.SIZE_OVERHEAD\n\n            if size <= MessageContainer.MAXIMUM_SIZE:\n                state.msg_id = self._state.write_data_as_message(\n                    buffer, state.data, isinstance(state.request, TLRequest),\n                    after_id=state.after.msg_id if state.after else None\n                )\n                batch.append(state)\n                self._log.debug('Assigned msg_id = %d to %s (%x)',\n                                state.msg_id, state.request.__class__.__name__,\n                                id(state.request))\n                continue\n\n            if batch:\n                # Put the item back since it can't be sent in this batch\n                self._deque.appendleft(state)\n                break\n\n            # If a single message exceeds the maximum size, then the\n            # message payload cannot be sent. Telegram would forcibly\n            # close the connection; message would never be confirmed.\n            #\n            # We don't put the item back because it can never be sent.\n            # If we did, we would loop again and reach this same path.\n            # Setting the exception twice results in `InvalidStateError`\n            # and this method should never return with error, which we\n            # really want to avoid.\n            self._log.warning(\n                'Message payload for %s is too long (%d) and cannot be sent',\n                state.request.__class__.__name__, len(state.data)\n            )\n            state.future.set_exception(\n                ValueError('Request payload is too big'))\n\n            size = 0\n            continue\n\n        if not batch:\n            return None, None\n\n        if len(batch) > 1:\n            # Inlined code to pack several messages into a container\n            data = struct.pack(\n                '<Ii', MessageContainer.CONSTRUCTOR_ID, len(batch)\n            ) + buffer.getvalue()\n            buffer = io.BytesIO()\n            container_id = self._state.write_data_as_message(\n                buffer, data, content_related=False\n            )\n            for s in batch:\n                s.container_id = container_id\n\n        data = buffer.getvalue()\n        return batch, data\n"
  },
  {
    "path": "telethon/functions.py",
    "content": "from .tl.functions import *\n"
  },
  {
    "path": "telethon/helpers.py",
    "content": "\"\"\"Various helpers not related to the Telegram API itself\"\"\"\nimport asyncio\nimport io\nimport enum\nimport os\nimport struct\nimport inspect\nimport logging\nimport functools\nimport sys\nfrom pathlib import Path\nfrom hashlib import sha1\n\n\nclass _EntityType(enum.Enum):\n    USER = 0\n    CHAT = 1\n    CHANNEL = 2\n\n\n_log = logging.getLogger(__name__)\n\n\n# region Multiple utilities\n\n\ndef generate_random_long(signed=True):\n    \"\"\"Generates a random long integer (8 bytes), which is optionally signed\"\"\"\n    return int.from_bytes(os.urandom(8), signed=signed, byteorder='little')\n\n\ndef ensure_parent_dir_exists(file_path):\n    \"\"\"Ensures that the parent directory exists\"\"\"\n    parent = os.path.dirname(file_path)\n    if parent:\n        os.makedirs(parent, exist_ok=True)\n\n\ndef add_surrogate(text):\n    return ''.join(\n        # SMP -> Surrogate Pairs (Telegram offsets are calculated with these).\n        # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more.\n        ''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))\n        if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text\n    )\n\n\ndef del_surrogate(text):\n    return text.encode('utf-16', 'surrogatepass').decode('utf-16')\n\n\ndef within_surrogate(text, index, *, length=None):\n    \"\"\"\n    `True` if ``index`` is within a surrogate (before and after it, not at!).\n    \"\"\"\n    if length is None:\n        length = len(text)\n\n    return (\n            1 < index < len(text) and  # in bounds\n            '\\ud800' <= text[index - 1] <= '\\udbff' and  # previous is\n            '\\ud800' <= text[index] <= '\\udfff'  # current is\n    )\n\n\ndef strip_text(text, entities):\n    \"\"\"\n    Strips whitespace from the given surrogated text modifying the provided\n    entities, also removing any empty (0-length) entities.\n\n    This assumes that the length of entities is greater or equal to 0, and\n    that no entity is out of bounds.\n    \"\"\"\n    if not entities:\n        return text.strip()\n\n    len_ori = len(text)\n    text = text.lstrip()\n    left_offset = len_ori - len(text)\n    text = text.rstrip()\n    len_final = len(text)\n\n    for i in reversed(range(len(entities))):\n        e = entities[i]\n        if e.length == 0:\n            del entities[i]\n            continue\n\n        if e.offset + e.length > left_offset:\n            if e.offset >= left_offset:\n                #  0 1|2 3 4 5       |       0 1|2 3 4 5\n                #     ^     ^        |          ^\n                #   lo(2)  o(5)      |      o(2)/lo(2)\n                e.offset -= left_offset\n                #     |0 1 2 3       |          |0 1 2 3\n                #           ^        |          ^\n                #     o=o-lo(3=5-2)  |    o=o-lo(0=2-2)\n            else:\n                # e.offset < left_offset and e.offset + e.length > left_offset\n                #  0 1 2 3|4 5 6 7 8 9 10\n                #   ^     ^           ^\n                #  o(1) lo(4)      o+l(1+9)\n                e.length = e.offset + e.length - left_offset\n                e.offset = 0\n                #         |0 1 2 3 4 5 6\n                #         ^           ^\n                #        o(0)  o+l=0+o+l-lo(6=0+6=0+1+9-4)\n        else:\n            # e.offset + e.length <= left_offset\n            #   0 1 2 3|4 5\n            #  ^       ^\n            # o(0)   o+l(4)\n            #        lo(4)\n            del entities[i]\n            continue\n\n        if e.offset + e.length <= len_final:\n            # |0 1 2 3 4 5 6 7 8 9\n            #   ^                 ^\n            #  o(1)       o+l(1+9)/lf(10)\n            continue\n        if e.offset >= len_final:\n            # |0 1 2 3 4\n            #           ^\n            #       o(5)/lf(5)\n            del entities[i]\n        else:\n            # e.offset < len_final and e.offset + e.length > len_final\n            # |0 1 2 3 4 5 (6) (7) (8) (9)\n            #   ^         ^           ^\n            #  o(1)     lf(6)      o+l(1+8)\n            e.length = len_final - e.offset\n            # |0 1 2 3 4 5\n            #   ^         ^\n            #  o(1) o+l=o+lf-o=lf(6=1+5=1+6-1)\n\n    return text\n\n\ndef retry_range(retries, force_retry=True):\n    \"\"\"\n    Generates an integer sequence starting from 1. If `retries` is\n    not a zero or a positive integer value, the sequence will be\n    infinite, otherwise it will end at `retries + 1`.\n    \"\"\"\n\n    # We need at least one iteration even if the retries are 0\n    # when force_retry is True.\n    if force_retry and not (retries is None or retries < 0):\n        retries += 1\n\n    attempt = 0\n    while attempt != retries:\n        attempt += 1\n        yield attempt\n\n\n\nasync def _maybe_await(value):\n    if inspect.isawaitable(value):\n        return await value\n    else:\n        return value\n\n\nasync def _cancel(log, **tasks):\n    \"\"\"\n    Helper to cancel one or more tasks gracefully, logging exceptions.\n    \"\"\"\n    for name, task in tasks.items():\n        if not task:\n            continue\n\n        task.cancel()\n        try:\n            await task\n        except asyncio.CancelledError:\n            pass\n        except RuntimeError:\n            # Probably: RuntimeError: await wasn't used with future\n            #\n            # See: https://github.com/python/cpython/blob/12d3061c7819a73d891dcce44327410eaf0e1bc2/Lib/asyncio/futures.py#L265\n            #\n            # Happens with _asyncio.Task instances (in \"Task cancelling\" state)\n            # trying to SIGINT the program right during initial connection, on\n            # _recv_loop coroutine (but we're creating its task explicitly with\n            # a loop, so how can it bug out like this?).\n            #\n            # Since we're aware of this error there's no point in logging it.\n            # *May* be https://bugs.python.org/issue37172\n            pass\n        except AssertionError as e:\n            # In Python 3.6, the above RuntimeError is an AssertionError\n            # See https://github.com/python/cpython/blob/7df32f844efed33ca781a016017eab7050263b90/Lib/asyncio/futures.py#L328\n            if e.args != (\"yield from wasn't used with future\",):\n                log.exception('Unhandled exception from %s after cancelling '\n                              '%s (%s)', name, type(task), task)\n        except Exception:\n            log.exception('Unhandled exception from %s after cancelling '\n                          '%s (%s)', name, type(task), task)\n\n\ndef _sync_enter(self):\n    \"\"\"\n    Helps to cut boilerplate on async context\n    managers that offer synchronous variants.\n    \"\"\"\n    if hasattr(self, 'loop'):\n        loop = self.loop\n    else:\n        loop = self._client.loop\n\n    if loop.is_running():\n        raise RuntimeError(\n            'You must use \"async with\" if the event loop '\n            'is running (i.e. you are inside an \"async def\")'\n        )\n\n    return loop.run_until_complete(self.__aenter__())\n\n\ndef _sync_exit(self, *args):\n    if hasattr(self, 'loop'):\n        loop = self.loop\n    else:\n        loop = self._client.loop\n\n    return loop.run_until_complete(self.__aexit__(*args))\n\n\ndef _entity_type(entity):\n    # This could be a `utils` method that just ran a few `isinstance` on\n    # `utils.get_peer(...)`'s result. However, there are *a lot* of auto\n    # casts going on, plenty of calls and temporary short-lived objects.\n    #\n    # So we just check if a string is in the class name.\n    # Still, assert that it's the right type to not return false results.\n    try:\n        if entity.SUBCLASS_OF_ID not in (\n                0x2d45687,  # crc32(b'Peer')\n                0xc91c90b6,  # crc32(b'InputPeer')\n                0xe669bf46,  # crc32(b'InputUser')\n                0x40f202fd,  # crc32(b'InputChannel')\n                0x2da17977,  # crc32(b'User')\n                0xc5af5d94,  # crc32(b'Chat')\n                0x1f4661b9,  # crc32(b'UserFull')\n                0xd49a2697,  # crc32(b'ChatFull')\n        ):\n            raise TypeError('{} does not have any entity type'.format(entity))\n    except AttributeError:\n        raise TypeError('{} is not a TLObject, cannot determine entity type'.format(entity))\n\n    name = entity.__class__.__name__\n    if 'User' in name:\n        return _EntityType.USER\n    elif 'Chat' in name:\n        return _EntityType.CHAT\n    elif 'Channel' in name:\n        return _EntityType.CHANNEL\n    elif 'Self' in name:\n        return _EntityType.USER\n\n    # 'Empty' in name or not found, we don't care, not a valid entity.\n    raise TypeError('{} does not have any entity type'.format(entity))\n\n# endregion\n\n# region Cryptographic related utils\n\n\ndef generate_key_data_from_nonce(server_nonce, new_nonce):\n    \"\"\"Generates the key data corresponding to the given nonce\"\"\"\n    server_nonce = server_nonce.to_bytes(16, 'little', signed=True)\n    new_nonce = new_nonce.to_bytes(32, 'little', signed=True)\n    hash1 = sha1(new_nonce + server_nonce).digest()\n    hash2 = sha1(server_nonce + new_nonce).digest()\n    hash3 = sha1(new_nonce + new_nonce).digest()\n\n    key = hash1 + hash2[:12]\n    iv = hash2[12:20] + hash3 + new_nonce[:4]\n    return key, iv\n\n\n# endregion\n\n# region Custom Classes\n\n\nclass TotalList(list):\n    \"\"\"\n    A list with an extra `total` property, which may not match its `len`\n    since the total represents the total amount of items *available*\n    somewhere else, not the items *in this list*.\n\n    Examples:\n\n        .. code-block:: python\n\n            # Telethon returns these lists in some cases (for example,\n            # only when a chunk is returned, but the \"total\" count\n            # is available).\n            result = await client.get_messages(chat, limit=10)\n\n            print(result.total)  # large number\n            print(len(result))  # 10\n            print(result[0])  # latest message\n\n            for x in result:  # show the 10 messages\n                print(x.text)\n\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.total = 0\n\n    def __str__(self):\n        return '[{}, total={}]'.format(\n            ', '.join(str(x) for x in self), self.total)\n\n    def __repr__(self):\n        return '[{}, total={}]'.format(\n            ', '.join(repr(x) for x in self), self.total)\n\n\nclass _FileStream(io.IOBase):\n    \"\"\"\n    Proxy around things that represent a file and need to be used as streams\n    which may or not need to be closed.\n\n    This will handle `pathlib.Path`, `str` paths, in-memory `bytes`, and\n    anything IO-like (including `aiofiles`).\n\n    It also provides access to the name and file size (also necessary).\n    \"\"\"\n    def __init__(self, file, *, file_size=None):\n        if isinstance(file, Path):\n            file = str(file.absolute())\n\n        self._file = file\n        self._name = None\n        self._size = file_size\n        self._stream = None\n        self._close_stream = None\n\n    async def __aenter__(self):\n        if isinstance(self._file, str):\n            self._name = os.path.basename(self._file)\n            self._size = os.path.getsize(self._file)\n            self._stream = open(self._file, 'rb')\n            self._close_stream = True\n            return self\n\n        if isinstance(self._file, bytes):\n            self._size = len(self._file)\n            self._stream = io.BytesIO(self._file)\n            self._close_stream = True\n            return self\n\n        if not callable(getattr(self._file, 'read', None)):\n            raise TypeError('file description should have a `read` method')\n\n        self._name = getattr(self._file, 'name', None)\n        self._stream = self._file\n        self._close_stream = False\n\n        if self._size is None:\n            if callable(getattr(self._file, 'seekable', None)):\n                seekable = await _maybe_await(self._file.seekable())\n            else:\n                seekable = False\n\n            if seekable:\n                pos = await _maybe_await(self._file.tell())\n                await _maybe_await(self._file.seek(0, os.SEEK_END))\n                self._size = await _maybe_await(self._file.tell())\n                await _maybe_await(self._file.seek(pos, os.SEEK_SET))\n            else:\n                _log.warning(\n                    'Could not determine file size beforehand so the entire '\n                    'file will be read in-memory')\n\n                data = await _maybe_await(self._file.read())\n                self._size = len(data)\n                self._stream = io.BytesIO(data)\n                self._close_stream = True\n\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        if self._close_stream and self._stream:\n            await _maybe_await(self._stream.close())\n\n    @property\n    def file_size(self):\n        return self._size\n\n    @property\n    def name(self):\n        return self._name\n\n    # Proxy all the methods. Doesn't need to be readable (makes multiline edits easier)\n    def read(self, *args, **kwargs): return self._stream.read(*args, **kwargs)\n    def readinto(self, *args, **kwargs): return self._stream.readinto(*args, **kwargs)\n    def write(self, *args, **kwargs): return self._stream.write(*args, **kwargs)\n    def fileno(self, *args, **kwargs): return self._stream.fileno(*args, **kwargs)\n    def flush(self, *args, **kwargs): return self._stream.flush(*args, **kwargs)\n    def isatty(self, *args, **kwargs): return self._stream.isatty(*args, **kwargs)\n    def readable(self, *args, **kwargs): return self._stream.readable(*args, **kwargs)\n    def readline(self, *args, **kwargs): return self._stream.readline(*args, **kwargs)\n    def readlines(self, *args, **kwargs): return self._stream.readlines(*args, **kwargs)\n    def seek(self, *args, **kwargs): return self._stream.seek(*args, **kwargs)\n    def seekable(self, *args, **kwargs): return self._stream.seekable(*args, **kwargs)\n    def tell(self, *args, **kwargs): return self._stream.tell(*args, **kwargs)\n    def truncate(self, *args, **kwargs): return self._stream.truncate(*args, **kwargs)\n    def writable(self, *args, **kwargs): return self._stream.writable(*args, **kwargs)\n    def writelines(self, *args, **kwargs): return self._stream.writelines(*args, **kwargs)\n\n    # close is special because it will be called by __del__ but we do NOT\n    # want to close the file unless we have to (we're just a wrapper).\n    # Instead, we do nothing (we should be used through the decorator which\n    # has its own mechanism to close the file correctly).\n    def close(self, *args, **kwargs):\n        pass\n\n# endregion\n\ndef get_running_loop():\n    if sys.version_info >= (3, 7):\n        try:\n            return asyncio.get_running_loop()\n        except RuntimeError:\n            return asyncio.get_event_loop_policy().get_event_loop()\n    else:\n        return asyncio.get_event_loop()\n"
  },
  {
    "path": "telethon/hints.py",
    "content": "import datetime\nimport typing\n\nfrom . import helpers\nfrom .tl import types, custom\n\nPhone = str\nUsername = str\nPeerID = int\nEntity = typing.Union[types.User, types.Chat, types.Channel]\nFullEntity = typing.Union[types.UserFull, types.messages.ChatFull, types.ChatFull, types.ChannelFull]\n\nEntityLike = typing.Union[\n    Phone,\n    Username,\n    PeerID,\n    types.TypePeer,\n    types.TypeInputPeer,\n    Entity,\n    FullEntity\n]\nEntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]]\n\nButtonLike = typing.Union[types.TypeKeyboardButton, custom.Button]\nMarkupLike = typing.Union[\n    types.TypeReplyMarkup,\n    ButtonLike,\n    typing.Sequence[ButtonLike],\n    typing.Sequence[typing.Sequence[ButtonLike]]\n]\n\nTotalList = helpers.TotalList\n\nDateLike = typing.Optional[typing.Union[float, datetime.datetime, datetime.date, datetime.timedelta]]\n\nLocalPath = str\nExternalUrl = str\nBotFileID = str\nFileLike = typing.Union[\n    LocalPath,\n    ExternalUrl,\n    BotFileID,\n    bytes,\n    typing.BinaryIO,\n    types.TypeMessageMedia,\n    types.TypeInputFile,\n    types.TypeInputFileLocation,\n    types.TypeInputMedia,\n    types.TypePhoto,\n    types.TypeInputPhoto,\n    types.TypeDocument,\n    types.TypeInputDocument\n]\n\n# Can't use `typing.Type` in Python 3.5.2\n# See https://github.com/python/typing/issues/266\ntry:\n    OutFileLike = typing.Union[\n        str,\n        typing.Type[bytes],\n        typing.BinaryIO\n    ]\nexcept TypeError:\n    OutFileLike = typing.Union[\n        str,\n        typing.BinaryIO\n    ]\n\nMessageLike = typing.Union[str, types.Message]\nMessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage]\n\nProgressCallback = typing.Callable[[int, int], None]\n"
  },
  {
    "path": "telethon/network/__init__.py",
    "content": "\"\"\"\nThis module contains several classes regarding network, low level connection\nwith Telegram's servers and the protocol used (TCP full, abridged, etc.).\n\"\"\"\nfrom .mtprotoplainsender import MTProtoPlainSender\nfrom .authenticator import do_authentication\nfrom .mtprotosender import MTProtoSender\nfrom .connection import (\n    Connection,\n    ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged,\n    ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged,\n    ConnectionTcpMTProxyIntermediate,\n    ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy\n)\n"
  },
  {
    "path": "telethon/network/authenticator.py",
    "content": "\"\"\"\nThis module contains several functions that authenticate the client machine\nwith Telegram's servers, effectively creating an authorization key.\n\"\"\"\nimport os\nimport time\nfrom hashlib import sha1\n\nfrom ..tl.types import (\n    ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk,\n    ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail\n)\nfrom .. import helpers\nfrom ..crypto import AES, AuthKey, Factorization, rsa\nfrom ..errors import SecurityError\nfrom ..extensions import BinaryReader\nfrom ..tl.functions import (\n    ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest\n)\n\n\nasync def do_authentication(sender):\n    \"\"\"\n    Executes the authentication process with the Telegram servers.\n\n    :param sender: a connected `MTProtoPlainSender`.\n    :return: returns a (authorization key, time offset) tuple.\n    \"\"\"\n    # Step 1 sending: PQ Request, endianness doesn't matter since it's random\n    nonce = int.from_bytes(os.urandom(16), 'big', signed=True)\n    res_pq = await sender.send(ReqPqMultiRequest(nonce))\n    assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq\n\n    if res_pq.nonce != nonce:\n        raise SecurityError('Step 1 invalid nonce from server')\n\n    pq = get_int(res_pq.pq)\n\n    # Step 2 sending: DH Exchange\n    p, q = Factorization.factorize(pq)\n    p, q = rsa.get_byte_array(p), rsa.get_byte_array(q)\n    new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True)\n\n    pq_inner_data = bytes(PQInnerData(\n        pq=rsa.get_byte_array(pq), p=p, q=q,\n        nonce=res_pq.nonce,\n        server_nonce=res_pq.server_nonce,\n        new_nonce=new_nonce\n    ))\n\n    # sha_digest + data + random_bytes\n    cipher_text, target_fingerprint = None, None\n    for fingerprint in res_pq.server_public_key_fingerprints:\n        cipher_text = rsa.encrypt(fingerprint, pq_inner_data)\n        if cipher_text is not None:\n            target_fingerprint = fingerprint\n            break\n\n    if cipher_text is None:\n        # Second attempt, but now we're allowed to use old keys\n        for fingerprint in res_pq.server_public_key_fingerprints:\n            cipher_text = rsa.encrypt(fingerprint, pq_inner_data, use_old=True)\n            if cipher_text is not None:\n                target_fingerprint = fingerprint\n                break\n\n    if cipher_text is None:\n        raise SecurityError(\n            'Step 2 could not find a valid key for fingerprints: {}'\n            .format(', '.join(\n                [str(f) for f in res_pq.server_public_key_fingerprints])\n            )\n        )\n\n    server_dh_params = await sender.send(ReqDHParamsRequest(\n        nonce=res_pq.nonce,\n        server_nonce=res_pq.server_nonce,\n        p=p, q=q,\n        public_key_fingerprint=target_fingerprint,\n        encrypted_data=cipher_text\n    ))\n\n    assert isinstance(\n        server_dh_params, (ServerDHParamsOk, ServerDHParamsFail)),\\\n        'Step 2.1 answer was %s' % server_dh_params\n\n    if server_dh_params.nonce != res_pq.nonce:\n        raise SecurityError('Step 2 invalid nonce from server')\n\n    if server_dh_params.server_nonce != res_pq.server_nonce:\n        raise SecurityError('Step 2 invalid server nonce from server')\n\n    if isinstance(server_dh_params, ServerDHParamsFail):\n        nnh = int.from_bytes(\n            sha1(new_nonce.to_bytes(32, 'little', signed=True)).digest()[4:20],\n            'little', signed=True\n        )\n        if server_dh_params.new_nonce_hash != nnh:\n            raise SecurityError('Step 2 invalid DH fail nonce from server')\n\n    assert isinstance(server_dh_params, ServerDHParamsOk),\\\n        'Step 2.2 answer was %s' % server_dh_params\n\n    # Step 3 sending: Complete DH Exchange\n    key, iv = helpers.generate_key_data_from_nonce(\n        res_pq.server_nonce, new_nonce\n    )\n    if len(server_dh_params.encrypted_answer) % 16 != 0:\n        # See PR#453\n        raise SecurityError('Step 3 AES block size mismatch')\n\n    plain_text_answer = AES.decrypt_ige(\n        server_dh_params.encrypted_answer, key, iv\n    )\n\n    with BinaryReader(plain_text_answer) as reader:\n        reader.read(20)  # hash sum\n        server_dh_inner = reader.tgread_object()\n        assert isinstance(server_dh_inner, ServerDHInnerData),\\\n            'Step 3 answer was %s' % server_dh_inner\n\n    if server_dh_inner.nonce != res_pq.nonce:\n        raise SecurityError('Step 3 Invalid nonce in encrypted answer')\n\n    if server_dh_inner.server_nonce != res_pq.server_nonce:\n        raise SecurityError('Step 3 Invalid server nonce in encrypted answer')\n\n    dh_prime = get_int(server_dh_inner.dh_prime, signed=False)\n    g = server_dh_inner.g\n    g_a = get_int(server_dh_inner.g_a, signed=False)\n    time_offset = server_dh_inner.server_time - int(time.time())\n\n    b = get_int(os.urandom(256), signed=False)\n    g_b = pow(g, b, dh_prime)\n    gab = pow(g_a, b, dh_prime)\n\n    # IMPORTANT: Apart from the conditions on the Diffie-Hellman prime\n    # dh_prime and generator g, both sides are to check that g, g_a and\n    # g_b are greater than 1 and less than dh_prime - 1. We recommend\n    # checking that g_a and g_b are between 2^{2048-64} and\n    # dh_prime - 2^{2048-64} as well.\n    # (https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete)\n    if not (1 < g < (dh_prime - 1)):\n        raise SecurityError('g_a is not within (1, dh_prime - 1)')\n\n    if not (1 < g_a < (dh_prime - 1)):\n        raise SecurityError('g_a is not within (1, dh_prime - 1)')\n\n    if not (1 < g_b < (dh_prime - 1)):\n        raise SecurityError('g_b is not within (1, dh_prime - 1)')\n\n    safety_range = 2 ** (2048 - 64)\n    if not (safety_range <= g_a <= (dh_prime - safety_range)):\n        raise SecurityError('g_a is not within (2^{2048-64}, dh_prime - 2^{2048-64})')\n\n    if not (safety_range <= g_b <= (dh_prime - safety_range)):\n        raise SecurityError('g_b is not within (2^{2048-64}, dh_prime - 2^{2048-64})')\n\n    # Prepare client DH Inner Data\n    client_dh_inner = bytes(ClientDHInnerData(\n        nonce=res_pq.nonce,\n        server_nonce=res_pq.server_nonce,\n        retry_id=0,  # TODO Actual retry ID\n        g_b=rsa.get_byte_array(g_b)\n    ))\n\n    client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner\n\n    # Encryption\n    client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv)\n\n    # Prepare Set client DH params\n    dh_gen = await sender.send(SetClientDHParamsRequest(\n        nonce=res_pq.nonce,\n        server_nonce=res_pq.server_nonce,\n        encrypted_data=client_dh_encrypted,\n    ))\n\n    nonce_types = (DhGenOk, DhGenRetry, DhGenFail)\n    assert isinstance(dh_gen, nonce_types), 'Step 3.1 answer was %s' % dh_gen\n    name = dh_gen.__class__.__name__\n    if dh_gen.nonce != res_pq.nonce:\n        raise SecurityError('Step 3 invalid {} nonce from server'.format(name))\n\n    if dh_gen.server_nonce != res_pq.server_nonce:\n        raise SecurityError(\n            'Step 3 invalid {} server nonce from server'.format(name))\n\n    auth_key = AuthKey(rsa.get_byte_array(gab))\n    nonce_number = 1 + nonce_types.index(type(dh_gen))\n    new_nonce_hash = auth_key.calc_new_nonce_hash(new_nonce, nonce_number)\n\n    dh_hash = getattr(dh_gen, 'new_nonce_hash{}'.format(nonce_number))\n    if dh_hash != new_nonce_hash:\n        raise SecurityError('Step 3 invalid new nonce hash')\n\n    if not isinstance(dh_gen, DhGenOk):\n        raise AssertionError('Step 3.2 answer was %s' % dh_gen)\n\n    return auth_key, time_offset\n\n\ndef get_int(byte_array, signed=True):\n    \"\"\"\n    Gets the specified integer from its byte array.\n    This should be used by this module alone, as it works with big endian.\n\n    :param byte_array: the byte array representing th integer.\n    :param signed: whether the number is signed or not.\n    :return: the integer representing the given byte array.\n    \"\"\"\n    return int.from_bytes(byte_array, byteorder='big', signed=signed)\n"
  },
  {
    "path": "telethon/network/connection/__init__.py",
    "content": "from .connection import Connection\nfrom .tcpfull import ConnectionTcpFull\nfrom .tcpintermediate import ConnectionTcpIntermediate\nfrom .tcpabridged import ConnectionTcpAbridged\nfrom .tcpobfuscated import ConnectionTcpObfuscated\nfrom .tcpmtproxy import (\n    TcpMTProxy,\n    ConnectionTcpMTProxyAbridged,\n    ConnectionTcpMTProxyIntermediate,\n    ConnectionTcpMTProxyRandomizedIntermediate\n)\nfrom .http import ConnectionHttp\n"
  },
  {
    "path": "telethon/network/connection/connection.py",
    "content": "import abc\nimport asyncio\nimport socket\nimport sys\n\ntry:\n    import ssl as ssl_mod\nexcept ImportError:\n    ssl_mod = None\n\ntry:\n    import python_socks\nexcept ImportError:\n    python_socks = None\n\nfrom ...errors import InvalidChecksumError, InvalidBufferError\nfrom ... import helpers\n\n\nclass Connection(abc.ABC):\n    \"\"\"\n    The `Connection` class is a wrapper around ``asyncio.open_connection``.\n\n    Subclasses will implement different transport modes as atomic operations,\n    which this class eases doing since the exposed interface simply puts and\n    gets complete data payloads to and from queues.\n\n    The only error that will raise from send and receive methods is\n    ``ConnectionError``, which will raise when attempting to send if\n    the client is disconnected (includes remote disconnections).\n    \"\"\"\n    # this static attribute should be redefined by `Connection` subclasses and\n    # should be one of `PacketCodec` implementations\n    packet_codec = None\n\n    def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None):\n        self._ip = ip\n        self._port = port\n        self._dc_id = dc_id  # only for MTProxy, it's an abstraction leak\n        self._log = loggers[__name__]\n        self._proxy = proxy\n        self._local_addr = local_addr\n        self._reader = None\n        self._writer = None\n        self._connected = False\n        self._send_task = None\n        self._recv_task = None\n        self._codec = None\n        self._obfuscation = None  # TcpObfuscated and MTProxy\n        self._send_queue = asyncio.Queue(1)\n        self._recv_queue = asyncio.Queue(1)\n\n    @staticmethod\n    def _wrap_socket_ssl(sock):\n        if ssl_mod is None:\n            raise RuntimeError(\n                'Cannot use proxy that requires SSL '\n                'without the SSL module being available'\n            )\n\n        return ssl_mod.wrap_socket(\n            sock,\n            do_handshake_on_connect=True,\n            ssl_version=ssl_mod.PROTOCOL_SSLv23,\n            ciphers='ADH-AES256-SHA')\n\n    @staticmethod\n    def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None):\n        if isinstance(proxy_type, str):\n            proxy_type = proxy_type.lower()\n\n        # Always prefer `python_socks` when available\n        if python_socks:\n            from python_socks import ProxyType\n\n            # We do the check for numerical values here\n            # to be backwards compatible with PySocks proxy format,\n            # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3)\n            if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == \"socks5\":\n                protocol = ProxyType.SOCKS5\n            elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == \"socks4\":\n                protocol = ProxyType.SOCKS4\n            elif proxy_type == ProxyType.HTTP or proxy_type == 3 or proxy_type == \"http\":\n                protocol = ProxyType.HTTP\n            else:\n                raise ValueError(\"Unknown proxy protocol type: {}\".format(proxy_type))\n\n            # This tuple must be compatible with `python_socks`' `Proxy.create()` signature\n            return protocol, addr, port, username, password, rdns\n\n        else:\n            from socks import SOCKS5, SOCKS4, HTTP\n\n            if proxy_type == 2 or proxy_type == \"socks5\":\n                protocol = SOCKS5\n            elif proxy_type == 1 or proxy_type == \"socks4\":\n                protocol = SOCKS4\n            elif proxy_type == 3 or proxy_type == \"http\":\n                protocol = HTTP\n            else:\n                raise ValueError(\"Unknown proxy protocol type: {}\".format(proxy_type))\n\n            # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature\n            return protocol, addr, port, rdns, username, password\n\n    async def _proxy_connect(self, timeout=None, local_addr=None):\n        if isinstance(self._proxy, (tuple, list)):\n            parsed = self._parse_proxy(*self._proxy)\n        elif isinstance(self._proxy, dict):\n            parsed = self._parse_proxy(**self._proxy)\n        else:\n            raise TypeError(\"Proxy of unknown format: {}\".format(type(self._proxy)))\n\n        # Always prefer `python_socks` when available\n        if python_socks:\n            # python_socks internal errors are not inherited from\n            # builtin IOError (just from Exception). Instead of adding those\n            # in exceptions clauses everywhere through the code, we\n            # rather monkey-patch them in place. Keep in mind that\n            # ProxyError takes error_code as keyword argument.\n\n            class ConnectionErrorExtra(ConnectionError):\n                def __init__(self, message, error_code=None):\n                    super().__init__(message)\n                    self.error_code = error_code\n\n            python_socks._errors.ProxyError = ConnectionErrorExtra\n            python_socks._errors.ProxyConnectionError = ConnectionError\n            python_socks._errors.ProxyTimeoutError = ConnectionError\n\n            from python_socks.async_.asyncio import Proxy\n\n            proxy = Proxy.create(*parsed)\n\n            # WARNING: If `local_addr` is set we use manual socket creation, because,\n            # unfortunately, `Proxy.connect()` does not expose `local_addr`\n            # argument, so if we want to bind socket locally, we need to manually\n            # create, bind and connect socket, and then pass to `Proxy.connect()` method.\n\n            if local_addr is None:\n                sock = await proxy.connect(\n                    dest_host=self._ip,\n                    dest_port=self._port,\n                    timeout=timeout\n                )\n            else:\n                # Here we start manual setup of the socket.\n                # The `address` represents the proxy ip and proxy port,\n                # not the destination one (!), because the socket\n                # connects to the proxy server, not destination server.\n                # IPv family is also checked on proxy address.\n                if ':' in proxy.proxy_host:\n                    mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0)\n                else:\n                    mode, address = socket.AF_INET, (proxy.proxy_host, proxy.proxy_port)\n\n                # Create a non-blocking socket and bind it (if local address is specified).\n                sock = socket.socket(mode, socket.SOCK_STREAM)\n                sock.setblocking(False)\n                sock.bind(local_addr)\n\n                # Actual TCP connection is performed here.\n                await asyncio.wait_for(\n                    helpers.get_running_loop().sock_connect(sock=sock, address=address),\n                    timeout=timeout\n                )\n\n                # As our socket is already created and connected,\n                # this call sets the destination host/port and\n                # starts protocol negotiations with the proxy server.\n                sock = await proxy.connect(\n                    dest_host=self._ip,\n                    dest_port=self._port,\n                    timeout=timeout,\n                    _socket=sock\n                )\n\n        else:\n            import socks\n\n            # Here `address` represents destination address (not proxy), because of\n            # the `PySocks` implementation of the connection routine.\n            # IPv family is checked on proxy address, not destination address.\n            if ':' in parsed[1]:\n                mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0)\n            else:\n                mode, address = socket.AF_INET, (self._ip, self._port)\n\n            # Setup socket, proxy, timeout and bind it (if necessary).\n            sock = socks.socksocket(mode, socket.SOCK_STREAM)\n            sock.set_proxy(*parsed)\n            sock.settimeout(timeout)\n\n            if local_addr is not None:\n                sock.bind(local_addr)\n\n            # Actual TCP connection and negotiation performed here.\n            await asyncio.wait_for(\n                helpers.get_running_loop().sock_connect(sock=sock, address=address),\n                timeout=timeout\n            )\n\n            sock.setblocking(False)\n\n        return sock\n\n    async def _connect(self, timeout=None, ssl=None):\n        if self._local_addr is not None:\n            # NOTE: If port is not specified, we use 0 port\n            # to notify the OS that port should be chosen randomly\n            # from the available ones.\n            if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2:\n                local_addr = self._local_addr\n            elif isinstance(self._local_addr, str):\n                local_addr = (self._local_addr, 0)\n            else:\n                raise ValueError(\"Unknown local address format: {}\".format(self._local_addr))\n        else:\n            local_addr = None\n\n        if not self._proxy:\n            self._reader, self._writer = await asyncio.wait_for(\n                asyncio.open_connection(\n                    host=self._ip,\n                    port=self._port,\n                    ssl=ssl,\n                    local_addr=local_addr\n                ), timeout=timeout)\n        else:\n            # Proxy setup, connection and negotiation is performed here.\n            sock = await self._proxy_connect(\n                timeout=timeout,\n                local_addr=local_addr\n            )\n\n            # Wrap socket in SSL context (if provided)\n            if ssl:\n                sock = self._wrap_socket_ssl(sock)\n\n            self._reader, self._writer = await asyncio.open_connection(sock=sock)\n\n        self._codec = self.packet_codec(self)\n        self._init_conn()\n        await self._writer.drain()\n\n    async def connect(self, timeout=None, ssl=None):\n        \"\"\"\n        Establishes a connection with the server.\n        \"\"\"\n        await self._connect(timeout=timeout, ssl=ssl)\n        self._connected = True\n\n        loop = helpers.get_running_loop()\n        self._send_task = loop.create_task(self._send_loop())\n        self._recv_task = loop.create_task(self._recv_loop())\n\n    async def disconnect(self):\n        \"\"\"\n        Disconnects from the server, and clears\n        pending outgoing and incoming messages.\n        \"\"\"\n        if not self._connected:\n            return\n\n        self._connected = False\n\n        await helpers._cancel(\n            self._log,\n            send_task=self._send_task,\n            recv_task=self._recv_task\n        )\n\n        if self._writer:\n            self._writer.close()\n            if sys.version_info >= (3, 7):\n                try:\n                    await asyncio.wait_for(self._writer.wait_closed(), timeout=10)\n                except asyncio.TimeoutError:\n                    # See issue #3917. For some users, this line was hanging indefinitely.\n                    # The hard timeout is not ideal (connection won't be properly closed),\n                    # but the code will at least be able to procceed.\n                    self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup')\n                except Exception as e:\n                    # Disconnecting should never raise. Seen:\n                    # * OSError: No route to host and\n                    # * OSError: [Errno 32] Broken pipe\n                    # * ConnectionResetError\n                    self._log.info('%s during disconnect: %s', type(e), e)\n\n    def send(self, data):\n        \"\"\"\n        Sends a packet of data through this connection mode.\n\n        This method returns a coroutine.\n        \"\"\"\n        if not self._connected:\n            raise ConnectionError('Not connected')\n\n        return self._send_queue.put(data)\n\n    async def recv(self):\n        \"\"\"\n        Receives a packet of data through this connection mode.\n\n        This method returns a coroutine.\n        \"\"\"\n        while self._connected:\n            result, err = await self._recv_queue.get()\n            if err:\n                raise err\n            if result:\n                return result\n\n        raise ConnectionError('Not connected')\n\n    async def _send_loop(self):\n        \"\"\"\n        This loop is constantly popping items off the queue to send them.\n        \"\"\"\n        try:\n            while self._connected:\n                self._send(await self._send_queue.get())\n                await self._writer.drain()\n        except asyncio.CancelledError:\n            pass\n        except Exception as e:\n            if isinstance(e, IOError):\n                self._log.info('The server closed the connection while sending')\n            else:\n                self._log.exception('Unexpected exception in the send loop')\n\n            await self.disconnect()\n\n    async def _recv_loop(self):\n        \"\"\"\n        This loop is constantly putting items on the queue as they're read.\n        \"\"\"\n        try:\n            while self._connected:\n                try:\n                    data = await self._recv()\n                except asyncio.CancelledError:\n                    break\n                except (IOError, asyncio.IncompleteReadError) as e:\n                    self._log.warning('Server closed the connection: %s', e)\n                    await self._recv_queue.put((None, e))\n                    await self.disconnect()\n                except InvalidChecksumError as e:\n                    self._log.warning('Server response had invalid checksum: %s', e)\n                    await self._recv_queue.put((None, e))\n                except InvalidBufferError as e:\n                    self._log.warning('Server response had invalid buffer: %s', e)\n                    await self._recv_queue.put((None, e))\n                except Exception as e:\n                    self._log.exception('Unexpected exception in the receive loop')\n                    await self._recv_queue.put((None, e))\n                    await self.disconnect()\n                else:\n                    await self._recv_queue.put((data, None))\n        finally:\n            await self.disconnect()\n\n\n    def _init_conn(self):\n        \"\"\"\n        This method will be called after `connect` is called.\n        After this method finishes, the writer will be drained.\n\n        Subclasses should make use of this if they need to send\n        data to Telegram to indicate which connection mode will\n        be used.\n        \"\"\"\n        if self._codec.tag:\n            self._writer.write(self._codec.tag)\n\n    def _send(self, data):\n        self._writer.write(self._codec.encode_packet(data))\n\n    async def _recv(self):\n        return await self._codec.read_packet(self._reader)\n\n    def __str__(self):\n        return '{}:{}/{}'.format(\n            self._ip, self._port,\n            self.__class__.__name__.replace('Connection', '')\n        )\n\n\nclass ObfuscatedConnection(Connection):\n    \"\"\"\n    Base class for \"obfuscated\" connections (\"obfuscated2\", \"mtproto proxy\")\n    \"\"\"\n    \"\"\"\n    This attribute should be redefined by subclasses\n    \"\"\"\n    obfuscated_io = None\n\n    def _init_conn(self):\n        self._obfuscation = self.obfuscated_io(self)\n        self._writer.write(self._obfuscation.header)\n\n    def _send(self, data):\n        self._obfuscation.write(self._codec.encode_packet(data))\n\n    async def _recv(self):\n        return await self._codec.read_packet(self._obfuscation)\n\n\nclass PacketCodec(abc.ABC):\n    \"\"\"\n    Base class for packet codecs\n    \"\"\"\n\n    \"\"\"\n    This attribute should be re-defined by subclass to define if some\n    \"magic bytes\" should be sent to server right after connection is made to\n    signal which protocol will be used\n    \"\"\"\n    tag = None\n\n    def __init__(self, connection):\n        \"\"\"\n        Codec is created when connection is just made.\n        \"\"\"\n        self._conn = connection\n\n    @abc.abstractmethod\n    def encode_packet(self, data):\n        \"\"\"\n        Encodes single packet and returns encoded bytes.\n        \"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    async def read_packet(self, reader):\n        \"\"\"\n        Reads single packet from `reader` object that should have\n        `readexactly(n)` method.\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "telethon/network/connection/http.py",
    "content": "import asyncio\n\nfrom .connection import Connection, PacketCodec\n\n\nSSL_PORT = 443\n\n\nclass HttpPacketCodec(PacketCodec):\n    tag = None\n    obfuscate_tag = None\n\n    def encode_packet(self, data):\n        return ('POST /api HTTP/1.1\\r\\n'\n                'Host: {}:{}\\r\\n'\n                'Content-Type: application/x-www-form-urlencoded\\r\\n'\n                'Connection: keep-alive\\r\\n'\n                'Keep-Alive: timeout=100000, max=10000000\\r\\n'\n                'Content-Length: {}\\r\\n\\r\\n'\n                .format(self._conn._ip, self._conn._port, len(data))\n                .encode('ascii') + data)\n\n    async def read_packet(self, reader):\n        while True:\n            line = await reader.readline()\n            if not line or line[-1] != b'\\n':\n                raise asyncio.IncompleteReadError(line, None)\n\n            if line.lower().startswith(b'content-length: '):\n                await reader.readexactly(2)\n                length = int(line[16:-2])\n                return await reader.readexactly(length)\n\n\nclass ConnectionHttp(Connection):\n    packet_codec = HttpPacketCodec\n\n    async def connect(self, timeout=None, ssl=None):\n        await super().connect(timeout=timeout, ssl=self._port == SSL_PORT)\n"
  },
  {
    "path": "telethon/network/connection/tcpabridged.py",
    "content": "import struct\n\nfrom .connection import Connection, PacketCodec\n\n\nclass AbridgedPacketCodec(PacketCodec):\n    tag = b'\\xef'\n    obfuscate_tag = b'\\xef\\xef\\xef\\xef'\n\n    def encode_packet(self, data):\n        length = len(data) >> 2\n        if length < 127:\n            length = struct.pack('B', length)\n        else:\n            length = b'\\x7f' + int.to_bytes(length, 3, 'little')\n        return length + data\n\n    async def read_packet(self, reader):\n        length = struct.unpack('<B', await reader.readexactly(1))[0]\n        if length >= 127:\n            length = struct.unpack(\n                '<i', await reader.readexactly(3) + b'\\0')[0]\n\n        return await reader.readexactly(length << 2)\n\n\nclass ConnectionTcpAbridged(Connection):\n    \"\"\"\n    This is the mode with the lowest overhead, as it will\n    only require 1 byte if the packet length is less than\n    508 bytes (127 << 2, which is very common).\n    \"\"\"\n    packet_codec = AbridgedPacketCodec\n"
  },
  {
    "path": "telethon/network/connection/tcpfull.py",
    "content": "import asyncio\nimport struct\nfrom zlib import crc32\n\nfrom .connection import Connection, PacketCodec\nfrom ...errors import InvalidChecksumError, InvalidBufferError\n\n\nclass FullPacketCodec(PacketCodec):\n    tag = None\n\n    def __init__(self, connection):\n        super().__init__(connection)\n        self._send_counter = 0  # Important or Telegram won't reply\n\n    def encode_packet(self, data):\n        # https://core.telegram.org/mtproto#tcp-transport\n        # total length, sequence number, packet and checksum (CRC32)\n        length = len(data) + 12\n        data = struct.pack('<ii', length, self._send_counter) + data\n        crc = struct.pack('<I', crc32(data))\n        self._send_counter += 1\n        return data + crc\n\n    async def read_packet(self, reader):\n        try:\n            packet_len_seq = await reader.readexactly(8)  # 4 and 4\n        except asyncio.IncompleteReadError as exc:\n            return exc.partial\n        packet_len, seq = struct.unpack('<ii', packet_len_seq)\n        if packet_len < 0 and seq < 0:\n            # It has been observed that the length and seq can be -429,\n            # followed by the body of 4 bytes also being -429.\n            # See https://github.com/LonamiWebs/Telethon/issues/4042.\n            body = await reader.readexactly(4)\n            raise InvalidBufferError(body)\n        elif packet_len < 8:\n            # Currently unknown why packet_len may be less than 8 but not negative.\n            # Attempting to `readexactly` with less than 0 fails without saying what\n            # the number was which is less helpful.\n            raise InvalidBufferError(packet_len_seq)\n\n        body = await reader.readexactly(packet_len - 8)\n        checksum = struct.unpack('<I', body[-4:])[0]\n        body = body[:-4]\n\n        valid_checksum = crc32(packet_len_seq + body)\n        if checksum != valid_checksum:\n            raise InvalidChecksumError(checksum, valid_checksum)\n\n        return body\n\n\nclass ConnectionTcpFull(Connection):\n    \"\"\"\n    Default Telegram mode. Sends 12 additional bytes and\n    needs to calculate the CRC value of the packet itself.\n    \"\"\"\n    packet_codec = FullPacketCodec\n"
  },
  {
    "path": "telethon/network/connection/tcpintermediate.py",
    "content": "import struct\nimport random\nimport os\n\nfrom .connection import Connection, PacketCodec\n\n\nclass IntermediatePacketCodec(PacketCodec):\n    tag = b'\\xee\\xee\\xee\\xee'\n    obfuscate_tag = tag\n\n    def encode_packet(self, data):\n        return struct.pack('<i', len(data)) + data\n\n    async def read_packet(self, reader):\n        length = struct.unpack('<i', await reader.readexactly(4))[0]\n        return await reader.readexactly(length)\n\n\nclass RandomizedIntermediatePacketCodec(IntermediatePacketCodec):\n    \"\"\"\n    Data packets are aligned to 4bytes. This codec adds random bytes of size\n    from 0 to 3 bytes, which are ignored by decoder.\n    \"\"\"\n    tag = None\n    obfuscate_tag = b'\\xdd\\xdd\\xdd\\xdd'\n\n    def encode_packet(self, data):\n        pad_size = random.randint(0, 3)\n        padding = os.urandom(pad_size)\n        return super().encode_packet(data + padding)\n\n    async def read_packet(self, reader):\n        packet_with_padding = await super().read_packet(reader)\n        pad_size = len(packet_with_padding) % 4\n        if pad_size > 0:\n            return packet_with_padding[:-pad_size]\n        return packet_with_padding\n\n\nclass ConnectionTcpIntermediate(Connection):\n    \"\"\"\n    Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`.\n    Always sends 4 extra bytes for the packet length.\n    \"\"\"\n    packet_codec = IntermediatePacketCodec\n"
  },
  {
    "path": "telethon/network/connection/tcpmtproxy.py",
    "content": "import asyncio\nimport hashlib\nimport base64\nimport os\n\nfrom .connection import ObfuscatedConnection\nfrom .tcpabridged import AbridgedPacketCodec\nfrom .tcpintermediate import (\n    IntermediatePacketCodec,\n    RandomizedIntermediatePacketCodec\n)\n\nfrom ...crypto import AESModeCTR\n\n\nclass MTProxyIO:\n    \"\"\"\n    It's very similar to tcpobfuscated.ObfuscatedIO, but the way\n    encryption keys, protocol tag and dc_id are encoded is different.\n    \"\"\"\n    header = None\n\n    def __init__(self, connection):\n        self._reader = connection._reader\n        self._writer = connection._writer\n\n        (self.header,\n         self._encrypt,\n         self._decrypt) = self.init_header(\n             connection._secret, connection._dc_id, connection.packet_codec)\n\n    @staticmethod\n    def init_header(secret, dc_id, packet_codec):\n        # Validate\n        is_dd = (len(secret) == 17) and (secret[0] == 0xDD)\n        is_rand_codec = issubclass(\n            packet_codec, RandomizedIntermediatePacketCodec)\n        if is_dd and not is_rand_codec:\n            raise ValueError(\n                \"Only RandomizedIntermediate can be used with dd-secrets\")\n        secret = secret[1:] if is_dd else secret\n        if len(secret) != 16:\n            raise ValueError(\n                \"MTProxy secret must be a hex-string representing 16 bytes\")\n\n        # Obfuscated messages secrets cannot start with any of these\n        keywords = (b'PVrG', b'GET ', b'POST', b'\\xee\\xee\\xee\\xee')\n        while True:\n            random = os.urandom(64)\n            if (random[0] != 0xef and\n                    random[:4] not in keywords and\n                    random[4:4] != b'\\0\\0\\0\\0'):\n                break\n\n        random = bytearray(random)\n        random_reversed = random[55:7:-1]  # Reversed (8, len=48)\n\n        # Encryption has \"continuous buffer\" enabled\n        encrypt_key = hashlib.sha256(\n            bytes(random[8:40]) + secret).digest()\n        encrypt_iv = bytes(random[40:56])\n        decrypt_key = hashlib.sha256(\n            bytes(random_reversed[:32]) + secret).digest()\n        decrypt_iv = bytes(random_reversed[32:48])\n\n        encryptor = AESModeCTR(encrypt_key, encrypt_iv)\n        decryptor = AESModeCTR(decrypt_key, decrypt_iv)\n\n        random[56:60] = packet_codec.obfuscate_tag\n\n        dc_id_bytes = dc_id.to_bytes(2, \"little\", signed=True)\n        random = random[:60] + dc_id_bytes + random[62:]\n        random[56:64] = encryptor.encrypt(bytes(random))[56:64]\n        return (random, encryptor, decryptor)\n\n    async def readexactly(self, n):\n        return self._decrypt.encrypt(await self._reader.readexactly(n))\n\n    def write(self, data):\n        self._writer.write(self._encrypt.encrypt(data))\n\n\nclass TcpMTProxy(ObfuscatedConnection):\n    \"\"\"\n    Connector which allows user to connect to the Telegram via proxy servers\n    commonly known as MTProxy.\n    Implemented very ugly due to the leaky abstractions in Telethon networking\n    classes that should be refactored later (TODO).\n\n    .. warning::\n\n        The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to\n        be changed. You shouldn't be using this class yet.\n    \"\"\"\n    packet_codec = None\n    obfuscated_io = MTProxyIO\n\n    # noinspection PyUnusedLocal\n    def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None):\n        # connect to proxy's host and port instead of telegram's ones\n        proxy_host, proxy_port = self.address_info(proxy)\n        self._secret = self.normalize_secret(proxy[2])\n        super().__init__(\n            proxy_host, proxy_port, dc_id, loggers=loggers)\n\n    async def _connect(self, timeout=None, ssl=None):\n        await super()._connect(timeout=timeout, ssl=ssl)\n\n        # Wait for EOF for 2 seconds (or if _wait_for_data's definition\n        # is missing or different, just sleep for 2 seconds). This way\n        # we give the proxy a chance to close the connection if the current\n        # codec (which the proxy detects with the data we sent) cannot\n        # be used for this proxy. This is a work around for #1134.\n        # TODO Sleeping for N seconds may not be the best solution\n        # TODO This fix could be welcome for HTTP proxies as well\n        try:\n            await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2)\n        except asyncio.TimeoutError:\n            pass\n        except Exception:\n            await asyncio.sleep(2)\n\n        if self._reader.at_eof():\n            await self.disconnect()\n            raise ConnectionError(\n                'Proxy closed the connection after sending initial payload')\n\n    @staticmethod\n    def address_info(proxy_info):\n        if proxy_info is None:\n            raise ValueError(\"No proxy info specified for MTProxy connection\")\n        return proxy_info[:2]\n\n    @staticmethod\n    def normalize_secret(secret):\n        if secret[:2] in (\"ee\", \"dd\"):  # Remove extra bytes\n            secret = secret[2:]\n\n        try:\n            secret_bytes = bytes.fromhex(secret)\n        except ValueError:\n            secret = secret + '=' * (-len(secret) % 4)\n            secret_bytes = base64.b64decode(secret.encode())\n\n        return secret_bytes[:16]  # Remove the domain from the secret (until domain support is added)\n\nclass ConnectionTcpMTProxyAbridged(TcpMTProxy):\n    \"\"\"\n    Connect to proxy using abridged protocol\n    \"\"\"\n    packet_codec = AbridgedPacketCodec\n\n\nclass ConnectionTcpMTProxyIntermediate(TcpMTProxy):\n    \"\"\"\n    Connect to proxy using intermediate protocol\n    \"\"\"\n    packet_codec = IntermediatePacketCodec\n\n\nclass ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy):\n    \"\"\"\n    Connect to proxy using randomized intermediate protocol (dd-secrets)\n    \"\"\"\n    packet_codec = RandomizedIntermediatePacketCodec\n"
  },
  {
    "path": "telethon/network/connection/tcpobfuscated.py",
    "content": "import os\n\nfrom .tcpabridged import AbridgedPacketCodec\nfrom .connection import ObfuscatedConnection\n\nfrom ...crypto import AESModeCTR\n\n\nclass ObfuscatedIO:\n    header = None\n\n    def __init__(self, connection):\n        self._reader = connection._reader\n        self._writer = connection._writer\n\n        (self.header,\n         self._encrypt,\n         self._decrypt) = self.init_header(connection.packet_codec)\n\n    @staticmethod\n    def init_header(packet_codec):\n        # Obfuscated messages secrets cannot start with any of these\n        keywords = (b'PVrG', b'GET ', b'POST', b'\\xee\\xee\\xee\\xee')\n        while True:\n            random = os.urandom(64)\n            if (random[0] != 0xef and\n                    random[:4] not in keywords and\n                    random[4:8] != b'\\0\\0\\0\\0'):\n                break\n\n        random = bytearray(random)\n        random_reversed = random[55:7:-1]  # Reversed (8, len=48)\n\n        # Encryption has \"continuous buffer\" enabled\n        encrypt_key = bytes(random[8:40])\n        encrypt_iv = bytes(random[40:56])\n        decrypt_key = bytes(random_reversed[:32])\n        decrypt_iv = bytes(random_reversed[32:48])\n\n        encryptor = AESModeCTR(encrypt_key, encrypt_iv)\n        decryptor = AESModeCTR(decrypt_key, decrypt_iv)\n\n        random[56:60] = packet_codec.obfuscate_tag\n        random[56:64] = encryptor.encrypt(bytes(random))[56:64]\n        return (random, encryptor, decryptor)\n\n    async def readexactly(self, n):\n        return self._decrypt.encrypt(await self._reader.readexactly(n))\n\n    def write(self, data):\n        self._writer.write(self._encrypt.encrypt(data))\n\n\nclass ConnectionTcpObfuscated(ObfuscatedConnection):\n    \"\"\"\n    Mode that Telegram defines as \"obfuscated2\". Encodes the packet\n    just like `ConnectionTcpAbridged`, but encrypts every message with\n    a randomly generated key using the AES-CTR mode so the packets are\n    harder to discern.\n    \"\"\"\n    obfuscated_io = ObfuscatedIO\n    packet_codec = AbridgedPacketCodec\n"
  },
  {
    "path": "telethon/network/mtprotoplainsender.py",
    "content": "\"\"\"\nThis module contains the class used to communicate with Telegram's servers\nin plain text, when no authorization key has been created yet.\n\"\"\"\nimport struct\n\nfrom .mtprotostate import MTProtoState\nfrom ..errors import InvalidBufferError\nfrom ..extensions import BinaryReader\n\n\nclass MTProtoPlainSender:\n    \"\"\"\n    MTProto Mobile Protocol plain sender\n    (https://core.telegram.org/mtproto/description#unencrypted-messages)\n    \"\"\"\n    def __init__(self, connection, *, loggers):\n        \"\"\"\n        Initializes the MTProto plain sender.\n\n        :param connection: the Connection to be used.\n        \"\"\"\n        self._state = MTProtoState(auth_key=None, loggers=loggers)\n        self._connection = connection\n\n    async def send(self, request):\n        \"\"\"\n        Sends and receives the result for the given request.\n        \"\"\"\n        body = bytes(request)\n        msg_id = self._state._get_new_msg_id()\n        await self._connection.send(\n            struct.pack('<qqi', 0, msg_id, len(body)) + body\n        )\n\n        body = await self._connection.recv()\n        if len(body) < 8:\n            raise InvalidBufferError(body)\n\n        with BinaryReader(body) as reader:\n            auth_key_id = reader.read_long()\n            assert auth_key_id == 0, 'Bad auth_key_id'\n\n            msg_id = reader.read_long()\n            assert msg_id != 0,  'Bad msg_id'\n            # ^ We should make sure that the read ``msg_id`` is greater\n            # than our own ``msg_id``. However, under some circumstances\n            # (bad system clock/working behind proxies) this seems to not\n            # be the case, which would cause endless assertion errors.\n\n            length = reader.read_int()\n            assert length > 0,  'Bad length'\n            # We could read length bytes and use those in a new reader to read\n            # the next TLObject without including the padding, but since the\n            # reader isn't used for anything else after this, it's unnecessary.\n            return reader.tgread_object()\n"
  },
  {
    "path": "telethon/network/mtprotosender.py",
    "content": "import asyncio\nimport collections\nimport struct\nimport datetime\nimport time\n\nfrom . import authenticator\nfrom ..extensions.messagepacker import MessagePacker\nfrom .mtprotoplainsender import MTProtoPlainSender\nfrom .requeststate import RequestState\nfrom .mtprotostate import MTProtoState\nfrom ..tl.tlobject import TLRequest\nfrom .. import helpers, utils\nfrom ..errors import (\n    BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError,\n    TypeNotFoundError, rpc_message_to_error\n)\nfrom ..extensions import BinaryReader\nfrom ..tl.core import RpcResult, MessageContainer, GzipPacked\nfrom ..tl.functions.auth import LogOutRequest\nfrom ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest\nfrom ..tl.types import (\n    MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts,\n    MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq,\n    MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone,\n    DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail\n)\nfrom ..tl import types as _tl\nfrom ..crypto import AuthKey\nfrom ..helpers import retry_range\n\n\nclass MTProtoSender:\n    \"\"\"\n    MTProto Mobile Protocol sender\n    (https://core.telegram.org/mtproto/description).\n\n    This class is responsible for wrapping requests into `TLMessage`'s,\n    sending them over the network and receiving them in a safe manner.\n\n    Automatic reconnection due to temporary network issues is a concern\n    for this class as well, including retry of messages that could not\n    be sent successfully.\n\n    A new authorization key will be generated on connection if no other\n    key exists yet.\n    \"\"\"\n    def __init__(self, auth_key, *, loggers,\n                 retries=5, delay=1, auto_reconnect=True, connect_timeout=None,\n                 auth_key_callback=None,\n                 updates_queue=None, auto_reconnect_callback=None):\n        self._connection = None\n        self._loggers = loggers\n        self._log = loggers[__name__]\n        self._retries = retries\n        self._delay = delay\n        self._auto_reconnect = auto_reconnect\n        self._connect_timeout = connect_timeout\n        self._auth_key_callback = auth_key_callback\n        self._updates_queue = updates_queue\n        self._auto_reconnect_callback = auto_reconnect_callback\n        self._connect_lock = asyncio.Lock()\n        self._ping = None\n\n        # Whether the user has explicitly connected or disconnected.\n        #\n        # If a disconnection happens for any other reason and it\n        # was *not* user action then the pending messages won't\n        # be cleared but on explicit user disconnection all the\n        # pending futures should be cancelled.\n        self._user_connected = False\n        self._reconnecting = False\n        self.__disconnected = None\n\n        # We need to join the loops upon disconnection\n        self._send_loop_handle = None\n        self._recv_loop_handle = None\n\n        # Preserving the references of the AuthKey and state is important\n        self.auth_key = auth_key or AuthKey(None)\n        self._state = MTProtoState(self.auth_key, loggers=self._loggers)\n\n        # Outgoing messages are put in a queue and sent in a batch.\n        # Note that here we're also storing their ``_RequestState``.\n        self._send_queue = MessagePacker(self._state, loggers=self._loggers)\n\n        # Sent states are remembered until a response is received.\n        self._pending_state = {}\n\n        # Responses must be acknowledged, and we can also batch these.\n        self._pending_ack = set()\n\n        # Similar to pending_messages but only for the last acknowledges.\n        # These can't go in pending_messages because no acknowledge for them\n        # is received, but we may still need to resend their state on bad salts.\n        self._last_acks = collections.deque(maxlen=10)\n\n        # Jump table from response ID to method that handles it\n        self._handlers = {\n            RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result,\n            MessageContainer.CONSTRUCTOR_ID: self._handle_container,\n            GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed,\n            Pong.CONSTRUCTOR_ID: self._handle_pong,\n            BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt,\n            BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification,\n            MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info,\n            MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info,\n            NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created,\n            MsgsAck.CONSTRUCTOR_ID: self._handle_ack,\n            FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts,\n            MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten,\n            MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten,\n            MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all,\n            DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session,\n            DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session,\n            DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key,\n            DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key,\n            DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key,\n        }\n\n    # Public API\n\n    async def connect(self, connection):\n        \"\"\"\n        Connects to the specified given connection using the given auth key.\n        \"\"\"\n        async with self._connect_lock:\n            if self._user_connected:\n                self._log.info('User is already connected!')\n                return False\n\n            self._connection = connection\n            await self._connect()\n            self._user_connected = True\n            return True\n\n    def is_connected(self):\n        return self._user_connected\n\n    def _transport_connected(self):\n        return (\n            not self._reconnecting\n            and self._connection is not None\n            and self._connection._connected\n        )\n\n    async def disconnect(self):\n        \"\"\"\n        Cleanly disconnects the instance from the network, cancels\n        all pending requests, and closes the send and receive loops.\n        \"\"\"\n        await self._disconnect()\n\n    def send(self, request, ordered=False):\n        \"\"\"\n        This method enqueues the given request to be sent. Its send\n        state will be saved until a response arrives, and a ``Future``\n        that will be resolved when the response arrives will be returned:\n\n        .. code-block:: python\n\n            async def method():\n                # Sending (enqueued for the send loop)\n                future = sender.send(request)\n                # Receiving (waits for the receive loop to read the result)\n                result = await future\n\n        Designed like this because Telegram may send the response at\n        any point, and it can send other items while one waits for it.\n        Once the response for this future arrives, it is set with the\n        received result, quite similar to how a ``receive()`` call\n        would otherwise work.\n\n        Since the receiving part is \"built in\" the future, it's\n        impossible to await receive a result that was never sent.\n        \"\"\"\n        if not self._user_connected:\n            raise ConnectionError('Cannot send requests while disconnected')\n\n        if not utils.is_list_like(request):\n            try:\n                state = RequestState(request)\n            except struct.error as e:\n                # \"struct.error: required argument is not an integer\" is not\n                # very helpful; log the request to find out what wasn't int.\n                self._log.error('Request caused struct.error: %s: %s', e, request)\n                raise\n\n            self._send_queue.append(state)\n            return state.future\n        else:\n            states = []\n            futures = []\n            state = None\n            for req in request:\n                try:\n                    state = RequestState(req, after=ordered and state)\n                except struct.error as e:\n                    self._log.error('Request caused struct.error: %s: %s', e, request)\n                    raise\n\n                states.append(state)\n                futures.append(state.future)\n\n            self._send_queue.extend(states)\n            return futures\n\n    @property\n    def disconnected(self):\n        \"\"\"\n        Future that resolves when the connection to Telegram\n        ends, either by user action or in the background.\n\n        Note that it may resolve in either a ``ConnectionError``\n        or any other unexpected error that could not be handled.\n        \"\"\"\n        return asyncio.shield(self._disconnected)\n\n    @property\n    def _disconnected(self):\n        if self.__disconnected is None:\n            self.__disconnected = helpers.get_running_loop().create_future()\n            self.__disconnected.set_result(None)\n        return self.__disconnected\n\n    # Private methods\n\n    async def _connect(self):\n        \"\"\"\n        Performs the actual connection, retrying, generating the\n        authorization key if necessary, and starting the send and\n        receive loops.\n        \"\"\"\n        self._log.info('Connecting to %s...', self._connection)\n\n        connected = False\n\n        for attempt in retry_range(self._retries):\n            if not connected:\n                connected = await self._try_connect(attempt)\n                if not connected:\n                    continue  # skip auth key generation until we're connected\n\n            if not self.auth_key:\n                try:\n                    if not await self._try_gen_auth_key(attempt):\n                        continue  # keep retrying until we have the auth key\n                except (IOError, asyncio.TimeoutError) as e:\n                    # Sometimes, specially during user-DC migrations,\n                    # Telegram may close the connection during auth_key\n                    # generation. If that's the case, we will need to\n                    # connect again.\n                    self._log.warning('Connection error %d during auth_key gen: %s: %s',\n                                      attempt, type(e).__name__, e)\n\n                    # Whatever the IOError was, make sure to disconnect so we can\n                    # reconnect cleanly after.\n                    await self._connection.disconnect()\n                    connected = False\n                    await asyncio.sleep(self._delay)\n                    continue  # next iteration we will try to reconnect\n\n            break  # all steps done, break retry loop\n        else:\n            if not connected:\n                raise ConnectionError('Connection to Telegram failed {} time(s)'.format(self._retries))\n\n            e = ConnectionError('auth_key generation failed {} time(s)'.format(self._retries))\n            await self._disconnect(error=e)\n            raise e\n\n        loop = helpers.get_running_loop()\n        self._log.debug('Starting send loop')\n        self._send_loop_handle = loop.create_task(self._send_loop())\n\n        self._log.debug('Starting receive loop')\n        self._recv_loop_handle = loop.create_task(self._recv_loop())\n\n        # _disconnected only completes after manual disconnection\n        # or errors after which the sender cannot continue such\n        # as failing to reconnect or any unexpected error.\n        if self._disconnected.done():\n            self.__disconnected = loop.create_future()\n\n        self._log.info('Connection to %s complete!', self._connection)\n\n    async def _try_connect(self, attempt):\n        try:\n            self._log.debug('Connection attempt %d...', attempt)\n            await self._connection.connect(timeout=self._connect_timeout)\n            self._log.debug('Connection success!')\n            return True\n        except (IOError, asyncio.TimeoutError) as e:\n            self._log.warning('Attempt %d at connecting failed: %s: %s',\n                              attempt, type(e).__name__, e)\n            await asyncio.sleep(self._delay)\n            return False\n\n    async def _try_gen_auth_key(self, attempt):\n        plain = MTProtoPlainSender(self._connection, loggers=self._loggers)\n        try:\n            self._log.debug('New auth_key attempt %d...', attempt)\n            self.auth_key.key, self._state.time_offset = \\\n                await authenticator.do_authentication(plain)\n\n            # This is *EXTREMELY* important since we don't control\n            # external references to the authorization key, we must\n            # notify whenever we change it. This is crucial when we\n            # switch to different data centers.\n            if self._auth_key_callback:\n                await self._auth_key_callback(self.auth_key)\n\n            self._log.debug('auth_key generation success!')\n            return True\n        except (SecurityError, AssertionError) as e:\n            self._log.warning('Attempt %d at new auth_key failed: %s', attempt, e)\n            await asyncio.sleep(self._delay)\n            return False\n\n    async def _disconnect(self, error=None):\n        if self._connection is None:\n            self._log.info('Not disconnecting (already have no connection)')\n            return\n\n        self._log.info('Disconnecting from %s...', self._connection)\n        self._user_connected = False\n        try:\n            self._log.debug('Closing current connection...')\n            await self._connection.disconnect()\n        finally:\n            self._log.debug('Cancelling %d pending message(s)...', len(self._pending_state))\n            for state in self._pending_state.values():\n                if error and not state.future.done():\n                    state.future.set_exception(error)\n                else:\n                    state.future.cancel()\n\n            self._pending_state.clear()\n            await helpers._cancel(\n                self._log,\n                send_loop_handle=self._send_loop_handle,\n                recv_loop_handle=self._recv_loop_handle\n            )\n\n            self._log.info('Disconnection from %s complete!', self._connection)\n            self._connection = None\n\n        if self._disconnected and not self._disconnected.done():\n            if error:\n                self._disconnected.set_exception(error)\n            else:\n                self._disconnected.set_result(None)\n\n    async def _reconnect(self, last_error):\n        \"\"\"\n        Cleanly disconnects and then reconnects.\n        \"\"\"\n        self._log.info('Closing current connection to begin reconnect...')\n        await self._connection.disconnect()\n\n        await helpers._cancel(\n            self._log,\n            send_loop_handle=self._send_loop_handle,\n            recv_loop_handle=self._recv_loop_handle\n        )\n\n        # TODO See comment in `_start_reconnect`\n        # Perhaps this should be the last thing to do?\n        # But _connect() creates tasks which may run and,\n        # if they see that reconnecting is True, they will end.\n        # Perhaps that task creation should not belong in connect?\n        self._reconnecting = False\n\n        # Start with a clean state (and thus session ID) to avoid old msgs\n        self._state.reset()\n\n        retries = self._retries if self._auto_reconnect else 0\n\n        attempt = 0\n        ok = True\n        # We're already \"retrying\" to connect, so we don't want to force retries\n        for attempt in retry_range(retries, force_retry=False):\n            try:\n                await self._connect()\n            except (IOError, asyncio.TimeoutError) as e:\n                last_error = e\n                self._log.info('Failed reconnection attempt %d with %s',\n                               attempt, e.__class__.__name__)\n                await asyncio.sleep(self._delay)\n            except BufferError as e:\n                # TODO there should probably only be one place to except all these errors\n                if isinstance(e, InvalidBufferError) and e.code == 404:\n                    self._log.info('Server does not know about the current auth key; the session may need to be recreated')\n                    last_error = AuthKeyNotFound()\n                    ok = False\n                    break\n                else:\n                    self._log.warning('Invalid buffer %s', e)\n\n            except Exception as e:\n                last_error = e\n                self._log.exception('Unexpected exception reconnecting on '\n                                    'attempt %d', attempt)\n\n                await asyncio.sleep(self._delay)\n            else:\n                self._send_queue.extend(self._pending_state.values())\n                self._pending_state.clear()\n\n                if self._auto_reconnect_callback:\n                    helpers.get_running_loop().create_task(self._auto_reconnect_callback())\n\n                break\n        else:\n            ok = False\n\n        if not ok:\n            self._log.error('Automatic reconnection failed %d time(s)', attempt)\n            # There may be no error (e.g. automatic reconnection was turned off).\n            error = last_error.with_traceback(None) if last_error else None\n            await self._disconnect(error=error)\n\n    def _start_reconnect(self, error):\n        \"\"\"Starts a reconnection in the background.\"\"\"\n        if self._user_connected and not self._reconnecting:\n            # We set reconnecting to True here and not inside the new task\n            # because it may happen that send/recv loop calls this again\n            # while the new task hasn't had a chance to run yet. This race\n            # condition puts `self.connection` in a bad state with two calls\n            # to its `connect` without disconnecting, so it creates a second\n            # receive loop. There can't be two tasks receiving data from\n            # the reader, since that causes an error, and the library just\n            # gets stuck.\n            # TODO It still gets stuck? Investigate where and why.\n            self._reconnecting = True\n            helpers.get_running_loop().create_task(self._reconnect(error))\n\n    def _keepalive_ping(self, rnd_id):\n        \"\"\"\n        Send a keep-alive ping. If a pong for the last ping was not received\n        yet, this means we're probably not connected.\n        \"\"\"\n        # TODO this is ugly, update loop shouldn't worry about this, sender should\n        if self._ping is None:\n            self._ping = rnd_id\n            self.send(PingRequest(rnd_id))\n        else:\n            self._start_reconnect(None)\n\n    # Loops\n\n    async def _send_loop(self):\n        \"\"\"\n        This loop is responsible for popping items off the send\n        queue, encrypting them, and sending them over the network.\n\n        Besides `connect`, only this method ever sends data.\n        \"\"\"\n        while self._user_connected and not self._reconnecting:\n            if self._pending_ack:\n                ack = RequestState(MsgsAck(list(self._pending_ack)))\n                self._send_queue.append(ack)\n                self._last_acks.append(ack)\n                self._pending_ack.clear()\n\n            self._log.debug('Waiting for messages to send...')\n            # TODO Wait for the connection send queue to be empty?\n            # This means that while it's not empty we can wait for\n            # more messages to be added to the send queue.\n            batch, data = await self._send_queue.get()\n\n            if not data:\n                continue\n\n            self._log.debug('Encrypting %d message(s) in %d bytes for sending',\n                            len(batch), len(data))\n\n            data = self._state.encrypt_message_data(data)\n\n            # Whether sending succeeds or not, the popped requests are now\n            # pending because they're removed from the queue. If a reconnect\n            # occurs, they will be removed from pending state and re-enqueued\n            # so even if the network fails they won't be lost. If they were\n            # never re-enqueued, the future waiting for a response \"locks\".\n            for state in batch:\n                if not isinstance(state, list):\n                    if isinstance(state.request, TLRequest):\n                        self._pending_state[state.msg_id] = state\n                else:\n                    for s in state:\n                        if isinstance(s.request, TLRequest):\n                            self._pending_state[s.msg_id] = s\n\n            try:\n                await self._connection.send(data)\n            except IOError as e:\n                self._log.info('Connection closed while sending data')\n                self._start_reconnect(e)\n                return\n\n            self._log.debug('Encrypted messages put in a queue to be sent')\n\n    async def _recv_loop(self):\n        \"\"\"\n        This loop is responsible for reading all incoming responses\n        from the network, decrypting and handling or dispatching them.\n\n        Besides `connect`, only this method ever receives data.\n        \"\"\"\n        while self._user_connected and not self._reconnecting:\n            self._log.debug('Receiving items from the network...')\n            try:\n                body = await self._connection.recv()\n            except asyncio.CancelledError:\n                raise  # bypass except Exception\n            except (IOError, asyncio.IncompleteReadError) as e:\n                self._log.info('Connection closed while receiving data: %s', e)\n                self._start_reconnect(e)\n                return\n            except InvalidBufferError as e:\n                if e.code == 429:\n                    self._log.warning('Server indicated flood error at transport level: %s', e)\n                    await self._disconnect(error=e)\n                else:\n                    self._log.exception('Server sent invalid buffer')\n                    self._start_reconnect(e)\n                return\n            except Exception as e:\n                self._log.exception('Unhandled error while receiving data')\n                self._start_reconnect(e)\n                return\n\n            try:\n                message = self._state.decrypt_message_data(body)\n                if message is None:\n                    continue  # this message is to be ignored\n            except TypeNotFoundError as e:\n                # Received object which we don't know how to deserialize\n                self._log.info('Type %08x not found, remaining data %r',\n                               e.invalid_constructor_id, e.remaining)\n                continue\n            except SecurityError as e:\n                # A step while decoding had the incorrect data. This message\n                # should not be considered safe and it should be ignored.\n                self._log.warning('Security error while unpacking a '\n                                  'received message: %s', e)\n                continue\n            except BufferError as e:\n                if isinstance(e, InvalidBufferError) and e.code == 404:\n                    self._log.info('Server does not know about the current auth key; the session may need to be recreated')\n                    await self._disconnect(error=AuthKeyNotFound())\n                else:\n                    self._log.warning('Invalid buffer %s', e)\n                    self._start_reconnect(e)\n                return\n            except Exception as e:\n                self._log.exception('Unhandled error while decrypting data')\n                self._start_reconnect(e)\n                return\n\n            try:\n                await self._process_message(message)\n            except Exception:\n                self._log.exception('Unhandled error while processing msgs')\n\n    # Response Handlers\n\n    async def _process_message(self, message):\n        \"\"\"\n        Adds the given message to the list of messages that must be\n        acknowledged and dispatches control to different ``_handle_*``\n        method based on its type.\n        \"\"\"\n        self._pending_ack.add(message.msg_id)\n        handler = self._handlers.get(message.obj.CONSTRUCTOR_ID,\n                                     self._handle_update)\n        await handler(message)\n\n    def _pop_states(self, msg_id):\n        \"\"\"\n        Pops the states known to match the given ID from pending messages.\n\n        This method should be used when the response isn't specific.\n        \"\"\"\n        state = self._pending_state.pop(msg_id, None)\n        if state:\n            return [state]\n\n        to_pop = []\n        for state in self._pending_state.values():\n            if state.container_id == msg_id:\n                to_pop.append(state.msg_id)\n\n        if to_pop:\n            return [self._pending_state.pop(x) for x in to_pop]\n\n        for ack in self._last_acks:\n            if ack.msg_id == msg_id:\n                return [ack]\n\n        return []\n\n    async def _handle_rpc_result(self, message):\n        \"\"\"\n        Handles the result for Remote Procedure Calls:\n\n            rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult;\n\n        This is where the future results for sent requests are set.\n        \"\"\"\n        rpc_result = message.obj\n        state = self._pending_state.pop(rpc_result.req_msg_id, None)\n        self._log.debug('Handling RPC result for message %d',\n                        rpc_result.req_msg_id)\n\n        if not state:\n            # TODO We should not get responses to things we never sent\n            # However receiving a File() with empty bytes is \"common\".\n            # See #658, #759 and #958. They seem to happen in a container\n            # which contain the real response right after.\n            #\n            # But, it might also happen that we get an *error* for no parent request.\n            # If that's the case attempting to read from body which is None would fail with:\n            # \"BufferError: No more data left to read (need 4, got 0: b''); last read None\".\n            # This seems to be particularly common for \"RpcError(error_code=-500, error_message='No workers running')\".\n            if rpc_result.error:\n                self._log.info('Received error without parent request: %s', rpc_result.error)\n            else:\n                try:\n                    with BinaryReader(rpc_result.body) as reader:\n                        if not isinstance(reader.tgread_object(), upload.File):\n                            raise ValueError('Not an upload.File')\n                except (TypeNotFoundError, ValueError):\n                    self._log.info('Received response without parent request: %s', rpc_result.body)\n            return\n\n        if rpc_result.error:\n            error = rpc_message_to_error(rpc_result.error, state.request)\n            self._send_queue.append(\n                RequestState(MsgsAck([state.msg_id])))\n\n            if not state.future.cancelled():\n                state.future.set_exception(error)\n        else:\n            try:\n                with BinaryReader(rpc_result.body) as reader:\n                    result = state.request.read_result(reader)\n            except Exception as e:\n                # e.g. TypeNotFoundError, should be propagated to caller\n                if not state.future.cancelled():\n                    state.future.set_exception(e)\n            else:\n                self._store_own_updates(result)\n                if not state.future.cancelled():\n                    state.future.set_result(result)\n\n    async def _handle_container(self, message):\n        \"\"\"\n        Processes the inner messages of a container with many of them:\n\n            msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;\n        \"\"\"\n        self._log.debug('Handling container')\n        for inner_message in message.obj.messages:\n            await self._process_message(inner_message)\n\n    async def _handle_gzip_packed(self, message):\n        \"\"\"\n        Unpacks the data from a gzipped object and processes it:\n\n            gzip_packed#3072cfa1 packed_data:bytes = Object;\n        \"\"\"\n        self._log.debug('Handling gzipped data')\n        with BinaryReader(message.obj.data) as reader:\n            message.obj = reader.tgread_object()\n            await self._process_message(message)\n\n    async def _handle_update(self, message):\n        try:\n            assert message.obj.SUBCLASS_OF_ID == 0x8af52aac  # crc32(b'Updates')\n        except AssertionError:\n            self._log.warning(\n                'Note: %s is not an update, not dispatching it %s',\n                message.obj.__class__.__name__,\n                message.obj\n            )\n            return\n\n        self._log.debug('Handling update %s', message.obj.__class__.__name__)\n        self._updates_queue.put_nowait(message.obj)\n\n    def _store_own_updates(self, obj, *, _update_ids=frozenset((\n        _tl.UpdateShortMessage.CONSTRUCTOR_ID,\n        _tl.UpdateShortChatMessage.CONSTRUCTOR_ID,\n        _tl.UpdateShort.CONSTRUCTOR_ID,\n        _tl.UpdatesCombined.CONSTRUCTOR_ID,\n        _tl.Updates.CONSTRUCTOR_ID,\n        _tl.UpdateShortSentMessage.CONSTRUCTOR_ID,\n    )), _update_like_ids=frozenset((\n        _tl.messages.AffectedHistory.CONSTRUCTOR_ID,\n        _tl.messages.AffectedMessages.CONSTRUCTOR_ID,\n        _tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID,\n    ))):\n        try:\n            if obj.CONSTRUCTOR_ID in _update_ids:\n                obj._self_outgoing = True  # flag to only process, but not dispatch these\n                self._updates_queue.put_nowait(obj)\n            elif obj.CONSTRUCTOR_ID in _update_like_ids:\n                # Ugly \"hack\" (?) - otherwise bots reliably detect gaps when deleting messages.\n                #\n                # Note: the `date` being `None` is used to check for `updatesTooLong`, so epoch\n                # is used instead. It is still not read, because `updateShort` has no `seq`.\n                #\n                # Some requests, such as `readHistory`, also return these types. But the `pts_count`\n                # seems to be zero, so while this will produce some bogus `updateDeleteMessages`,\n                # it's still one of the \"cleaner\" approaches to handling the new `pts`.\n                # `updateDeleteMessages` is probably the \"least-invasive\" update that can be used.\n                upd = _tl.UpdateShort(\n                    _tl.UpdateDeleteMessages([], obj.pts, obj.pts_count),\n                    datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)\n                )\n                upd._self_outgoing = True\n                self._updates_queue.put_nowait(upd)\n            elif obj.CONSTRUCTOR_ID == _tl.messages.InvitedUsers.CONSTRUCTOR_ID:\n                obj.updates._self_outgoing = True\n                self._updates_queue.put_nowait(obj.updates)\n\n        except AttributeError:\n            pass\n\n    async def _handle_pong(self, message):\n        \"\"\"\n        Handles pong results, which don't come inside a ``rpc_result``\n        but are still sent through a request:\n\n            pong#347773c5 msg_id:long ping_id:long = Pong;\n        \"\"\"\n        pong = message.obj\n        self._log.debug('Handling pong for message %d', pong.msg_id)\n        if self._ping == pong.ping_id:\n            self._ping = None\n\n        state = self._pending_state.pop(pong.msg_id, None)\n        if state:\n            state.future.set_result(pong)\n\n    async def _handle_bad_server_salt(self, message):\n        \"\"\"\n        Corrects the currently used server salt to use the right value\n        before enqueuing the rejected message to be re-sent:\n\n            bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int\n            error_code:int new_server_salt:long = BadMsgNotification;\n        \"\"\"\n        bad_salt = message.obj\n        self._log.debug('Handling bad salt for message %d', bad_salt.bad_msg_id)\n        self._state.salt = bad_salt.new_server_salt\n        states = self._pop_states(bad_salt.bad_msg_id)\n        self._send_queue.extend(states)\n\n        self._log.debug('%d message(s) will be resent', len(states))\n\n    async def _handle_bad_notification(self, message):\n        \"\"\"\n        Adjusts the current state to be correct based on the\n        received bad message notification whenever possible:\n\n            bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int\n            error_code:int = BadMsgNotification;\n        \"\"\"\n        bad_msg = message.obj\n        states = self._pop_states(bad_msg.bad_msg_id)\n\n        self._log.debug('Handling bad msg %s', bad_msg)\n        if bad_msg.error_code in (16, 17):\n            # Sent msg_id too low or too high (respectively).\n            # Use the current msg_id to determine the right time offset.\n            to = self._state.update_time_offset(\n                correct_msg_id=message.msg_id)\n            self._log.info('System clock is wrong, set time offset to %ds', to)\n        elif bad_msg.error_code == 32:\n            # msg_seqno too low, so just pump it up by some \"large\" amount\n            # TODO A better fix would be to start with a new fresh session ID\n            self._state._sequence += 64\n        elif bad_msg.error_code == 33:\n            # msg_seqno too high never seems to happen but just in case\n            self._state._sequence -= 16\n        else:\n            for state in states:\n                state.future.set_exception(\n                    BadMessageError(state.request, bad_msg.error_code))\n            return\n\n        # Messages are to be re-sent once we've corrected the issue\n        self._send_queue.extend(states)\n        self._log.debug('%d messages will be resent due to bad msg',\n                        len(states))\n\n    async def _handle_detailed_info(self, message):\n        \"\"\"\n        Updates the current status with the received detailed information:\n\n            msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long\n            bytes:int status:int = MsgDetailedInfo;\n        \"\"\"\n        # TODO https://goo.gl/VvpCC6\n        msg_id = message.obj.answer_msg_id\n        self._log.debug('Handling detailed info for message %d', msg_id)\n        self._pending_ack.add(msg_id)\n\n    async def _handle_new_detailed_info(self, message):\n        \"\"\"\n        Updates the current status with the received detailed information:\n\n            msg_new_detailed_info#809db6df answer_msg_id:long\n            bytes:int status:int = MsgDetailedInfo;\n        \"\"\"\n        # TODO https://goo.gl/G7DPsR\n        msg_id = message.obj.answer_msg_id\n        self._log.debug('Handling new detailed info for message %d', msg_id)\n        self._pending_ack.add(msg_id)\n\n    async def _handle_new_session_created(self, message):\n        \"\"\"\n        Updates the current status with the received session information:\n\n            new_session_created#9ec20908 first_msg_id:long unique_id:long\n            server_salt:long = NewSession;\n        \"\"\"\n        # TODO https://goo.gl/LMyN7A\n        self._log.debug('Handling new session created')\n        self._state.salt = message.obj.server_salt\n\n    async def _handle_ack(self, message):\n        \"\"\"\n        Handles a server acknowledge about our messages. Normally\n        these can be ignored except in the case of ``auth.logOut``:\n\n            auth.logOut#5717da40 = Bool;\n\n        Telegram doesn't seem to send its result so we need to confirm\n        it manually. No other request is known to have this behaviour.\n\n        Since the ID of sent messages consisting of a container is\n        never returned (unless on a bad notification), this method\n        also removes containers messages when any of their inner\n        messages are acknowledged.\n        \"\"\"\n        ack = message.obj\n        self._log.debug('Handling acknowledge for %s', str(ack.msg_ids))\n        for msg_id in ack.msg_ids:\n            state = self._pending_state.get(msg_id)\n            if state and isinstance(state.request, LogOutRequest):\n                del self._pending_state[msg_id]\n                if not state.future.cancelled():\n                    state.future.set_result(True)\n\n    async def _handle_future_salts(self, message):\n        \"\"\"\n        Handles future salt results, which don't come inside a\n        ``rpc_result`` but are still sent through a request:\n\n            future_salts#ae500895 req_msg_id:long now:int\n            salts:vector<future_salt> = FutureSalts;\n        \"\"\"\n        # TODO save these salts and automatically adjust to the\n        # correct one whenever the salt in use expires.\n        self._log.debug('Handling future salts for message %d', message.msg_id)\n        state = self._pending_state.pop(message.msg_id, None)\n        if state:\n            state.future.set_result(message.obj)\n\n    async def _handle_state_forgotten(self, message):\n        \"\"\"\n        Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by\n        enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.\n        \"\"\"\n        self._send_queue.append(RequestState(MsgsStateInfo(\n            req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)\n        )))\n\n    async def _handle_msg_all(self, message):\n        \"\"\"\n        Handles :tl:`MsgsAllInfo` by doing nothing (yet).\n        \"\"\"\n\n    async def _handle_destroy_session(self, message):\n        \"\"\"\n        Handles both :tl:`DestroySessionOk` and :tl:`DestroySessionNone`.\n        It behaves pretty much like handling an RPC result.\n        \"\"\"\n        for msg_id, state in self._pending_state.items():\n            if isinstance(state.request, DestroySessionRequest)\\\n                    and state.request.session_id == message.obj.session_id:\n                break\n        else:\n            return\n\n        del self._pending_state[msg_id]\n        if not state.future.cancelled():\n            state.future.set_result(message.obj)\n\n    async def _handle_destroy_auth_key(self, message):\n        \"\"\"\n        Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`.\n\n        :tl:`DestroyAuthKey` is not intended for users to use, but they still\n        might, and the response won't come in `rpc_result`, so thhat's worked\n        around here.\n        \"\"\"\n        self._log.debug('Handling destroy auth key %s', message.obj)\n        for msg_id, state in list(self._pending_state.items()):\n            if isinstance(state.request, DestroyAuthKeyRequest):\n                del self._pending_state[msg_id]\n                if not state.future.cancelled():\n                    state.future.set_result(message.obj)\n\n        # If the auth key has been destroyed, that pretty much means the\n        # library can't continue as our auth key will no longer be found\n        # on the server.\n        # Even if the library didn't disconnect, the server would (and then\n        # the library would reconnect and learn about auth key being invalid).\n        if isinstance(message.obj, DestroyAuthKeyOk):\n            await self._disconnect(error=AuthKeyNotFound())\n"
  },
  {
    "path": "telethon/network/mtprotostate.py",
    "content": "import os\nimport struct\nimport time\nfrom hashlib import sha256\nfrom collections import deque\n\nfrom ..crypto import AES\nfrom ..errors import SecurityError, InvalidBufferError\nfrom ..extensions import BinaryReader\nfrom ..tl.core import TLMessage\nfrom ..tl.tlobject import TLRequest\nfrom ..tl.functions import InvokeAfterMsgRequest\nfrom ..tl.core.gzippacked import GzipPacked\nfrom ..tl.types import BadServerSalt, BadMsgNotification\n\n\n# N is not  specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable\nMAX_RECENT_MSG_IDS = 500\n\nMSG_TOO_NEW_DELTA = 30\nMSG_TOO_OLD_DELTA = 300\n\n# Something must be wrong if we ignore too many messages at the same time\nMAX_CONSECUTIVE_IGNORED = 10\n\n\nclass _OpaqueRequest(TLRequest):\n    \"\"\"\n    Wraps a serialized request into a type that can be serialized again.\n    \"\"\"\n    def __init__(self, data: bytes):\n        self.data = data\n\n    def _bytes(self):\n        return self.data\n\n\n\nclass MTProtoState:\n    \"\"\"\n    `telethon.network.mtprotosender.MTProtoSender` needs to hold a state\n    in order to be able to encrypt and decrypt incoming/outgoing messages,\n    as well as generating the message IDs. Instances of this class hold\n    together all the required information.\n\n    It doesn't make sense to use `telethon.sessions.abstract.Session` for\n    the sender because the sender should *not* be concerned about storing\n    this information to disk, as one may create as many senders as they\n    desire to any other data center, or some CDN. Using the same session\n    for all these is not a good idea as each need their own authkey, and\n    the concept of \"copying\" sessions with the unnecessary entities or\n    updates state for these connections doesn't make sense.\n\n    While it would be possible to have a `MTProtoPlainState` that does no\n    encryption so that it was usable through the `MTProtoLayer` and thus\n    avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more\n    focused to efficiency and this state is also more advanced (since it\n    supports gzipping and invoking after other message IDs). There are too\n    many methods that would be needed to make it convenient to use for the\n    authentication process, at which point the `MTProtoPlainSender` is better.\n    \"\"\"\n    def __init__(self, auth_key, loggers):\n        self.auth_key = auth_key\n        self._log = loggers[__name__]\n        self.time_offset = 0\n        self.salt = 0\n\n        self.id = self._sequence = self._last_msg_id = None\n        self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS)\n        self._highest_remote_id = 0\n        self._ignore_count = 0\n        self.reset()\n\n    def reset(self):\n        \"\"\"\n        Resets the state.\n        \"\"\"\n        # Session IDs can be random on every connection\n        self.id = struct.unpack('q', os.urandom(8))[0]\n        self._sequence = 0\n        self._last_msg_id = 0\n        self._recent_remote_ids.clear()\n        self._highest_remote_id = 0\n        self._ignore_count = 0\n\n    def update_message_id(self, message):\n        \"\"\"\n        Updates the message ID to a new one,\n        used when the time offset changed.\n        \"\"\"\n        message.msg_id = self._get_new_msg_id()\n\n    @staticmethod\n    def _calc_key(auth_key, msg_key, client):\n        \"\"\"\n        Calculate the key based on Telegram guidelines for MTProto 2,\n        specifying whether it's the client or not. See\n        https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector\n        \"\"\"\n        x = 0 if client else 8\n        sha256a = sha256(msg_key + auth_key[x: x + 36]).digest()\n        sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest()\n\n        aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32]\n        aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32]\n\n        return aes_key, aes_iv\n\n    def write_data_as_message(self, buffer, data, content_related,\n                              *, after_id=None):\n        \"\"\"\n        Writes a message containing the given data into buffer.\n\n        Returns the message id.\n        \"\"\"\n        msg_id = self._get_new_msg_id()\n        seq_no = self._get_seq_no(content_related)\n        if after_id is None:\n            body = GzipPacked.gzip_if_smaller(content_related, data)\n        else:\n            # The `RequestState` stores `bytes(request)`, not the request itself.\n            # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping.\n            body = GzipPacked.gzip_if_smaller(content_related,\n                bytes(InvokeAfterMsgRequest(after_id, _OpaqueRequest(data))))\n\n        buffer.write(struct.pack('<qii', msg_id, seq_no, len(body)))\n        buffer.write(body)\n        return msg_id\n\n    def encrypt_message_data(self, data):\n        \"\"\"\n        Encrypts the given message data using the current authorization key\n        following MTProto 2.0 guidelines core.telegram.org/mtproto/description.\n        \"\"\"\n        data = struct.pack('<qq', self.salt, self.id) + data\n        padding = os.urandom(-(len(data) + 12) % 16 + 12)\n\n        # Being substr(what, offset, length); x = 0 for client\n        # \"msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)\"\n        msg_key_large = sha256(\n            self.auth_key.key[88:88 + 32] + data + padding).digest()\n\n        # \"msg_key = substr (msg_key_large, 8, 16)\"\n        msg_key = msg_key_large[8:24]\n        aes_key, aes_iv = self._calc_key(self.auth_key.key, msg_key, True)\n\n        key_id = struct.pack('<Q', self.auth_key.key_id)\n        return (key_id + msg_key +\n                AES.encrypt_ige(data + padding, aes_key, aes_iv))\n\n    def decrypt_message_data(self, body):\n        \"\"\"\n        Inverse of `encrypt_message_data` for incoming server messages.\n        \"\"\"\n        now = time.time()  # get the time as early as possible, even if other checks make it go unused\n\n        if len(body) < 8:\n            raise InvalidBufferError(body)\n\n        # TODO Check salt, session_id and sequence_number\n        key_id = struct.unpack('<Q', body[:8])[0]\n        if key_id != self.auth_key.key_id:\n            raise SecurityError('Server replied with an invalid auth key')\n\n        msg_key = body[8:24]\n        aes_key, aes_iv = self._calc_key(self.auth_key.key, msg_key, False)\n        body = AES.decrypt_ige(body[24:], aes_key, aes_iv)\n\n        # https://core.telegram.org/mtproto/security_guidelines\n        # Sections \"checking sha256 hash\" and \"message length\"\n        our_key = sha256(self.auth_key.key[96:96 + 32] + body)\n        if msg_key != our_key.digest()[8:24]:\n            raise SecurityError(\n                \"Received msg_key doesn't match with expected one\")\n\n        reader = BinaryReader(body)\n        reader.read_long()  # remote_salt\n        if reader.read_long() != self.id:\n            raise SecurityError('Server replied with a wrong session ID (see FAQ for details)')\n\n        remote_msg_id = reader.read_long()\n\n        if remote_msg_id % 2 != 1:\n            raise SecurityError('Server sent an even msg_id')\n\n        # Only perform the (somewhat expensive) check of duplicate if we did receive a lower ID\n        if remote_msg_id <= self._highest_remote_id and remote_msg_id in self._recent_remote_ids:\n            self._log.warning('Server resent the older message %d, ignoring', remote_msg_id)\n            self._count_ignored()\n            return None\n\n        remote_sequence = reader.read_int()\n        reader.read_int()  # msg_len for the inner object, padding ignored\n\n        # We could read msg_len bytes and use those in a new reader to read\n        # the next TLObject without including the padding, but since the\n        # reader isn't used for anything else after this, it's unnecessary.\n        obj = reader.tgread_object()\n\n        # \"Certain client-to-server service messages containing data sent by the client to the\n        # server (for example, msg_id of a recent client query) may, nonetheless, be processed\n        # on the client even if the time appears to be \"incorrect\". This is especially true of\n        # messages to change server_salt and notifications about invalid time on the client.\"\n        #\n        # This means we skip the time check for certain types of messages.\n        if obj.CONSTRUCTOR_ID in (BadServerSalt.CONSTRUCTOR_ID, BadMsgNotification.CONSTRUCTOR_ID):\n            if not self._highest_remote_id and not self.time_offset:\n                # If the first message we receive is a bad notification, take this opportunity\n                # to adjust the time offset. Assume it will remain stable afterwards. Updating\n                # the offset unconditionally would make the next checks pointless.\n                self.update_time_offset(remote_msg_id)\n        else:\n            remote_msg_time = remote_msg_id >> 32\n            time_delta = (now + self.time_offset) - remote_msg_time\n\n            if time_delta > MSG_TOO_OLD_DELTA:\n                self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id)\n                self._count_ignored()\n                return None\n\n            if -time_delta > MSG_TOO_NEW_DELTA:\n                self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id)\n                self._count_ignored()\n                return None\n\n        self._recent_remote_ids.append(remote_msg_id)\n        self._highest_remote_id = remote_msg_id\n        self._ignore_count = 0\n\n        return TLMessage(remote_msg_id, remote_sequence, obj)\n\n    def _count_ignored(self):\n        # It's possible that ignoring a message \"bricks\" the connection,\n        # but this should not happen unless there's something else wrong.\n        self._ignore_count += 1\n        if self._ignore_count >= MAX_CONSECUTIVE_IGNORED:\n            raise SecurityError('Too many messages had to be ignored consecutively')\n\n    def _get_new_msg_id(self):\n        \"\"\"\n        Generates a new unique message ID based on the current\n        time (in ms) since epoch, applying a known time offset.\n        \"\"\"\n        now = time.time() + self.time_offset\n        nanoseconds = int((now - int(now)) * 1e+9)\n        new_msg_id = (int(now) << 32) | (nanoseconds << 2)\n\n        if self._last_msg_id >= new_msg_id:\n            new_msg_id = self._last_msg_id + 4\n\n        self._last_msg_id = new_msg_id\n        return new_msg_id\n\n    def update_time_offset(self, correct_msg_id):\n        \"\"\"\n        Updates the time offset to the correct\n        one given a known valid message ID.\n        \"\"\"\n        bad = self._get_new_msg_id()\n        old = self.time_offset\n\n        now = int(time.time())\n        correct = correct_msg_id >> 32\n        self.time_offset = correct - now\n\n        if self.time_offset != old:\n            self._last_msg_id = 0\n            self._log.debug(\n                'Updated time offset (old offset %d, bad %d, good %d, new %d)',\n                old, bad, correct_msg_id, self.time_offset\n            )\n\n        return self.time_offset\n\n    def _get_seq_no(self, content_related):\n        \"\"\"\n        Generates the next sequence number depending on whether\n        it should be for a content-related query or not.\n        \"\"\"\n        if content_related:\n            result = self._sequence * 2 + 1\n            self._sequence += 1\n            return result\n        else:\n            return self._sequence * 2\n"
  },
  {
    "path": "telethon/network/requeststate.py",
    "content": "import asyncio\n\n\nclass RequestState:\n    \"\"\"\n    This request state holds several information relevant to sent messages,\n    in particular the message ID assigned to the request, the container ID\n    it belongs to, the request itself, the request as bytes, and the future\n    result that will eventually be resolved.\n    \"\"\"\n    __slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after')\n\n    def __init__(self, request, after=None):\n        self.container_id = None\n        self.msg_id = None\n        self.request = request\n        self.data = bytes(request)\n        self.future = asyncio.Future()\n        self.after = after\n"
  },
  {
    "path": "telethon/password.py",
    "content": "import hashlib\nimport os\n\nfrom .crypto import factorization\nfrom .tl import types\n\n\ndef check_prime_and_good_check(prime: int, g: int):\n    good_prime_bits_count = 2048\n    if prime < 0 or prime.bit_length() != good_prime_bits_count:\n        raise ValueError('bad prime count {}, expected {}'\n                         .format(prime.bit_length(), good_prime_bits_count))\n\n    # TODO This is awfully slow\n    if factorization.Factorization.factorize(prime)[0] != 1:\n        raise ValueError('given \"prime\" is not prime')\n\n    if g == 2:\n        if prime % 8 != 7:\n            raise ValueError('bad g {}, mod8 {}'.format(g, prime % 8))\n    elif g == 3:\n        if prime % 3 != 2:\n            raise ValueError('bad g {}, mod3 {}'.format(g, prime % 3))\n    elif g == 4:\n        pass\n    elif g == 5:\n        if prime % 5 not in (1, 4):\n            raise ValueError('bad g {}, mod5 {}'.format(g, prime % 5))\n    elif g == 6:\n        if prime % 24 not in (19, 23):\n            raise ValueError('bad g {}, mod24 {}'.format(g, prime % 24))\n    elif g == 7:\n        if prime % 7 not in (3, 5, 6):\n            raise ValueError('bad g {}, mod7 {}'.format(g, prime % 7))\n    else:\n        raise ValueError('bad g {}'.format(g))\n\n    prime_sub1_div2 = (prime - 1) // 2\n    if factorization.Factorization.factorize(prime_sub1_div2)[0] != 1:\n        raise ValueError('(prime - 1) // 2 is not prime')\n\n    # Else it's good\n\n\ndef check_prime_and_good(prime_bytes: bytes, g: int):\n    good_prime = bytes((\n        0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73,\n        0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F,\n        0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB,\n        0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13, 0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37,\n        0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A, 0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB,\n        0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64,\n        0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D, 0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88,\n        0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F, 0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4,\n        0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E, 0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41,\n        0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14, 0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54,\n        0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4, 0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D,\n        0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF, 0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4,\n        0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0, 0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05,\n        0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59, 0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F,\n        0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE, 0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF,\n        0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03, 0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B))\n\n    if good_prime == prime_bytes:\n        if g in (3, 4, 5, 7):\n            return  # It's good\n\n    check_prime_and_good_check(int.from_bytes(prime_bytes, 'big'), g)\n\n\ndef is_good_large(number: int, p: int) -> bool:\n    return number > 0 and p - number > 0\n\n\nSIZE_FOR_HASH = 256\n\n\ndef num_bytes_for_hash(number: bytes) -> bytes:\n    return bytes(SIZE_FOR_HASH - len(number)) + number\n\n\ndef big_num_for_hash(g: int) -> bytes:\n    return g.to_bytes(SIZE_FOR_HASH, 'big')\n\n\ndef sha256(*p: bytes) -> bytes:\n    hash = hashlib.sha256()\n    for q in p:\n        hash.update(q)\n    return hash.digest()\n\n\ndef is_good_mod_exp_first(modexp, prime) -> bool:\n    diff = prime - modexp\n    min_diff_bits_count = 2048 - 64\n    max_mod_exp_size = 256\n    if diff < 0 or \\\n            diff.bit_length() < min_diff_bits_count or \\\n            modexp.bit_length() < min_diff_bits_count or \\\n            (modexp.bit_length() + 7) // 8 > max_mod_exp_size:\n        return False\n    return True\n\n\ndef xor(a: bytes, b: bytes) -> bytes:\n    return bytes(x ^ y for x, y in zip(a, b))\n\n\ndef pbkdf2sha512(password: bytes, salt: bytes, iterations: int):\n    return hashlib.pbkdf2_hmac('sha512', password, salt, iterations)\n\n\ndef compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow,\n                 password: str):\n    hash1 = sha256(algo.salt1, password.encode('utf-8'), algo.salt1)\n    hash2 = sha256(algo.salt2, hash1, algo.salt2)\n    hash3 = pbkdf2sha512(hash2, algo.salt1, 100000)\n    return sha256(algo.salt2, hash3, algo.salt2)\n\n\ndef compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow,\n                   password: str):\n    try:\n        check_prime_and_good(algo.p, algo.g)\n    except ValueError:\n        raise ValueError('bad p/g in password')\n\n    value = pow(algo.g,\n                int.from_bytes(compute_hash(algo, password), 'big'),\n                int.from_bytes(algo.p, 'big'))\n\n    return big_num_for_hash(value)\n\n\n# https://github.com/telegramdesktop/tdesktop/blob/18b74b90451a7db2379a9d753c9cbaf8734b4d5d/Telegram/SourceFiles/core/core_cloud_password.cpp\ndef compute_check(request: types.account.Password, password: str):\n    algo = request.current_algo\n    if not isinstance(algo, types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow):\n        raise ValueError('unsupported password algorithm {}'\n                         .format(algo.__class__.__name__))\n\n    pw_hash = compute_hash(algo, password)\n\n    p = int.from_bytes(algo.p, 'big')\n    g = algo.g\n    B = int.from_bytes(request.srp_B, 'big')\n    try:\n        check_prime_and_good(algo.p, g)\n    except ValueError:\n        raise ValueError('bad p/g in password')\n\n    if not is_good_large(B, p):\n        raise ValueError('bad b in check')\n\n    x = int.from_bytes(pw_hash, 'big')\n    p_for_hash = num_bytes_for_hash(algo.p)\n    g_for_hash = big_num_for_hash(g)\n    b_for_hash = num_bytes_for_hash(request.srp_B)\n    g_x = pow(g, x, p)\n    k = int.from_bytes(sha256(p_for_hash, g_for_hash), 'big')\n    kg_x = (k * g_x) % p\n\n    def generate_and_check_random():\n        random_size = 256\n        while True:\n            random = os.urandom(random_size)\n            a = int.from_bytes(random, 'big')\n            A = pow(g, a, p)\n            if is_good_mod_exp_first(A, p):\n                a_for_hash = big_num_for_hash(A)\n                u = int.from_bytes(sha256(a_for_hash, b_for_hash), 'big')\n                if u > 0:\n                    return (a, a_for_hash, u)\n\n    a, a_for_hash, u = generate_and_check_random()\n    g_b = (B - kg_x) % p\n    if not is_good_mod_exp_first(g_b, p):\n        raise ValueError('bad g_b')\n\n    ux = u * x\n    a_ux = a + ux\n    S = pow(g_b, a_ux, p)\n    K = sha256(big_num_for_hash(S))\n    M1 = sha256(\n        xor(sha256(p_for_hash), sha256(g_for_hash)),\n        sha256(algo.salt1),\n        sha256(algo.salt2),\n        a_for_hash,\n        b_for_hash,\n        K\n    )\n\n    return types.InputCheckPasswordSRP(\n        request.srp_id, bytes(a_for_hash), bytes(M1))\n"
  },
  {
    "path": "telethon/requestiter.py",
    "content": "import abc\nimport asyncio\nimport time\n\nfrom . import helpers\n\n\nclass RequestIter(abc.ABC):\n    \"\"\"\n    Helper class to deal with requests that need offsets to iterate.\n\n    It has some facilities, such as automatically sleeping a desired\n    amount of time between requests if needed (but not more).\n\n    Can be used synchronously if the event loop is not running and\n    as an asynchronous iterator otherwise.\n\n    `limit` is the total amount of items that the iterator should return.\n    This is handled on this base class, and will be always ``>= 0``.\n\n    `left` will be reset every time the iterator is used and will indicate\n    the amount of items that should be emitted left, so that subclasses can\n    be more efficient and fetch only as many items as they need.\n\n    Iterators may be used with ``reversed``, and their `reverse` flag will\n    be set to `True` if that's the case. Note that if this flag is set,\n    `buffer` should be filled in reverse too.\n    \"\"\"\n    def __init__(self, client, limit, *, reverse=False, wait_time=None, **kwargs):\n        self.client = client\n        self.reverse = reverse\n        self.wait_time = wait_time\n        self.kwargs = kwargs\n        self.limit = max(float('inf') if limit is None else limit, 0)\n        self.left = self.limit\n        self.buffer = None\n        self.index = 0\n        self.total = None\n        self.last_load = 0\n\n    async def _init(self, **kwargs):\n        \"\"\"\n        Called when asynchronous initialization is necessary. All keyword\n        arguments passed to `__init__` will be forwarded here, and it's\n        preferable to use named arguments in the subclasses without defaults\n        to avoid forgetting or misspelling any of them.\n\n        This method may ``raise StopAsyncIteration`` if it cannot continue.\n\n        This method may actually fill the initial buffer if it needs to,\n        and similarly to `_load_next_chunk`, ``return True`` to indicate\n        that this is the last iteration (just the initial load).\n        \"\"\"\n\n    async def __anext__(self):\n        if self.buffer is None:\n            self.buffer = []\n            if await self._init(**self.kwargs):\n                self.left = len(self.buffer)\n\n        if self.left <= 0:  # <= 0 because subclasses may change it\n            raise StopAsyncIteration\n\n        if self.index == len(self.buffer):\n            # asyncio will handle times <= 0 to sleep 0 seconds\n            if self.wait_time:\n                await asyncio.sleep(\n                    self.wait_time - (time.time() - self.last_load)\n                )\n                self.last_load = time.time()\n\n            self.index = 0\n            self.buffer = []\n            if await self._load_next_chunk():\n                self.left = len(self.buffer)\n\n        if not self.buffer:\n            raise StopAsyncIteration\n\n        result = self.buffer[self.index]\n        self.left -= 1\n        self.index += 1\n        return result\n\n    def __next__(self):\n        try:\n            return self.client.loop.run_until_complete(self.__anext__())\n        except StopAsyncIteration:\n            raise StopIteration\n\n    def __aiter__(self):\n        self.buffer = None\n        self.index = 0\n        self.last_load = 0\n        self.left = self.limit\n        return self\n\n    def __iter__(self):\n        if self.client.loop.is_running():\n            raise RuntimeError(\n                'You must use \"async for\" if the event loop '\n                'is running (i.e. you are inside an \"async def\")'\n            )\n\n        return self.__aiter__()\n\n    async def collect(self):\n        \"\"\"\n        Create a `self` iterator and collect it into a `TotalList`\n        (a normal list with a `.total` attribute).\n        \"\"\"\n        result = helpers.TotalList()\n        async for message in self:\n            result.append(message)\n\n        result.total = self.total\n        return result\n\n    @abc.abstractmethod\n    async def _load_next_chunk(self):\n        \"\"\"\n        Called when the next chunk is necessary.\n\n        It should extend the `buffer` with new items.\n\n        It should return `True` if it's the last chunk,\n        after which moment the method won't be called again\n        during the same iteration.\n        \"\"\"\n        raise NotImplementedError\n\n    def __reversed__(self):\n        self.reverse = not self.reverse\n        return self  # __aiter__ will be called after, too\n"
  },
  {
    "path": "telethon/sessions/__init__.py",
    "content": "from .abstract import Session\nfrom .memory import MemorySession\nfrom .sqlite import SQLiteSession\nfrom .string import StringSession\n"
  },
  {
    "path": "telethon/sessions/abstract.py",
    "content": "from abc import ABC, abstractmethod\n\n\nclass Session(ABC):\n    def __init__(self):\n        pass\n\n    def clone(self, to_instance=None):\n        \"\"\"\n        Creates a clone of this session file.\n        \"\"\"\n        return to_instance or self.__class__()\n\n    @abstractmethod\n    def set_dc(self, dc_id, server_address, port):\n        \"\"\"\n        Sets the information of the data center address and port that\n        the library should connect to, as well as the data center ID,\n        which is currently unused.\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    @abstractmethod\n    def dc_id(self):\n        \"\"\"\n        Returns the currently-used data center ID.\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    @abstractmethod\n    def server_address(self):\n        \"\"\"\n        Returns the server address where the library should connect to.\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    @abstractmethod\n    def port(self):\n        \"\"\"\n        Returns the port to which the library should connect to.\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    @abstractmethod\n    def auth_key(self):\n        \"\"\"\n        Returns an ``AuthKey`` instance associated with the saved\n        data center, or `None` if a new one should be generated.\n        \"\"\"\n        raise NotImplementedError\n\n    @auth_key.setter\n    @abstractmethod\n    def auth_key(self, value):\n        \"\"\"\n        Sets the ``AuthKey`` to be used for the saved data center.\n        \"\"\"\n        raise NotImplementedError\n\n    @property\n    @abstractmethod\n    def takeout_id(self):\n        \"\"\"\n        Returns an ID of the takeout process initialized for this session,\n        or `None` if there's no were any unfinished takeout requests.\n        \"\"\"\n        raise NotImplementedError\n\n    @takeout_id.setter\n    @abstractmethod\n    def takeout_id(self, value):\n        \"\"\"\n        Sets the ID of the unfinished takeout process for this session.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_update_state(self, entity_id):\n        \"\"\"\n        Returns the ``UpdateState`` associated with the given `entity_id`.\n        If the `entity_id` is 0, it should return the ``UpdateState`` for\n        no specific channel (the \"general\" state). If no state is known\n        it should ``return None``.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def set_update_state(self, entity_id, state):\n        \"\"\"\n        Sets the given ``UpdateState`` for the specified `entity_id`, which\n        should be 0 if the ``UpdateState`` is the \"general\" state (and not\n        for any specific channel).\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_update_states(self):\n        \"\"\"\n        Returns an iterable over all known pairs of ``(entity ID, update state)``.\n        \"\"\"\n\n    def close(self):\n        \"\"\"\n        Called on client disconnection. Should be used to\n        free any used resources. Can be left empty if none.\n        \"\"\"\n\n    @abstractmethod\n    def save(self):\n        \"\"\"\n        Called whenever important properties change. It should\n        make persist the relevant session information to disk.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def delete(self):\n        \"\"\"\n        Called upon client.log_out(). Should delete the stored\n        information from disk since it's not valid anymore.\n        \"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def list_sessions(cls):\n        \"\"\"\n        Lists available sessions. Not used by the library itself.\n        \"\"\"\n        return []\n\n    @abstractmethod\n    def process_entities(self, tlo):\n        \"\"\"\n        Processes the input ``TLObject`` or ``list`` and saves\n        whatever information is relevant (e.g., ID or access hash).\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_input_entity(self, key):\n        \"\"\"\n        Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``).\n        The library uses this method whenever an ``InputPeer`` is needed\n        to suit several purposes (e.g. user only provided its ID or wishes\n        to use a cached username to avoid extra RPC).\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def cache_file(self, md5_digest, file_size, instance):\n        \"\"\"\n        Caches the given file information persistently, so that it\n        doesn't need to be re-uploaded in case the file is used again.\n\n        The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``,\n        both with an ``.id`` and ``.access_hash`` attributes.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractmethod\n    def get_file(self, md5_digest, file_size, cls):\n        \"\"\"\n        Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size``\n        match an existing saved record. The class will either be an\n        ``InputPhoto`` or ``InputDocument``, both with two parameters\n        ``id`` and ``access_hash`` in that order.\n        \"\"\"\n        raise NotImplementedError\n"
  },
  {
    "path": "telethon/sessions/memory.py",
    "content": "from enum import Enum\n\nfrom .abstract import Session\nfrom .. import utils\nfrom ..tl import TLObject\nfrom ..tl.types import (\n    PeerUser, PeerChat, PeerChannel,\n    InputPeerUser, InputPeerChat, InputPeerChannel,\n    InputPhoto, InputDocument\n)\n\n\nclass _SentFileType(Enum):\n    DOCUMENT = 0\n    PHOTO = 1\n\n    @staticmethod\n    def from_type(cls):\n        if cls == InputDocument:\n            return _SentFileType.DOCUMENT\n        elif cls == InputPhoto:\n            return _SentFileType.PHOTO\n        else:\n            raise ValueError('The cls must be either InputDocument/InputPhoto')\n\n\nclass MemorySession(Session):\n    def __init__(self):\n        super().__init__()\n\n        self._dc_id = 0\n        self._server_address = None\n        self._port = None\n        self._auth_key = None\n        self._tmp_auth_key = None\n        self._takeout_id = None\n\n        self._files = {}\n        self._entities = set()\n        self._update_states = {}\n\n    def set_dc(self, dc_id, server_address, port):\n        self._dc_id = dc_id or 0\n        self._server_address = server_address\n        self._port = port\n\n    @property\n    def dc_id(self):\n        return self._dc_id\n\n    @property\n    def server_address(self):\n        return self._server_address\n\n    @property\n    def port(self):\n        return self._port\n\n    @property\n    def auth_key(self):\n        return self._auth_key\n\n    @property\n    def tmp_auth_key(self):\n        return self._tmp_auth_key\n\n    @auth_key.setter\n    def auth_key(self, value):\n        self._auth_key = value\n\n    @auth_key.setter\n    def tmp_auth_key(self, value):\n        self._tmp_auth_key = value\n\n    @property\n    def takeout_id(self):\n        return self._takeout_id\n\n    @takeout_id.setter\n    def takeout_id(self, value):\n        self._takeout_id = value\n\n    def get_update_state(self, entity_id):\n        return self._update_states.get(entity_id, None)\n\n    def set_update_state(self, entity_id, state):\n        self._update_states[entity_id] = state\n\n    def get_update_states(self):\n        return self._update_states.items()\n\n    def close(self):\n        pass\n\n    def save(self):\n        pass\n\n    def delete(self):\n        pass\n\n    @staticmethod\n    def _entity_values_to_row(id, hash, username, phone, name):\n        # While this is a simple implementation it might be overrode by,\n        # other classes so they don't need to implement the plural form\n        # of the method. Don't remove.\n        return id, hash, username, phone, name\n\n    def _entity_to_row(self, e):\n        if not isinstance(e, TLObject):\n            return\n        try:\n            p = utils.get_input_peer(e, allow_self=False)\n            marked_id = utils.get_peer_id(p)\n        except TypeError:\n            # Note: `get_input_peer` already checks for non-zero `access_hash`.\n            #        See issues #354 and #392. It also checks that the entity\n            #        is not `min`, because its `access_hash` cannot be used\n            #        anywhere (since layer 102, there are two access hashes).\n            return\n\n        if isinstance(p, (InputPeerUser, InputPeerChannel)):\n            p_hash = p.access_hash\n        elif isinstance(p, InputPeerChat):\n            p_hash = 0\n        else:\n            return\n\n        username = getattr(e, 'username', None) or None\n        if username is not None:\n            username = username.lower()\n        phone = getattr(e, 'phone', None)\n        name = utils.get_display_name(e) or None\n        return self._entity_values_to_row(\n            marked_id, p_hash, username, phone, name\n        )\n\n    def _entities_to_rows(self, tlo):\n        if not isinstance(tlo, TLObject) and utils.is_list_like(tlo):\n            # This may be a list of users already for instance\n            entities = tlo\n        else:\n            entities = []\n            if hasattr(tlo, 'user'):\n                entities.append(tlo.user)\n            if hasattr(tlo, 'chat'):\n                entities.append(tlo.chat)\n            if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats):\n                entities.extend(tlo.chats)\n            if hasattr(tlo, 'users') and utils.is_list_like(tlo.users):\n                entities.extend(tlo.users)\n\n        rows = []  # Rows to add (id, hash, username, phone, name)\n        for e in entities:\n            row = self._entity_to_row(e)\n            if row:\n                rows.append(row)\n        return rows\n\n    def process_entities(self, tlo):\n        self._entities |= set(self._entities_to_rows(tlo))\n\n    def get_entity_rows_by_phone(self, phone):\n        try:\n            return next((id, hash) for id, hash, _, found_phone, _\n                        in self._entities if found_phone == phone)\n        except StopIteration:\n            pass\n\n    def get_entity_rows_by_username(self, username):\n        try:\n            return next((id, hash) for id, hash, found_username, _, _\n                        in self._entities if found_username == username)\n        except StopIteration:\n            pass\n\n    def get_entity_rows_by_name(self, name):\n        try:\n            return next((id, hash) for id, hash, _, _, found_name\n                        in self._entities if found_name == name)\n        except StopIteration:\n            pass\n\n    def get_entity_rows_by_id(self, id, exact=True):\n        try:\n            if exact:\n                return next((found_id, hash) for found_id, hash, _, _, _\n                            in self._entities if found_id == id)\n            else:\n                ids = (\n                    utils.get_peer_id(PeerUser(id)),\n                    utils.get_peer_id(PeerChat(id)),\n                    utils.get_peer_id(PeerChannel(id))\n                )\n                return next((found_id, hash) for found_id, hash, _, _, _\n                            in self._entities if found_id in ids)\n        except StopIteration:\n            pass\n\n    def get_input_entity(self, key):\n        try:\n            if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd):\n                # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel'))\n                # We already have an Input version, so nothing else required\n                return key\n            # Try to early return if this key can be casted as input peer\n            return utils.get_input_peer(key)\n        except (AttributeError, TypeError):\n            # Not a TLObject or can't be cast into InputPeer\n            if isinstance(key, TLObject):\n                key = utils.get_peer_id(key)\n                exact = True\n            else:\n                exact = not isinstance(key, int) or key < 0\n\n        result = None\n        if isinstance(key, str):\n            phone = utils.parse_phone(key)\n            if phone:\n                result = self.get_entity_rows_by_phone(phone)\n            else:\n                username, invite = utils.parse_username(key)\n                if username and not invite:\n                    result = self.get_entity_rows_by_username(username)\n                else:\n                    tup = utils.resolve_invite_link(key)[1]\n                    if tup:\n                        result = self.get_entity_rows_by_id(tup, exact=False)\n\n        elif isinstance(key, int):\n            result = self.get_entity_rows_by_id(key, exact)\n\n        if not result and isinstance(key, str):\n            result = self.get_entity_rows_by_name(key)\n\n        if result:\n            entity_id, entity_hash = result  # unpack resulting tuple\n            entity_id, kind = utils.resolve_id(entity_id)\n            # removes the mark and returns type of entity\n            if kind == PeerUser:\n                return InputPeerUser(entity_id, entity_hash)\n            elif kind == PeerChat:\n                return InputPeerChat(entity_id)\n            elif kind == PeerChannel:\n                return InputPeerChannel(entity_id, entity_hash)\n        else:\n            raise ValueError('Could not find input entity with key ', key)\n\n    def cache_file(self, md5_digest, file_size, instance):\n        if not isinstance(instance, (InputDocument, InputPhoto)):\n            raise TypeError('Cannot cache %s instance' % type(instance))\n        key = (md5_digest, file_size, _SentFileType.from_type(type(instance)))\n        value = (instance.id, instance.access_hash)\n        self._files[key] = value\n\n    def get_file(self, md5_digest, file_size, cls):\n        key = (md5_digest, file_size, _SentFileType.from_type(cls))\n        try:\n            return cls(*self._files[key])\n        except KeyError:\n            return None\n"
  },
  {
    "path": "telethon/sessions/sqlite.py",
    "content": "import datetime\nimport os\nimport time\n\nfrom ..tl import types\nfrom .memory import MemorySession, _SentFileType\nfrom .. import utils\nfrom ..crypto import AuthKey\nfrom ..tl.types import (\n    InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel\n)\n\ntry:\n    import sqlite3\n    sqlite3_err = None\nexcept ImportError as e:\n    sqlite3 = None\n    sqlite3_err = type(e)\n\nEXTENSION = '.session'\nCURRENT_VERSION = 8  # database version\n\n\nclass SQLiteSession(MemorySession):\n    \"\"\"This session contains the required information to login into your\n       Telegram account. NEVER give the saved session file to anyone, since\n       they would gain instant access to all your messages and contacts.\n\n       If you think the session has been compromised, close all the sessions\n       through an official Telegram client to revoke the authorization.\n    \"\"\"\n\n    def __init__(self, session_id=None, store_tmp_auth_key_on_disk:bool=False):\n        if sqlite3 is None:\n            raise sqlite3_err\n\n        super().__init__()\n        self.filename = ':memory:'\n        self.save_entities = True\n        self.store_tmp_auth_key_on_disk = store_tmp_auth_key_on_disk\n\n        if session_id:\n            self.filename = session_id\n            if not self.filename.endswith(EXTENSION):\n                self.filename += EXTENSION\n\n        self._conn = None\n        c = self._cursor()\n        c.execute(\"select name from sqlite_master \"\n                  \"where type='table' and name='version'\")\n        if c.fetchone():\n            # Tables already exist, check for the version\n            c.execute(\"select version from version\")\n            version = c.fetchone()[0]\n            if version < CURRENT_VERSION:\n                self._upgrade_database(old=version)\n                c.execute(\"delete from version\")\n                c.execute(\"insert into version values (?)\", (CURRENT_VERSION,))\n                self.save()\n\n            # These values will be saved\n            c.execute('select * from sessions')\n            tuple_ = c.fetchone()\n            if tuple_:\n                self._dc_id, self._server_address, self._port, key, tmp_key, \\\n                    self._takeout_id = tuple_\n                self._auth_key = AuthKey(data=key)\n                self._tmp_auth_key = AuthKey(data=tmp_key)\n\n            c.close()\n        else:\n            # Tables don't exist, create new ones\n            self._create_table(\n                c,\n                \"version (version integer primary key)\"\n                ,\n                \"\"\"sessions (\n                    dc_id integer primary key,\n                    server_address text,\n                    port integer,\n                    auth_key blob,\n                    takeout_id integer,\n                    tmp_auth_key blob\n                )\"\"\"\n                ,\n                \"\"\"entities (\n                    id integer primary key,\n                    hash integer not null,\n                    username text,\n                    phone integer,\n                    name text,\n                    date integer\n                )\"\"\"\n                ,\n                \"\"\"sent_files (\n                    md5_digest blob,\n                    file_size integer,\n                    type integer,\n                    id integer,\n                    hash integer,\n                    primary key(md5_digest, file_size, type)\n                )\"\"\"\n                ,\n                \"\"\"update_state (\n                    id integer primary key,\n                    pts integer,\n                    qts integer,\n                    date integer,\n                    seq integer\n                )\"\"\"\n            )\n            c.execute(\"insert into version values (?)\", (CURRENT_VERSION,))\n            self._update_session_table()\n            c.close()\n            self.save()\n\n    def clone(self, to_instance=None):\n        cloned = super().clone(to_instance)\n        cloned.save_entities = self.save_entities\n        return cloned\n\n    def _upgrade_database(self, old):\n        c = self._cursor()\n        if old == 1:\n            old += 1\n            # old == 1 doesn't have the old sent_files so no need to drop\n        if old == 2:\n            old += 1\n            # Old cache from old sent_files lasts then a day anyway, drop\n            c.execute('drop table sent_files')\n            self._create_table(c, \"\"\"sent_files (\n                md5_digest blob,\n                file_size integer,\n                type integer,\n                id integer,\n                hash integer,\n                primary key(md5_digest, file_size, type)\n            )\"\"\")\n        if old == 3:\n            old += 1\n            self._create_table(c, \"\"\"update_state (\n                id integer primary key,\n                pts integer,\n                qts integer,\n                date integer,\n                seq integer\n            )\"\"\")\n        if old == 4:\n            old += 1\n            c.execute(\"alter table sessions add column takeout_id integer\")\n        if old == 5:\n            # Not really any schema upgrade, but potentially all access\n            # hashes for User and Channel are wrong, so drop them off.\n            old += 1\n            c.execute('delete from entities')\n        if old == 6:\n            old += 1\n            c.execute(\"alter table entities add column date integer\")\n        if old == 7:\n            old += 1\n            c.execute(\"alter table sessions add column tmp_auth_key blob\")\n\n        c.close()\n\n    @staticmethod\n    def _create_table(c, *definitions):\n        for definition in definitions:\n            c.execute('create table {}'.format(definition))\n\n    # Data from sessions should be kept as properties\n    # not to fetch the database every time we need it\n    def set_dc(self, dc_id, server_address, port):\n        super().set_dc(dc_id, server_address, port)\n        self._update_session_table()\n\n        # Fetch the auth_key corresponding to this data center\n        row = self._execute('select auth_key, tmp_auth_key from sessions')\n        if row and row[0]:\n            self._auth_key = AuthKey(data=row[0])\n        else:\n            self._auth_key = None\n\n        if row and row[1]:\n            self._tmp_auth_key = AuthKey(data=row[1])\n        else:\n            self._tmp_auth_key = None\n\n    @MemorySession.auth_key.setter\n    def auth_key(self, value):\n        self._auth_key = value\n        self._update_session_table()\n\n    @MemorySession.tmp_auth_key.setter\n    def tmp_auth_key(self, value):\n        self._tmp_auth_key = value\n        self._update_session_table()\n\n    @MemorySession.takeout_id.setter\n    def takeout_id(self, value):\n        self._takeout_id = value\n        self._update_session_table()\n\n    def _update_session_table(self):\n        c = self._cursor()\n        # While we can save multiple rows into the sessions table\n        # currently we only want to keep ONE as the tables don't\n        # tell us which auth_key's are usable and will work. Needs\n        # some more work before being able to save auth_key's for\n        # multiple DCs. Probably done differently.\n        c.execute('delete from sessions')\n        c.execute('insert or replace into sessions values (?,?,?,?,?,?)', (\n            self._dc_id,\n            self._server_address,\n            self._port,\n            self._auth_key.key if self._auth_key else b'',\n            self._takeout_id,\n            self._tmp_auth_key.key if (self.store_tmp_auth_key_on_disk and self._tmp_auth_key) else b''\n        ))\n        c.close()\n\n    def get_update_state(self, entity_id):\n        row = self._execute('select pts, qts, date, seq from update_state '\n                            'where id = ?', entity_id)\n        if row:\n            pts, qts, date, seq = row\n            date = datetime.datetime.fromtimestamp(\n                date, tz=datetime.timezone.utc)\n            return types.updates.State(pts, qts, date, seq, unread_count=0)\n\n    def set_update_state(self, entity_id, state):\n        self._execute('insert or replace into update_state values (?,?,?,?,?)',\n                      entity_id, state.pts, state.qts,\n                      state.date.timestamp(), state.seq)\n\n    def get_update_states(self):\n        c = self._cursor()\n        try:\n            rows = c.execute('select id, pts, qts, date, seq from update_state').fetchall()\n            return ((row[0], types.updates.State(\n                pts=row[1],\n                qts=row[2],\n                date=datetime.datetime.fromtimestamp(row[3], tz=datetime.timezone.utc),\n                seq=row[4],\n                unread_count=0)\n            ) for row in rows)\n        finally:\n            c.close()\n\n    def save(self):\n        \"\"\"Saves the current session object as session_user_id.session\"\"\"\n        # This is a no-op if there are no changes to commit, so there's\n        # no need for us to keep track of an \"unsaved changes\" variable.\n        if self._conn is not None:\n            self._conn.commit()\n\n    def _cursor(self):\n        \"\"\"Asserts that the connection is open and returns a cursor\"\"\"\n        if self._conn is None:\n            self._conn = sqlite3.connect(self.filename,\n                                         check_same_thread=False)\n        return self._conn.cursor()\n\n    def _execute(self, stmt, *values):\n        \"\"\"\n        Gets a cursor, executes `stmt` and closes the cursor,\n        fetching one row afterwards and returning its result.\n        \"\"\"\n        c = self._cursor()\n        try:\n            return c.execute(stmt, values).fetchone()\n        finally:\n            c.close()\n\n    def close(self):\n        \"\"\"Closes the connection unless we're working in-memory\"\"\"\n        if self.filename != ':memory:':\n            if self._conn is not None:\n                self._conn.commit()\n                self._conn.close()\n                self._conn = None\n\n    def delete(self):\n        \"\"\"Deletes the current session file\"\"\"\n        if self.filename == ':memory:':\n            return True\n        try:\n            os.remove(self.filename)\n            return True\n        except OSError:\n            return False\n\n    @classmethod\n    def list_sessions(cls):\n        \"\"\"Lists all the sessions of the users who have ever connected\n           using this client and never logged out\n        \"\"\"\n        return [os.path.splitext(os.path.basename(f))[0]\n                for f in os.listdir('.') if f.endswith(EXTENSION)]\n\n    # Entity processing\n\n    def process_entities(self, tlo):\n        \"\"\"\n        Processes all the found entities on the given TLObject,\n        unless .save_entities is False.\n        \"\"\"\n        if not self.save_entities:\n            return\n\n        rows = self._entities_to_rows(tlo)\n        if not rows:\n            return\n\n        c = self._cursor()\n        try:\n            now_tup = (int(time.time()),)\n            rows = [row + now_tup for row in rows]\n            c.executemany(\n                'insert or replace into entities values (?,?,?,?,?,?)', rows)\n        finally:\n            c.close()\n\n    def get_entity_rows_by_phone(self, phone):\n        return self._execute(\n            'select id, hash from entities where phone = ?', phone)\n\n    def get_entity_rows_by_username(self, username):\n        c = self._cursor()\n        try:\n            results = c.execute(\n                'select id, hash, date from entities where username = ?',\n                (username,)\n            ).fetchall()\n\n            if not results:\n                return None\n\n            # If there is more than one result for the same username, evict the oldest one\n            if len(results) > 1:\n                results.sort(key=lambda t: t[2] or 0)\n                c.executemany('update entities set username = null where id = ?',\n                              [(t[0],) for t in results[:-1]])\n\n            return results[-1][0], results[-1][1]\n        finally:\n            c.close()\n\n    def get_entity_rows_by_name(self, name):\n        return self._execute(\n            'select id, hash from entities where name = ?', name)\n\n    def get_entity_rows_by_id(self, id, exact=True):\n        if exact:\n            return self._execute(\n                'select id, hash from entities where id = ?', id)\n        else:\n            return self._execute(\n                'select id, hash from entities where id in (?,?,?)',\n                utils.get_peer_id(PeerUser(id)),\n                utils.get_peer_id(PeerChat(id)),\n                utils.get_peer_id(PeerChannel(id))\n            )\n\n    # File processing\n\n    def get_file(self, md5_digest, file_size, cls):\n        row = self._execute(\n            'select id, hash from sent_files '\n            'where md5_digest = ? and file_size = ? and type = ?',\n            md5_digest, file_size, _SentFileType.from_type(cls).value\n        )\n        if row:\n            # Both allowed classes have (id, access_hash) as parameters\n            return cls(row[0], row[1])\n\n    def cache_file(self, md5_digest, file_size, instance):\n        if not isinstance(instance, (InputDocument, InputPhoto)):\n            raise TypeError('Cannot cache %s instance' % type(instance))\n\n        self._execute(\n            'insert or replace into sent_files values (?,?,?,?,?)',\n            md5_digest, file_size,\n            _SentFileType.from_type(type(instance)).value,\n            instance.id, instance.access_hash\n        )\n"
  },
  {
    "path": "telethon/sessions/string.py",
    "content": "import base64\nimport ipaddress\nimport struct\n\nfrom .abstract import Session\nfrom .memory import MemorySession\nfrom ..crypto import AuthKey\n\n_STRUCT_PREFORMAT = '>B{}sH256s'\n\nCURRENT_VERSION = '1'\n\n\nclass StringSession(MemorySession):\n    \"\"\"\n    This session file can be easily saved and loaded as a string. According\n    to the initial design, it contains only the data that is necessary for\n    successful connection and authentication, so takeout ID is not stored.\n\n    It is thought to be used where you don't want to create any on-disk\n    files but would still like to be able to save and load existing sessions\n    by other means.\n\n    You can use custom `encode` and `decode` functions, if present:\n\n    * `encode` definition must be ``def encode(value: bytes) -> str:``.\n    * `decode` definition must be ``def decode(value: str) -> bytes:``.\n    \"\"\"\n    def __init__(self, string: str = None):\n        super().__init__()\n        if string:\n            if string[0] != CURRENT_VERSION:\n                raise ValueError('Not a valid string')\n\n            string = string[1:]\n            ip_len = 4 if len(string) == 352 else 16\n            self._dc_id, ip, self._port, key = struct.unpack(\n                _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string))\n\n            self._server_address = ipaddress.ip_address(ip).compressed\n            if any(key):\n                self._auth_key = AuthKey(key)\n\n    @staticmethod\n    def encode(x: bytes) -> str:\n        return base64.urlsafe_b64encode(x).decode('ascii')\n\n    @staticmethod\n    def decode(x: str) -> bytes:\n        return base64.urlsafe_b64decode(x)\n\n    def save(self: Session):\n        if not self.auth_key:\n            return ''\n\n        ip = ipaddress.ip_address(self.server_address).packed\n        return CURRENT_VERSION + StringSession.encode(struct.pack(\n            _STRUCT_PREFORMAT.format(len(ip)),\n            self.dc_id,\n            ip,\n            self.port,\n            self.auth_key.key\n        ))\n"
  },
  {
    "path": "telethon/sync.py",
    "content": "\"\"\"\nThis magical module will rewrite all public methods in the public interface\nof the library so they can run the loop on their own if it's not already\nrunning. This rewrite may not be desirable if the end user always uses the\nmethods they way they should be ran, but it's incredibly useful for quick\nscripts and the runtime overhead is relatively low.\n\nSome really common methods which are hardly used offer this ability by\ndefault, such as ``.start()`` and ``.run_until_disconnected()`` (since\nyou may want to start, and then run until disconnected while using async\nevent handlers).\n\"\"\"\nimport asyncio\nimport functools\nimport inspect\n\nfrom . import events, errors, utils, connection, helpers\nfrom .client.account import _TakeoutClient\nfrom .client.telegramclient import TelegramClient\nfrom .tl import types, functions, custom\nfrom .tl.custom import (\n    Draft, Dialog, MessageButton, Forward, Button,\n    Message, InlineResult, Conversation\n)\nfrom .tl.custom.chatgetter import ChatGetter\nfrom .tl.custom.sendergetter import SenderGetter\n\n\ndef _syncify_wrap(t, method_name):\n    method = getattr(t, method_name)\n\n    @functools.wraps(method)\n    def syncified(*args, **kwargs):\n        coro = method(*args, **kwargs)\n        loop = helpers.get_running_loop()\n        if loop.is_running():\n            return coro\n        else:\n            return loop.run_until_complete(coro)\n\n    # Save an accessible reference to the original method\n    setattr(syncified, '__tl.sync', method)\n    setattr(t, method_name, syncified)\n\n\ndef syncify(*types):\n    \"\"\"\n    Converts all the methods in the given types (class definitions)\n    into synchronous, which return either the coroutine or the result\n    based on whether ``asyncio's`` event loop is running.\n    \"\"\"\n    # Our asynchronous generators all are `RequestIter`, which already\n    # provide a synchronous iterator variant, so we don't need to worry\n    # about asyncgenfunction's here.\n    for t in types:\n        for name in dir(t):\n            if not name.startswith('_') or name == '__call__':\n                if inspect.iscoroutinefunction(getattr(t, name)):\n                    _syncify_wrap(t, name)\n\n\nsyncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton,\n        ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation)\n\n\n# Private special case, since a conversation's methods return\n# futures (but the public function themselves are synchronous).\n_syncify_wrap(Conversation, '_get_result')\n\n__all__ = [\n    'TelegramClient', 'Button',\n    'types', 'functions', 'custom', 'errors',\n    'events', 'utils', 'connection'\n]\n"
  },
  {
    "path": "telethon/tl/__init__.py",
    "content": "from .tlobject import TLObject, TLRequest\n"
  },
  {
    "path": "telethon/tl/core/__init__.py",
    "content": "\"\"\"\nThis module holds core \"special\" types, which are more convenient ways\nto do stuff in a `telethon.network.mtprotosender.MTProtoSender` instance.\n\nOnly special cases are gzip-packed data, the response message (not a\nclient message), the message container which references these messages\nand would otherwise conflict with the rest, and finally the RpcResult:\n\n    rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult;\n\nThree things to note with this definition:\n1. The constructor ID is actually ``42d36c2c``.\n2. Those bytes are not read like the rest of bytes (length + payload).\n   They are actually the raw bytes of another object, which can't be\n   read directly because it depends on per-request information (since\n   some can return ``Vector<int>`` and ``Vector<long>``).\n3. Those bytes may be gzipped data, which needs to be treated early.\n\"\"\"\nfrom .tlmessage import TLMessage\nfrom .gzippacked import GzipPacked\nfrom .messagecontainer import MessageContainer\nfrom .rpcresult import RpcResult\n\ncore_objects = {x.CONSTRUCTOR_ID: x for x in (\n    GzipPacked, MessageContainer, RpcResult\n)}\n"
  },
  {
    "path": "telethon/tl/core/gzippacked.py",
    "content": "try:\n    from isal import igzip as gzip\nexcept ImportError:\n    import gzip\nimport struct\n\nfrom .. import TLObject\n\n\nclass GzipPacked(TLObject):\n    CONSTRUCTOR_ID = 0x3072cfa1\n\n    def __init__(self, data):\n        self.data = data\n\n    @staticmethod\n    def gzip_if_smaller(content_related, data):\n        \"\"\"Calls bytes(request), and based on a certain threshold,\n           optionally gzips the resulting data. If the gzipped data is\n           smaller than the original byte array, this is returned instead.\n\n           Note that this only applies to content related requests.\n        \"\"\"\n        if content_related and len(data) > 512:\n            gzipped = bytes(GzipPacked(data))\n            return gzipped if len(gzipped) < len(data) else data\n        else:\n            return data\n\n    def __bytes__(self):\n        return struct.pack('<I', GzipPacked.CONSTRUCTOR_ID) + \\\n               TLObject.serialize_bytes(gzip.compress(self.data))\n\n    @staticmethod\n    def read(reader):\n        constructor = reader.read_int(signed=False)\n        assert constructor == GzipPacked.CONSTRUCTOR_ID\n        return gzip.decompress(reader.tgread_bytes())\n\n    @classmethod\n    def from_reader(cls, reader):\n        return GzipPacked(gzip.decompress(reader.tgread_bytes()))\n\n    def to_dict(self):\n        return {\n            '_': 'GzipPacked',\n            'data': self.data\n        }\n"
  },
  {
    "path": "telethon/tl/core/messagecontainer.py",
    "content": "from .tlmessage import TLMessage\nfrom ..tlobject import TLObject\n\n\nclass MessageContainer(TLObject):\n    CONSTRUCTOR_ID = 0x73f1f8dc\n\n    # Maximum size in bytes for the inner payload of the container.\n    # Telegram will close the connection if the payload is bigger.\n    # The overhead of the container itself is subtracted.\n    MAXIMUM_SIZE = 1044456 - 8\n\n    # Maximum amount of messages that can't be sent inside a single\n    # container, inclusive. Beyond this limit Telegram will respond\n    # with BAD_MESSAGE 64 (invalid container).\n    #\n    # This limit is not 100% accurate and may in some cases be higher.\n    # However, sending up to 100 requests at once in a single container\n    # is a reasonable conservative value, since it could also depend on\n    # other factors like size per request, but we cannot know this.\n    MAXIMUM_LENGTH = 100\n\n    def __init__(self, messages):\n        self.messages = messages\n\n    def to_dict(self):\n        return {\n            '_': 'MessageContainer',\n            'messages':\n                [] if self.messages is None else [\n                    None if x is None else x.to_dict() for x in self.messages\n                ],\n        }\n\n    @classmethod\n    def from_reader(cls, reader):\n        # This assumes that .read_* calls are done in the order they appear\n        messages = []\n        for _ in range(reader.read_int()):\n            msg_id = reader.read_long()\n            seq_no = reader.read_int()\n            length = reader.read_int()\n            before = reader.tell_position()\n            obj = reader.tgread_object()  # May over-read e.g. RpcResult\n            reader.set_position(before + length)\n            messages.append(TLMessage(msg_id, seq_no, obj))\n        return MessageContainer(messages)\n"
  },
  {
    "path": "telethon/tl/core/rpcresult.py",
    "content": "from .gzippacked import GzipPacked\nfrom .. import TLObject\nfrom ..types import RpcError\n\n\nclass RpcResult(TLObject):\n    CONSTRUCTOR_ID = 0xf35c6d01\n\n    def __init__(self, req_msg_id, body, error):\n        self.req_msg_id = req_msg_id\n        self.body = body\n        self.error = error\n\n    @classmethod\n    def from_reader(cls, reader):\n        msg_id = reader.read_long()\n        inner_code = reader.read_int(signed=False)\n        if inner_code == RpcError.CONSTRUCTOR_ID:\n            return RpcResult(msg_id, None, RpcError.from_reader(reader))\n        if inner_code == GzipPacked.CONSTRUCTOR_ID:\n            return RpcResult(msg_id, GzipPacked.from_reader(reader).data, None)\n\n        reader.seek(-4)\n        # This reader.read() will read more than necessary, but it's okay.\n        # We could make use of MessageContainer's length here, but since\n        # it's not necessary we don't need to care about it.\n        return RpcResult(msg_id, reader.read(), None)\n\n    def to_dict(self):\n        return {\n            '_': 'RpcResult',\n            'req_msg_id': self.req_msg_id,\n            'body': self.body,\n            'error': self.error\n        }\n"
  },
  {
    "path": "telethon/tl/core/tlmessage.py",
    "content": "from .. import TLObject\n\n\nclass TLMessage(TLObject):\n    \"\"\"\n    https://core.telegram.org/mtproto/service_messages#simple-container.\n\n    Messages are what's ultimately sent to Telegram:\n        message msg_id:long seqno:int bytes:int body:bytes = Message;\n\n    Each message has its own unique identifier, and the body is simply\n    the serialized request that should be executed on the server, or\n    the response object from Telegram. Since the body is always a valid\n    object, it makes sense to store the object and not the bytes to\n    ease working with them.\n\n    There is no need to add serializing logic here since that can be\n    inlined and is unlikely to change. Thus these are only needed to\n    encapsulate responses.\n    \"\"\"\n    SIZE_OVERHEAD = 12\n\n    def __init__(self, msg_id, seq_no, obj):\n        self.msg_id = msg_id\n        self.seq_no = seq_no\n        self.obj = obj\n\n    def to_dict(self):\n        return {\n            '_': 'TLMessage',\n            'msg_id': self.msg_id,\n            'seq_no': self.seq_no,\n            'obj': self.obj\n        }\n"
  },
  {
    "path": "telethon/tl/custom/__init__.py",
    "content": "from .adminlogevent import AdminLogEvent\nfrom .draft import Draft\nfrom .dialog import Dialog\nfrom .inputsizedfile import InputSizedFile\nfrom .messagebutton import MessageButton\nfrom .forward import Forward\nfrom .message import Message\nfrom .button import Button\nfrom .inlinebuilder import InlineBuilder\nfrom .inlineresult import InlineResult\nfrom .inlineresults import InlineResults\nfrom .conversation import Conversation\nfrom .qrlogin import QRLogin\nfrom .participantpermissions import ParticipantPermissions\n"
  },
  {
    "path": "telethon/tl/custom/adminlogevent.py",
    "content": "from ...tl import types\nfrom ...utils import get_input_peer\n\n\nclass AdminLogEvent:\n    \"\"\"\n    Represents a more friendly interface for admin log events.\n\n    Members:\n        original (:tl:`ChannelAdminLogEvent`):\n            The original :tl:`ChannelAdminLogEvent`.\n\n        entities (`dict`):\n            A dictionary mapping user IDs to :tl:`User`.\n\n            When `old` and `new` are :tl:`ChannelParticipant`, you can\n            use this dictionary to map the ``user_id``, ``kicked_by``,\n            ``inviter_id`` and ``promoted_by`` IDs to their :tl:`User`.\n\n        user (:tl:`User`):\n            The user that caused this action (``entities[original.user_id]``).\n\n        input_user (:tl:`InputPeerUser`):\n            Input variant of `user`.\n    \"\"\"\n    def __init__(self, original, entities):\n        self.original = original\n        self.entities = entities\n        self.user = entities[original.user_id]\n        self.input_user = get_input_peer(self.user)\n\n    @property\n    def id(self):\n        \"\"\"\n        The ID of this event.\n        \"\"\"\n        return self.original.id\n\n    @property\n    def date(self):\n        \"\"\"\n        The date when this event occurred.\n        \"\"\"\n        return self.original.date\n\n    @property\n    def user_id(self):\n        \"\"\"\n        The ID of the user that triggered this event.\n        \"\"\"\n        return self.original.user_id\n\n    @property\n    def action(self):\n        \"\"\"\n        The original :tl:`ChannelAdminLogEventAction`.\n        \"\"\"\n        return self.original.action\n\n    @property\n    def old(self):\n        \"\"\"\n        The old value from the event.\n        \"\"\"\n        ori = self.original.action\n        if isinstance(ori, (\n                types.ChannelAdminLogEventActionChangeAbout,\n                types.ChannelAdminLogEventActionChangeTitle,\n                types.ChannelAdminLogEventActionChangeUsername,\n                types.ChannelAdminLogEventActionChangeLocation,\n                types.ChannelAdminLogEventActionChangeHistoryTTL,\n        )):\n            return ori.prev_value\n        elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto):\n            return ori.prev_photo\n        elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet):\n            return ori.prev_stickerset\n        elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage):\n            return ori.prev_message\n        elif isinstance(ori, (\n                types.ChannelAdminLogEventActionParticipantToggleAdmin,\n                types.ChannelAdminLogEventActionParticipantToggleBan\n        )):\n            return ori.prev_participant\n        elif isinstance(ori, (\n                types.ChannelAdminLogEventActionToggleInvites,\n                types.ChannelAdminLogEventActionTogglePreHistoryHidden,\n                types.ChannelAdminLogEventActionToggleSignatures\n        )):\n            return not ori.new_value\n        elif isinstance(ori, types.ChannelAdminLogEventActionDeleteMessage):\n            return ori.message\n        elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights):\n            return ori.prev_banned_rights\n        elif isinstance(ori, types.ChannelAdminLogEventActionDiscardGroupCall):\n            return ori.call\n        elif isinstance(ori, (\n            types.ChannelAdminLogEventActionExportedInviteDelete,\n            types.ChannelAdminLogEventActionExportedInviteRevoke,\n            types.ChannelAdminLogEventActionParticipantJoinByInvite,\n        )):\n            return ori.invite\n        elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit):\n            return ori.prev_invite\n\n    @property\n    def new(self):\n        \"\"\"\n        The new value present in the event.\n        \"\"\"\n        ori = self.original.action\n        if isinstance(ori, (\n                types.ChannelAdminLogEventActionChangeAbout,\n                types.ChannelAdminLogEventActionChangeTitle,\n                types.ChannelAdminLogEventActionChangeUsername,\n                types.ChannelAdminLogEventActionToggleInvites,\n                types.ChannelAdminLogEventActionTogglePreHistoryHidden,\n                types.ChannelAdminLogEventActionToggleSignatures,\n                types.ChannelAdminLogEventActionChangeLocation,\n                types.ChannelAdminLogEventActionChangeHistoryTTL,\n        )):\n            return ori.new_value\n        elif isinstance(ori, types.ChannelAdminLogEventActionChangePhoto):\n            return ori.new_photo\n        elif isinstance(ori, types.ChannelAdminLogEventActionChangeStickerSet):\n            return ori.new_stickerset\n        elif isinstance(ori, types.ChannelAdminLogEventActionEditMessage):\n            return ori.new_message\n        elif isinstance(ori, (\n                types.ChannelAdminLogEventActionParticipantToggleAdmin,\n                types.ChannelAdminLogEventActionParticipantToggleBan\n        )):\n            return ori.new_participant\n        elif isinstance(ori, (\n            types.ChannelAdminLogEventActionParticipantInvite,\n            types.ChannelAdminLogEventActionParticipantVolume,\n        )):\n            return ori.participant\n        elif isinstance(ori, types.ChannelAdminLogEventActionDefaultBannedRights):\n            return ori.new_banned_rights\n        elif isinstance(ori, types.ChannelAdminLogEventActionStopPoll):\n            return ori.message\n        elif isinstance(ori, types.ChannelAdminLogEventActionStartGroupCall):\n            return ori.call\n        elif isinstance(ori, (\n                types.ChannelAdminLogEventActionParticipantMute,\n                types.ChannelAdminLogEventActionParticipantUnmute,\n        )):\n            return ori.participant\n        elif isinstance(ori, types.ChannelAdminLogEventActionToggleGroupCallSetting):\n            return ori.join_muted\n        elif isinstance(ori, types.ChannelAdminLogEventActionExportedInviteEdit):\n            return ori.new_invite\n\n    @property\n    def changed_about(self):\n        \"\"\"\n        Whether the channel's about was changed or not.\n\n        If `True`, `old` and `new` will be present as `str`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeAbout)\n\n    @property\n    def changed_title(self):\n        \"\"\"\n        Whether the channel's title was changed or not.\n\n        If `True`, `old` and `new` will be present as `str`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeTitle)\n\n    @property\n    def changed_username(self):\n        \"\"\"\n        Whether the channel's username was changed or not.\n\n        If `True`, `old` and `new` will be present as `str`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeUsername)\n\n    @property\n    def changed_photo(self):\n        \"\"\"\n        Whether the channel's photo was changed or not.\n\n        If `True`, `old` and `new` will be present as :tl:`Photo`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangePhoto)\n\n    @property\n    def changed_sticker_set(self):\n        \"\"\"\n        Whether the channel's sticker set was changed or not.\n\n        If `True`, `old` and `new` will be present as :tl:`InputStickerSet`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeStickerSet)\n\n    @property\n    def changed_message(self):\n        \"\"\"\n        Whether a message in this channel was edited or not.\n\n        If `True`, `old` and `new` will be present as\n        `Message <telethon.tl.custom.message.Message>`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionEditMessage)\n\n    @property\n    def deleted_message(self):\n        \"\"\"\n        Whether a message in this channel was deleted or not.\n\n        If `True`, `old` will be present as\n        `Message <telethon.tl.custom.message.Message>`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionDeleteMessage)\n\n    @property\n    def changed_admin(self):\n        \"\"\"\n        Whether the permissions for an admin in this channel\n        changed or not.\n\n        If `True`, `old` and `new` will be present as\n        :tl:`ChannelParticipant`.\n        \"\"\"\n        return isinstance(\n            self.original.action,\n            types.ChannelAdminLogEventActionParticipantToggleAdmin)\n\n    @property\n    def changed_restrictions(self):\n        \"\"\"\n        Whether a message in this channel was edited or not.\n\n        If `True`, `old` and `new` will be present as\n        :tl:`ChannelParticipant`.\n        \"\"\"\n        return isinstance(\n            self.original.action,\n            types.ChannelAdminLogEventActionParticipantToggleBan)\n\n    @property\n    def changed_invites(self):\n        \"\"\"\n        Whether the invites in the channel were toggled or not.\n\n        If `True`, `old` and `new` will be present as `bool`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionToggleInvites)\n\n    @property\n    def changed_location(self):\n        \"\"\"\n        Whether the location setting of the channel has changed or not.\n\n        If `True`, `old` and `new` will be present as :tl:`ChannelLocation`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeLocation)\n\n    @property\n    def joined(self):\n        \"\"\"\n        Whether `user` joined through the channel's\n        public username or not.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantJoin)\n\n    @property\n    def joined_invite(self):\n        \"\"\"\n        Whether a new user joined through an invite\n        link to the channel or not.\n\n        If `True`, `new` will be present as\n        :tl:`ChannelParticipant`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantInvite)\n\n    @property\n    def left(self):\n        \"\"\"\n        Whether `user` left the channel or not.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantLeave)\n\n    @property\n    def changed_hide_history(self):\n        \"\"\"\n        Whether hiding the previous message history for new members\n        in the channel was toggled or not.\n\n        If `True`, `old` and `new` will be present as `bool`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionTogglePreHistoryHidden)\n\n    @property\n    def changed_signatures(self):\n        \"\"\"\n        Whether the message signatures in the channel were toggled\n        or not.\n\n        If `True`, `old` and `new` will be present as `bool`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionToggleSignatures)\n\n    @property\n    def changed_pin(self):\n        \"\"\"\n        Whether a new message in this channel was pinned or not.\n\n        If `True`, `new` will be present as\n        `Message <telethon.tl.custom.message.Message>`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionUpdatePinned)\n\n    @property\n    def changed_default_banned_rights(self):\n        \"\"\"\n        Whether the default banned rights were changed or not.\n\n        If `True`, `old` and `new` will\n        be present as :tl:`ChatBannedRights`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionDefaultBannedRights)\n\n    @property\n    def stopped_poll(self):\n        \"\"\"\n        Whether a poll was stopped or not.\n\n        If `True`, `new` will be present as\n        `Message <telethon.tl.custom.message.Message>`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionStopPoll)\n\n    @property\n    def started_group_call(self):\n        \"\"\"\n        Whether a group call was started or not.\n\n        If `True`, `new` will be present as :tl:`InputGroupCall`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionStartGroupCall)\n\n    @property\n    def discarded_group_call(self):\n        \"\"\"\n        Whether a group call was started or not.\n\n        If `True`, `old` will be present as :tl:`InputGroupCall`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionDiscardGroupCall)\n\n    @property\n    def user_muted(self):\n        \"\"\"\n        Whether a participant was muted in the ongoing group call or not.\n\n        If `True`, `new` will be present as :tl:`GroupCallParticipant`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantMute)\n\n    @property\n    def user_unmutted(self):\n        \"\"\"\n        Whether a participant was unmuted from the ongoing group call or not.\n\n        If `True`, `new` will be present as :tl:`GroupCallParticipant`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantUnmute)\n\n    @property\n    def changed_call_settings(self):\n        \"\"\"\n        Whether the group call settings were changed or not.\n\n        If `True`, `new` will be `True` if new users are muted on join.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionToggleGroupCallSetting)\n\n    @property\n    def changed_history_ttl(self):\n        \"\"\"\n        Whether the Time To Live of the message history has changed.\n\n        Messages sent after this change will have a ``ttl_period`` in seconds\n        indicating how long they should live for before being auto-deleted.\n\n        If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionChangeHistoryTTL)\n\n    @property\n    def deleted_exported_invite(self):\n        \"\"\"\n        Whether the exported chat invite has been deleted.\n\n        If `True`, `old` will be the deleted :tl:`ExportedChatInvite`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionExportedInviteDelete)\n\n    @property\n    def edited_exported_invite(self):\n        \"\"\"\n        Whether the exported chat invite has been edited.\n\n        If `True`, `old` and `new` will be the old and new\n        :tl:`ExportedChatInvite`, respectively.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionExportedInviteEdit)\n\n    @property\n    def revoked_exported_invite(self):\n        \"\"\"\n        Whether the exported chat invite has been revoked.\n\n        If `True`, `old` will be the revoked :tl:`ExportedChatInvite`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionExportedInviteRevoke)\n\n    @property\n    def joined_by_invite(self):\n        \"\"\"\n        Whether a new participant has joined with the use of an invite link.\n\n        If `True`, `old` will be pre-existing (old) :tl:`ExportedChatInvite`\n        used to join.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantJoinByInvite)\n\n    @property\n    def changed_user_volume(self):\n        \"\"\"\n        Whether a participant's volume in a call has been changed.\n\n        If `True`, `new` will be the updated :tl:`GroupCallParticipant`.\n        \"\"\"\n        return isinstance(self.original.action,\n                          types.ChannelAdminLogEventActionParticipantVolume)\n\n    def __str__(self):\n        return str(self.original)\n\n    def stringify(self):\n        return self.original.stringify()\n"
  },
  {
    "path": "telethon/tl/custom/button.py",
    "content": "from .. import types\nfrom ... import utils\n\n\nclass Button:\n    \"\"\"\n    .. note::\n\n        This class is used to **define** reply markups, e.g. when\n        sending a message or replying to events. When you access\n        `Message.buttons <telethon.tl.custom.message.Message.buttons>`\n        they are actually `MessageButton\n        <telethon.tl.custom.messagebutton.MessageButton>`,\n        so you might want to refer to that class instead.\n\n    Helper class to allow defining ``reply_markup`` when\n    sending a message with inline or keyboard buttons.\n\n    You should make use of the defined class methods to create button\n    instances instead making them yourself (i.e. don't do ``Button(...)``\n    but instead use methods line `Button.inline(...) <inline>` etc.\n\n    You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game`\n    together to create inline buttons (under the message).\n\n    You can use `text`, `request_location`, `request_phone` and `request_poll`\n    together to create a reply markup (replaces the user keyboard).\n    You can also configure the aspect of the reply with these.\n    The latest message with a reply markup will be the one shown to the user\n    (messages contain the buttons, not the chat itself).\n\n    You **cannot** mix the two type of buttons together,\n    and it will error if you try to do so.\n\n    The text for all buttons may be at most 142 characters.\n    If more characters are given, Telegram will cut the text\n    to 128 characters and add the ellipsis (…) character as\n    the 129.\n    \"\"\"\n    def __init__(self, button, *, resize, single_use, selective,\n                 persistent, placeholder):\n        self.button = button\n        self.resize = resize\n        self.single_use = single_use\n        self.selective = selective\n        self.persistent = persistent\n        self.placeholder = placeholder\n\n    @staticmethod\n    def _is_inline(button):\n        \"\"\"\n        Returns `True` if the button belongs to an inline keyboard.\n        \"\"\"\n        return isinstance(button, (\n            types.KeyboardButtonCopy,\n            types.KeyboardButtonBuy,\n            types.KeyboardButtonCallback,\n            types.KeyboardButtonGame,\n            types.KeyboardButtonSwitchInline,\n            types.KeyboardButtonUrl,\n            types.InputKeyboardButtonUrlAuth,\n            types.KeyboardButtonWebView,\n        ))\n\n    @staticmethod\n    def inline(text, data=None):\n        \"\"\"\n        Creates a new inline button with some payload data in it.\n\n        If `data` is omitted, the given `text` will be used as `data`.\n        In any case `data` should be either `bytes` or `str`.\n\n        Note that the given `data` must be less or equal to 64 bytes.\n        If more than 64 bytes are passed as data, ``ValueError`` is raised.\n        If you need to store more than 64 bytes, consider saving the real\n        data in a database and a reference to that data inside the button.\n\n        When the user clicks this button, `events.CallbackQuery\n        <telethon.events.callbackquery.CallbackQuery>` will trigger with the\n        same data that the button contained, so that you can determine which\n        button was pressed.\n        \"\"\"\n        if not data:\n            data = text.encode('utf-8')\n        elif not isinstance(data, (bytes, bytearray, memoryview)):\n            data = str(data).encode('utf-8')\n\n        if len(data) > 64:\n            raise ValueError('Too many bytes for the data')\n\n        return types.KeyboardButtonCallback(text, data)\n\n    @staticmethod\n    def switch_inline(text, query='', same_peer=False):\n        \"\"\"\n        Creates a new inline button to switch to inline query.\n\n        If `query` is given, it will be the default text to be used\n        when making the inline query.\n\n        If ``same_peer is True`` the inline query will directly be\n        set under the currently opened chat. Otherwise, the user will\n        have to select a different dialog to make the query.\n\n        When the user clicks this button, after a chat is selected, their\n        input field will be filled with the username of your bot followed\n        by the query text, ready to make inline queries.\n        \"\"\"\n        return types.KeyboardButtonSwitchInline(text, query, same_peer)\n\n    @staticmethod\n    def url(text, url=None):\n        \"\"\"\n        Creates a new inline button to open the desired URL on click.\n\n        If no `url` is given, the `text` will be used as said URL instead.\n\n        You cannot detect that the user clicked this button directly.\n\n        When the user clicks this button, a confirmation box will be shown\n        to the user asking whether they want to open the displayed URL unless\n        the domain is trusted, and once confirmed the URL will open in their\n        device.\n        \"\"\"\n        return types.KeyboardButtonUrl(text, url or text)\n\n    @staticmethod\n    def auth(text, url=None, *, bot=None, write_access=False, fwd_text=None):\n        \"\"\"\n        Creates a new inline button to authorize the user at the given URL.\n\n        You should set the `url` to be on the same domain as the one configured\n        for the desired `bot` via `@BotFather <https://t.me/BotFather>`_ using\n        the ``/setdomain`` command.\n\n        For more information about letting the user login via Telegram to\n        a certain domain, see https://core.telegram.org/widgets/login.\n\n        If no `url` is specified, it will default to `text`.\n\n        Args:\n            bot (`hints.EntityLike`):\n                The bot that requires this authorization. By default, this\n                is the bot that is currently logged in (itself), although\n                you may pass a different input peer.\n\n                .. note::\n\n                    For now, you cannot use ID or username for this argument.\n                    If you want to use a different bot than the one currently\n                    logged in, you must manually use `client.get_input_entity()\n                    <telethon.client.users.UserMethods.get_input_entity>`.\n\n            write_access (`bool`):\n                Whether write access is required or not.\n                This is `False` by default (read-only access).\n\n            fwd_text (`str`):\n                The new text to show in the button if the message is\n                forwarded. By default, the button text will be the same.\n\n        When the user clicks this button, a confirmation box will be shown\n        to the user asking whether they want to login to the specified domain.\n        \"\"\"\n        return types.InputKeyboardButtonUrlAuth(\n            text=text,\n            url=url or text,\n            bot=utils.get_input_user(bot or types.InputUserSelf()),\n            request_write_access=write_access,\n            fwd_text=fwd_text\n        )\n\n    @classmethod\n    def text(cls, text, *, resize=None, single_use=None, selective=None,\n             persistent=None, placeholder=None):\n        \"\"\"\n        Creates a new keyboard button with the given text.\n\n        Args:\n            text (`str`):\n                The title of the button.\n\n            resize (`bool`):\n                If present, the entire keyboard will be reconfigured to\n                be resized and be smaller if there are not many buttons.\n\n            single_use (`bool`):\n                If present, the entire keyboard will be reconfigured to\n                be usable only once before it hides itself.\n\n            selective (`bool`):\n                If present, the entire keyboard will be reconfigured to\n                be \"selective\". The keyboard will be shown only to specific\n                users. It will target users that are @mentioned in the text\n                of the message or to the sender of the message you reply to.\n\n            persistent (`bool`):\n                If present, always show the keyboard when the regular keyboard\n                is hidden. Defaults to false, in which case the custom keyboard\n                can be hidden and revealed via the keyboard icon.\n\n            placeholder (`str`):\n                The placeholder to be shown in the input field when the keyboard is active;\n                1-64 characters\n\n        When the user clicks this button, a text message with the same text\n        as the button will be sent, and can be handled with `events.NewMessage\n        <telethon.events.newmessage.NewMessage>`. You cannot distinguish\n        between a button press and the user typing and sending exactly the\n        same text on their own.\n        \"\"\"\n        return cls(\n            types.KeyboardButton(text),\n            resize=resize,\n            single_use=single_use,\n            selective=selective,\n            persistent=persistent,\n            placeholder=placeholder\n        )\n\n    @classmethod\n    def request_location(cls, text, *, resize=None, single_use=None, selective=None,\n                         persistent=None, placeholder=None):\n        \"\"\"\n        Creates a new keyboard button to request the user's location on click.\n\n        ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``\n         are documented in `text`.\n\n        When the user clicks this button, a confirmation box will be shown\n        to the user asking whether they want to share their location with the\n        bot, and if confirmed a message with geo media will be sent.\n        \"\"\"\n        return cls(\n            types.KeyboardButtonRequestGeoLocation(text),\n            resize=resize,\n            single_use=single_use,\n            selective=selective,\n            persistent=persistent,\n            placeholder=placeholder\n        )\n\n    @classmethod\n    def request_phone(cls, text, *, resize=None, single_use=None,\n                      selective=None, persistent=None, placeholder=None):\n        \"\"\"\n        Creates a new keyboard button to request the user's phone on click.\n\n        ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``\n         are documented in `text`.\n\n        When the user clicks this button, a confirmation box will be shown\n        to the user asking whether they want to share their phone with the\n        bot, and if confirmed a message with contact media will be sent.\n        \"\"\"\n        return cls(\n            types.KeyboardButtonRequestPhone(text),\n            resize=resize,\n            single_use=single_use,\n            selective=selective,\n            placeholder=placeholder,\n            persistent=persistent\n        )\n\n    @classmethod\n    def request_poll(cls, text, *, force_quiz=False, resize=None, single_use=None,\n                     selective=None, persistent=None, placeholder=None):\n        \"\"\"\n        Creates a new keyboard button to request the user to create a poll.\n\n        If `force_quiz` is `False`, the user will be allowed to choose whether\n        they want their poll to be a quiz or not. Otherwise, the user will be\n        forced to create a quiz when creating the poll.\n\n        If a poll is a quiz, there will be only one answer that is valid, and\n        the votes cannot be retracted. Otherwise, users can vote and retract\n        the vote, and the pol might be multiple choice.\n\n        ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``\n         are documented in `text`.\n\n        When the user clicks this button, a screen letting the user create a\n        poll will be shown, and if they do create one, the poll will be sent.\n        \"\"\"\n        return cls(\n            types.KeyboardButtonRequestPoll(text, quiz=force_quiz),\n            resize=resize,\n            single_use=single_use,\n            selective=selective,\n            persistent=persistent,\n            placeholder=placeholder\n        )\n\n    @staticmethod\n    def clear(selective=None):\n        \"\"\"\n        Clears all keyboard buttons after sending a message with this markup.\n        When used, no other button should be present or it will be ignored.\n\n       ``selective`` is as documented in `text`.\n\n        \"\"\"\n        return types.ReplyKeyboardHide(selective=selective)\n\n    @staticmethod\n    def force_reply(single_use=None, selective=None, placeholder=None):\n        \"\"\"\n        Forces a reply to the message with this markup. If used,\n        no other button should be present or it will be ignored.\n\n        ``single_use``, ``selective`` and ``placeholder`` are as documented in `text`.\n\n        \"\"\"\n        return types.ReplyKeyboardForceReply(\n            single_use=single_use,\n            selective=selective,\n            placeholder=placeholder)\n\n    @staticmethod\n    def buy(text):\n        \"\"\"\n        Creates a new inline button to buy a product.\n\n        This can only be used when sending files of type\n        :tl:`InputMediaInvoice`, and must be the first button.\n\n        If the button is not specified, Telegram will automatically\n        add the button to the message. See the\n        `Payments API <https://core.telegram.org/api/payments>`__\n        documentation for more information.\n        \"\"\"\n        return types.KeyboardButtonBuy(text)\n\n    @staticmethod\n    def game(text):\n        \"\"\"\n        Creates a new inline button to start playing a game.\n\n        This should be used when sending files of type\n        :tl:`InputMediaGame`, and must be the first button.\n\n        See the\n        `Games <https://core.telegram.org/api/bots/games>`__\n        documentation for more information on using games.\n        \"\"\"\n        return types.KeyboardButtonGame(text)\n"
  },
  {
    "path": "telethon/tl/custom/chatgetter.py",
    "content": "import abc\n\nfrom ... import errors, utils\nfrom ...tl import types\n\n\nclass ChatGetter(abc.ABC):\n    \"\"\"\n    Helper base class that introduces the `chat`, `input_chat`\n    and `chat_id` properties and `get_chat` and `get_input_chat`\n    methods.\n    \"\"\"\n    def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None):\n        self._chat_peer = chat_peer\n        self._input_chat = input_chat\n        self._chat = chat\n        self._broadcast = broadcast\n        self._client = None\n\n    @property\n    def chat(self):\n        \"\"\"\n        Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object\n        belongs to. It may be `None` if Telegram didn't send the chat.\n\n        If you only need the ID, use `chat_id` instead.\n\n        If you need to call a method which needs\n        this chat, use `input_chat` instead.\n\n        If you're using `telethon.events`, use `get_chat()` instead.\n        \"\"\"\n        return self._chat\n\n    async def get_chat(self):\n        \"\"\"\n        Returns `chat`, but will make an API call to find the\n        chat unless it's already cached.\n\n        If you only need the ID, use `chat_id` instead.\n\n        If you need to call a method which needs\n        this chat, use `get_input_chat()` instead.\n        \"\"\"\n        # See `get_sender` for information about 'min'.\n        if (self._chat is None or getattr(self._chat, 'min', None))\\\n                and await self.get_input_chat():\n            try:\n                self._chat =\\\n                    await self._client.get_entity(self._input_chat)\n            except ValueError:\n                await self._refetch_chat()\n        return self._chat\n\n    @property\n    def input_chat(self):\n        \"\"\"\n        This :tl:`InputPeer` is the input version of the chat where the\n        message was sent. Similarly to `input_sender\n        <telethon.tl.custom.sendergetter.SenderGetter.input_sender>`, this\n        doesn't have things like username or similar, but still useful in\n        some cases.\n\n        Note that this might not be available if the library doesn't\n        have enough information available.\n        \"\"\"\n        if self._input_chat is None and self._chat_peer and self._client:\n            try:\n                self._input_chat = self._client._mb_entity_cache.get(\n                        utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer()\n            except AttributeError:\n                pass\n\n        return self._input_chat\n\n    async def get_input_chat(self):\n        \"\"\"\n        Returns `input_chat`, but will make an API call to find the\n        input chat unless it's already cached.\n        \"\"\"\n        if self.input_chat is None and self.chat_id and self._client:\n            try:\n                # The chat may be recent, look in dialogs\n                target = self.chat_id\n                async for d in self._client.iter_dialogs(100):\n                    if d.id == target:\n                        self._chat = d.entity\n                        self._input_chat = d.input_entity\n                        break\n            except errors.RPCError:\n                pass\n\n        return self._input_chat\n\n    @property\n    def chat_id(self):\n        \"\"\"\n        Returns the marked chat integer ID. Note that this value **will\n        be different** from ``peer_id`` for incoming private messages, since\n        the chat *to* which the messages go is to your own person, but\n        the *chat* itself is with the one who sent the message.\n\n        TL;DR; this gets the ID that you expect.\n\n        If there is a chat in the object, `chat_id` will *always* be set,\n        which is why you should use it instead of `chat.id <chat>`.\n        \"\"\"\n        return utils.get_peer_id(self._chat_peer) if self._chat_peer else None\n\n    @property\n    def is_private(self):\n        \"\"\"\n        `True` if the message was sent as a private message.\n\n        Returns `None` if there isn't enough information\n        (e.g. on `events.MessageDeleted <telethon.events.messagedeleted.MessageDeleted>`).\n        \"\"\"\n        return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None\n\n    @property\n    def is_group(self):\n        \"\"\"\n        True if the message was sent on a group or megagroup.\n\n        Returns `None` if there isn't enough information\n        (e.g. on `events.MessageDeleted <telethon.events.messagedeleted.MessageDeleted>`).\n        \"\"\"\n        # TODO Cache could tell us more in the future\n        if self._broadcast is None and hasattr(self.chat, 'broadcast'):\n            self._broadcast = bool(self.chat.broadcast)\n\n        if isinstance(self._chat_peer, types.PeerChannel):\n            if self._broadcast is None:\n                return None\n            else:\n                return not self._broadcast\n\n        return isinstance(self._chat_peer, types.PeerChat)\n\n    @property\n    def is_channel(self):\n        \"\"\"`True` if the message was sent on a megagroup or channel.\"\"\"\n        # The only case where chat peer could be none is in MessageDeleted,\n        # however those always have the peer in channels.\n        return isinstance(self._chat_peer, types.PeerChannel)\n\n    async def _refetch_chat(self):\n        \"\"\"\n        Re-fetches chat information through other means.\n        \"\"\"\n"
  },
  {
    "path": "telethon/tl/custom/conversation.py",
    "content": "import asyncio\nimport functools\nimport inspect\nimport itertools\nimport time\n\nfrom .chatgetter import ChatGetter\nfrom ... import helpers, utils, errors\n\n# Sometimes the edits arrive very fast (within the same second).\n# In that case we add a small delta so that the age is older, for\n# comparision purposes. This value is enough for up to 1000 messages.\n_EDIT_COLLISION_DELTA = 0.001\n\n\ndef _checks_cancelled(f):\n    @functools.wraps(f)\n    def wrapper(self, *args, **kwargs):\n        if self._cancelled:\n            raise asyncio.CancelledError('The conversation was cancelled before')\n\n        return f(self, *args, **kwargs)\n    return wrapper\n\n\nclass Conversation(ChatGetter):\n    \"\"\"\n    Represents a conversation inside an specific chat.\n\n    A conversation keeps track of new messages since it was\n    created until its exit and easily lets you query the\n    current state.\n\n    If you need a conversation across two or more chats,\n    you should use two conversations and synchronize them\n    as you better see fit.\n    \"\"\"\n    _id_counter = 0\n    _custom_counter = 0\n\n    def __init__(self, client, input_chat,\n                 *, timeout, total_timeout, max_messages,\n                 exclusive, replies_are_responses):\n        # This call resets the client\n        ChatGetter.__init__(self, input_chat=input_chat)\n\n        self._id = Conversation._id_counter\n        Conversation._id_counter += 1\n\n        self._client = client\n        self._timeout = timeout\n        self._total_timeout = total_timeout\n        self._total_due = None\n\n        self._outgoing = set()\n        self._last_outgoing = 0\n        self._incoming = []\n        self._last_incoming = 0\n        self._max_incoming = max_messages\n        self._last_read = None\n        self._custom = {}\n\n        self._pending_responses = {}\n        self._pending_replies = {}\n        self._pending_edits = {}\n        self._pending_reads = {}\n\n        self._exclusive = exclusive\n        self._cancelled = False\n\n        # The user is able to expect two responses for the same message.\n        # {desired message ID: next incoming index}\n        self._response_indices = {}\n        if replies_are_responses:\n            self._reply_indices = self._response_indices\n        else:\n            self._reply_indices = {}\n\n        self._edit_dates = {}\n\n    @_checks_cancelled\n    async def send_message(self, *args, **kwargs):\n        \"\"\"\n        Sends a message in the context of this conversation. Shorthand\n        for `telethon.client.messages.MessageMethods.send_message` with\n        ``entity`` already set.\n        \"\"\"\n        sent = await self._client.send_message(\n            self._input_chat, *args, **kwargs)\n\n        # Albums will be lists, so handle that\n        ms = sent if isinstance(sent, list) else (sent,)\n        self._outgoing.update(m.id for m in ms)\n        self._last_outgoing = ms[-1].id\n        return sent\n\n    @_checks_cancelled\n    async def send_file(self, *args, **kwargs):\n        \"\"\"\n        Sends a file in the context of this conversation. Shorthand\n        for `telethon.client.uploads.UploadMethods.send_file` with\n        ``entity`` already set.\n        \"\"\"\n        sent = await self._client.send_file(\n            self._input_chat, *args, **kwargs)\n\n        # Albums will be lists, so handle that\n        ms = sent if isinstance(sent, list) else (sent,)\n        self._outgoing.update(m.id for m in ms)\n        self._last_outgoing = ms[-1].id\n        return sent\n\n    @_checks_cancelled\n    def mark_read(self, message=None):\n        \"\"\"\n        Marks as read the latest received message if ``message is None``.\n        Otherwise, marks as read until the given message (or message ID).\n\n        This is equivalent to calling `client.send_read_acknowledge\n        <telethon.client.messages.MessageMethods.send_read_acknowledge>`.\n        \"\"\"\n        if message is None:\n            if self._incoming:\n                message = self._incoming[-1].id\n            else:\n                message = 0\n        elif not isinstance(message, int):\n            message = message.id\n\n        return self._client.send_read_acknowledge(\n            self._input_chat, max_id=message)\n\n    def get_response(self, message=None, *, timeout=None):\n        \"\"\"\n        Gets the next message that responds to a previous one. This is\n        the method you need most of the time, along with `get_edit`.\n\n        Args:\n            message (`Message <telethon.tl.custom.message.Message>` | `int`, optional):\n                The message (or the message ID) for which a response\n                is expected. By default this is the last sent message.\n\n            timeout (`int` | `float`, optional):\n                If present, this `timeout` (in seconds) will override the\n                per-action timeout defined for the conversation.\n\n        .. code-block:: python\n\n            async with client.conversation(...) as conv:\n                await conv.send_message('Hey, what is your name?')\n\n                response = await conv.get_response()\n                name = response.text\n\n                await conv.send_message('Nice to meet you, {}!'.format(name))\n        \"\"\"\n        return self._get_message(\n            message, self._response_indices, self._pending_responses, timeout,\n            lambda x, y: True\n        )\n\n    def get_reply(self, message=None, *, timeout=None):\n        \"\"\"\n        Gets the next message that explicitly replies to a previous one.\n        \"\"\"\n        return self._get_message(\n            message, self._reply_indices, self._pending_replies, timeout,\n            lambda x, y: x.reply_to and x.reply_to.reply_to_msg_id == y\n        )\n\n    def _get_message(\n            self, target_message, indices, pending, timeout, condition):\n        \"\"\"\n        Gets the next desired message under the desired condition.\n\n        Args:\n            target_message (`object`):\n                The target message for which we want to find another\n                response that applies based on `condition`.\n\n            indices (`dict`):\n                This dictionary remembers the last ID chosen for the\n                input `target_message`.\n\n            pending (`dict`):\n                This dictionary remembers {msg_id: Future} to be set\n                once `condition` is met.\n\n            timeout (`int`):\n                The timeout (in seconds) override to use for this operation.\n\n            condition (`callable`):\n                The condition callable that checks if an incoming\n                message is a valid response.\n        \"\"\"\n        start_time = time.time()\n        target_id = self._get_message_id(target_message)\n\n        # If there is no last-chosen ID, make sure to pick one *after*\n        # the input message, since we don't want responses back in time\n        if target_id not in indices:\n            for i, incoming in enumerate(self._incoming):\n                if incoming.id > target_id:\n                    indices[target_id] = i\n                    break\n            else:\n                indices[target_id] = len(self._incoming)\n\n        # We will always return a future from here, even if the result\n        # can be set immediately. Otherwise, needing to await only\n        # sometimes is an annoying edge case (i.e. we would return\n        # a `Message` but `get_response()` always `await`'s).\n        future = self._client.loop.create_future()\n\n        # If there are enough responses saved return the next one\n        last_idx = indices[target_id]\n        if last_idx < len(self._incoming):\n            incoming = self._incoming[last_idx]\n            if condition(incoming, target_id):\n                indices[target_id] += 1\n                future.set_result(incoming)\n                return future\n\n        # Otherwise the next incoming response will be the one to use\n        #\n        # Note how we fill \"pending\" before giving control back to the\n        # event loop through \"await\". We want to register it as soon as\n        # possible, since any other task switch may arrive with the result.\n        pending[target_id] = future\n        return self._get_result(future, start_time, timeout, pending, target_id)\n\n    def get_edit(self, message=None, *, timeout=None):\n        \"\"\"\n        Awaits for an edit after the last message to arrive.\n        The arguments are the same as those for `get_response`.\n        \"\"\"\n        start_time = time.time()\n        target_id = self._get_message_id(message)\n\n        target_date = self._edit_dates.get(target_id, 0)\n        earliest_edit = min(\n            (x for x in self._incoming\n             if x.edit_date\n             and x.id > target_id\n             and x.edit_date.timestamp() > target_date\n             ),\n            key=lambda x: x.edit_date.timestamp(),\n            default=None\n        )\n\n        future = self._client.loop.create_future()\n        if earliest_edit and earliest_edit.edit_date.timestamp() > target_date:\n            self._edit_dates[target_id] = earliest_edit.edit_date.timestamp()\n            future.set_result(earliest_edit)\n            return future  # we should always return something we can await\n\n        # Otherwise the next incoming response will be the one to use\n        self._pending_edits[target_id] = future\n        return self._get_result(future, start_time, timeout, self._pending_edits, target_id)\n\n    def wait_read(self, message=None, *, timeout=None):\n        \"\"\"\n        Awaits for the sent message to be marked as read. Note that\n        receiving a response doesn't imply the message was read, and\n        this action will also trigger even without a response.\n        \"\"\"\n        start_time = time.time()\n        future = self._client.loop.create_future()\n        target_id = self._get_message_id(message)\n\n        if self._last_read is None:\n            self._last_read = target_id - 1\n\n        if self._last_read >= target_id:\n            return\n\n        self._pending_reads[target_id] = future\n        return self._get_result(future, start_time, timeout, self._pending_reads, target_id)\n\n    async def wait_event(self, event, *, timeout=None):\n        \"\"\"\n        Waits for a custom event to occur. Timeouts still apply.\n\n        .. note::\n\n            **Only use this if there isn't another method available!**\n            For example, don't use `wait_event` for new messages,\n            since `get_response` already exists, etc.\n\n        Unless you're certain that your code will run fast enough,\n        generally you should get a \"handle\" of this special coroutine\n        before acting. In this example you will see how to wait for a user\n        to join a group with proper use of `wait_event`:\n\n        .. code-block:: python\n\n            from telethon import TelegramClient, events\n\n            client = TelegramClient(...)\n            group_id = ...\n\n            async def main():\n                # Could also get the user id from an event; this is just an example\n                user_id = ...\n\n                async with client.conversation(user_id) as conv:\n                    # Get a handle to the future event we'll wait for\n                    handle = conv.wait_event(events.ChatAction(\n                        group_id,\n                        func=lambda e: e.user_joined and e.user_id == user_id\n                    ))\n\n                    # Perform whatever action in between\n                    await conv.send_message('Please join this group before speaking to me!')\n\n                    # Wait for the event we registered above to fire\n                    event = await handle\n\n                    # Continue with the conversation\n                    await conv.send_message('Thanks!')\n\n        This way your event can be registered before acting,\n        since the response may arrive before your event was\n        registered. It depends on your use case since this\n        also means the event can arrive before you send\n        a previous action.\n        \"\"\"\n        start_time = time.time()\n        if isinstance(event, type):\n            event = event()\n\n        await event.resolve(self._client)\n\n        counter = Conversation._custom_counter\n        Conversation._custom_counter += 1\n\n        future = self._client.loop.create_future()\n        self._custom[counter] = (event, future)\n        try:\n            return await self._get_result(future, start_time, timeout, self._custom, counter)\n        finally:\n            # Need to remove it from the dict if it times out, else we may\n            # try and fail to set the result later (#1618).\n            self._custom.pop(counter, None)\n\n    async def _check_custom(self, built):\n        for key, (ev, fut) in list(self._custom.items()):\n            ev_type = type(ev)\n            inst = built[ev_type]\n\n            if inst:\n                filter = ev.filter(inst)\n                if inspect.isawaitable(filter):\n                    filter = await filter\n\n                if filter:\n                    fut.set_result(inst)\n                    del self._custom[key]\n\n    def _on_new_message(self, response):\n        response = response.message\n        if response.chat_id != self.chat_id or response.out:\n            return\n\n        if len(self._incoming) == self._max_incoming:\n            self._cancel_all(ValueError('Too many incoming messages'))\n            return\n\n        self._incoming.append(response)\n\n        # Most of the time, these dictionaries will contain just one item\n        # TODO In fact, why not make it be that way? Force one item only.\n        #      How often will people want to wait for two responses at\n        #      the same time? It's impossible, first one will arrive\n        #      and then another, so they can do that.\n        for msg_id, future in list(self._pending_responses.items()):\n            self._response_indices[msg_id] = len(self._incoming)\n            future.set_result(response)\n            del self._pending_responses[msg_id]\n\n        for msg_id, future in list(self._pending_replies.items()):\n            if response.reply_to and msg_id == response.reply_to.reply_to_msg_id:\n                self._reply_indices[msg_id] = len(self._incoming)\n                future.set_result(response)\n                del self._pending_replies[msg_id]\n\n    def _on_edit(self, message):\n        message = message.message\n        if message.chat_id != self.chat_id or message.out:\n            return\n\n        # We have to update our incoming messages with the new edit date\n        for i, m in enumerate(self._incoming):\n            if m.id == message.id:\n                self._incoming[i] = message\n                break\n\n        for msg_id, future in list(self._pending_edits.items()):\n            if msg_id < message.id:\n                edit_ts = message.edit_date.timestamp()\n\n                # We compare <= because edit_ts resolution is always to\n                # seconds, but we may have increased _edit_dates before.\n                # Since the dates are ever growing this is not a problem.\n                if edit_ts <= self._edit_dates.get(msg_id, 0):\n                    self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA\n                else:\n                    self._edit_dates[msg_id] = message.edit_date.timestamp()\n\n                future.set_result(message)\n                del self._pending_edits[msg_id]\n\n    def _on_read(self, event):\n        if event.chat_id != self.chat_id or event.inbox:\n            return\n\n        self._last_read = event.max_id\n\n        for msg_id, pending in list(self._pending_reads.items()):\n            if msg_id >= self._last_read:\n                pending.set_result(True)\n                del self._pending_reads[msg_id]\n\n    def _get_message_id(self, message):\n        if message is not None:  # 0 is valid but false-y, check for None\n            return message if isinstance(message, int) else message.id\n        elif self._last_outgoing:\n            return self._last_outgoing\n        else:\n            raise ValueError('No message was sent previously')\n\n    @_checks_cancelled\n    def _get_result(self, future, start_time, timeout, pending, target_id):\n        due = self._total_due\n        if timeout is None:\n            timeout = self._timeout\n\n        if timeout is not None:\n            due = min(due, start_time + timeout)\n\n        # NOTE: We can't try/finally to pop from pending here because\n        #       the event loop needs to get back to us, but it might\n        #       dispatch another update before, and in that case a\n        #       response could be set twice. So responses must be\n        #       cleared when their futures are set to a result.\n        return asyncio.wait_for(\n            future,\n            timeout=None if due == float('inf') else due - time.time()\n        )\n\n    def _cancel_all(self, exception=None):\n        self._cancelled = True\n        for pending in itertools.chain(\n                self._pending_responses.values(),\n                self._pending_replies.values(),\n                self._pending_edits.values()):\n            if exception:\n                pending.set_exception(exception)\n            else:\n                pending.cancel()\n\n        for _, fut in self._custom.values():\n            if exception:\n                fut.set_exception(exception)\n            else:\n                fut.cancel()\n\n    async def __aenter__(self):\n        self._input_chat = \\\n            await self._client.get_input_entity(self._input_chat)\n\n        self._chat_peer = utils.get_peer(self._input_chat)\n\n        # Make sure we're the only conversation in this chat if it's exclusive\n        chat_id = utils.get_peer_id(self._chat_peer)\n        conv_set = self._client._conversations[chat_id]\n        if self._exclusive and conv_set:\n            raise errors.AlreadyInConversationError()\n\n        conv_set.add(self)\n        self._cancelled = False\n\n        self._last_outgoing = 0\n        self._last_incoming = 0\n        for d in (\n                self._outgoing, self._incoming,\n                self._pending_responses, self._pending_replies,\n                self._pending_edits, self._response_indices,\n                self._reply_indices, self._edit_dates, self._custom):\n            d.clear()\n\n        if self._total_timeout:\n            self._total_due = time.time() + self._total_timeout\n        else:\n            self._total_due = float('inf')\n\n        return self\n\n    def cancel(self):\n        \"\"\"\n        Cancels the current conversation. Pending responses and subsequent\n        calls to get a response will raise ``asyncio.CancelledError``.\n\n        This method is synchronous and should not be awaited.\n        \"\"\"\n        self._cancel_all()\n\n    async def cancel_all(self):\n        \"\"\"\n        Calls `cancel` on *all* conversations in this chat.\n\n        Note that you should ``await`` this method, since it's meant to be\n        used outside of a context manager, and it needs to resolve the chat.\n        \"\"\"\n        chat_id = await self._client.get_peer_id(self._input_chat)\n        for conv in self._client._conversations[chat_id]:\n            conv.cancel()\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        chat_id = utils.get_peer_id(self._chat_peer)\n        conv_set = self._client._conversations[chat_id]\n        conv_set.discard(self)\n        if not conv_set:\n            del self._client._conversations[chat_id]\n\n        self._cancel_all()\n\n    __enter__ = helpers._sync_enter\n    __exit__ = helpers._sync_exit\n"
  },
  {
    "path": "telethon/tl/custom/dialog.py",
    "content": "from . import Draft\nfrom .. import TLObject, types, functions\nfrom ... import utils\n\n\nclass Dialog:\n    \"\"\"\n    Custom class that encapsulates a dialog (an open \"conversation\" with\n    someone, a group or a channel) providing an abstraction to easily\n    access the input version/normal entity/message etc. The library will\n    return instances of this class when calling :meth:`.get_dialogs()`.\n\n    Args:\n        dialog (:tl:`Dialog`):\n            The original ``Dialog`` instance.\n\n        pinned (`bool`):\n            Whether this dialog is pinned to the top or not.\n\n        folder_id (`folder_id`):\n            The folder ID that this dialog belongs to.\n\n        archived (`bool`):\n            Whether this dialog is archived or not (``folder_id is None``).\n\n        message (`Message <telethon.tl.custom.message.Message>`):\n            The last message sent on this dialog. Note that this member\n            will not be updated when new messages arrive, it's only set\n            on creation of the instance.\n\n        date (`datetime`):\n            The date of the last message sent on this dialog.\n\n        entity (`entity`):\n            The entity that belongs to this dialog (user, chat or channel).\n\n        input_entity (:tl:`InputPeer`):\n            Input version of the entity.\n\n        id (`int`):\n            The marked ID of the entity, which is guaranteed to be unique.\n\n        name (`str`):\n            Display name for this dialog. For chats and channels this is\n            their title, and for users it's \"First-Name Last-Name\".\n\n        title (`str`):\n            Alias for `name`.\n\n        unread_count (`int`):\n            How many messages are currently unread in this dialog. Note that\n            this value won't update when new messages arrive.\n\n        unread_mentions_count (`int`):\n            How many mentions are currently unread in this dialog. Note that\n            this value won't update when new messages arrive.\n\n        draft (`Draft <telethon.tl.custom.draft.Draft>`):\n            The draft object in this dialog. It will not be `None`,\n            so you can call ``draft.set_message(...)``.\n\n        is_user (`bool`):\n            `True` if the `entity` is a :tl:`User`.\n\n        is_group (`bool`):\n            `True` if the `entity` is a :tl:`Chat`\n            or a :tl:`Channel` megagroup.\n\n        is_channel (`bool`):\n            `True` if the `entity` is a :tl:`Channel`.\n    \"\"\"\n    def __init__(self, client, dialog, entities, message):\n        # Both entities and messages being dicts {ID: item}\n        self._client = client\n        self.dialog = dialog\n        self.pinned = bool(dialog.pinned)\n        self.folder_id = dialog.folder_id\n        self.archived = dialog.folder_id is not None\n        self.message = message\n        self.date = getattr(self.message, 'date', None)\n\n        self.entity = entities[utils.get_peer_id(dialog.peer)]\n        self.input_entity = utils.get_input_peer(self.entity)\n        self.id = utils.get_peer_id(self.entity)  # ^ May be InputPeerSelf()\n        self.name = self.title = utils.get_display_name(self.entity)\n\n        self.unread_count = dialog.unread_count\n        self.unread_mentions_count = dialog.unread_mentions_count\n\n        self.draft = Draft(client, self.entity, self.dialog.draft)\n\n        self.is_user = isinstance(self.entity, types.User)\n        self.is_group = (\n            isinstance(self.entity, (types.Chat, types.ChatForbidden)) or\n            (isinstance(self.entity, types.Channel) and self.entity.megagroup)\n        )\n        self.is_channel = isinstance(self.entity, types.Channel)\n\n    async def send_message(self, *args, **kwargs):\n        \"\"\"\n        Sends a message to this dialog. This is just a wrapper around\n        ``client.send_message(dialog.input_entity, *args, **kwargs)``.\n        \"\"\"\n        return await self._client.send_message(\n            self.input_entity, *args, **kwargs)\n\n    async def delete(self, revoke=False):\n        \"\"\"\n        Deletes the dialog from your dialog list. If you own the\n        channel this won't destroy it, only delete it from the list.\n\n        Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog`\n        with ``entity`` already set.\n        \"\"\"\n        # Pass the entire entity so the method can determine whether\n        # the `Chat` is deactivated (in which case we don't kick ourselves,\n        # or it would raise `PEER_ID_INVALID`).\n        await self._client.delete_dialog(self.entity, revoke=revoke)\n\n    async def archive(self, folder=1):\n        \"\"\"\n        Archives (or un-archives) this dialog.\n\n        Args:\n            folder (`int`, optional):\n                The folder to which the dialog should be archived to.\n\n                If you want to \"un-archive\" it, use ``folder=0``.\n\n        Returns:\n            The :tl:`Updates` object that the request produces.\n\n        Example:\n\n            .. code-block:: python\n\n                # Archiving\n                dialog.archive()\n\n                # Un-archiving\n                dialog.archive(0)\n        \"\"\"\n        return await self._client(functions.folders.EditPeerFoldersRequest([\n            types.InputFolderPeer(self.input_entity, folder_id=folder)\n        ]))\n\n    def to_dict(self):\n        return {\n            '_': 'Dialog',\n            'name': self.name,\n            'date': self.date,\n            'draft': self.draft,\n            'message': self.message,\n            'entity': self.entity,\n        }\n\n    def __str__(self):\n        return TLObject.pretty_format(self.to_dict())\n\n    def stringify(self):\n        return TLObject.pretty_format(self.to_dict(), indent=0)\n"
  },
  {
    "path": "telethon/tl/custom/draft.py",
    "content": "import datetime\n\nfrom .. import TLObject, types\nfrom ..functions.messages import SaveDraftRequest\nfrom ..types import DraftMessage\nfrom ...errors import RPCError\nfrom ...extensions import markdown\nfrom ...utils import get_input_peer, get_peer, get_peer_id\n\n\nclass Draft:\n    \"\"\"\n    Custom class that encapsulates a draft on the Telegram servers, providing\n    an abstraction to change the message conveniently. The library will return\n    instances of this class when calling :meth:`get_drafts()`.\n\n    Args:\n        date (`datetime`):\n            The date of the draft.\n\n        link_preview (`bool`):\n            Whether the link preview is enabled or not.\n\n        reply_to_msg_id (`int`):\n            The message ID that the draft will reply to.\n    \"\"\"\n    def __init__(self, client, entity, draft):\n        self._client = client\n        self._peer = get_peer(entity)\n        self._entity = entity\n        self._input_entity = get_input_peer(entity) if entity else None\n\n        if not draft or not isinstance(draft, DraftMessage):\n            draft = DraftMessage('', None, None, None, None)\n\n        self._text = markdown.unparse(draft.message, draft.entities)\n        self._raw_text = draft.message\n        self.date = draft.date\n        self.link_preview = not draft.no_webpage\n        self.reply_to_msg_id = draft.reply_to.reply_to_msg_id if isinstance(draft.reply_to, types.InputReplyToMessage) else None\n\n    @property\n    def entity(self):\n        \"\"\"\n        The entity that belongs to this dialog (user, chat or channel).\n        \"\"\"\n        return self._entity\n\n    @property\n    def input_entity(self):\n        \"\"\"\n        Input version of the entity.\n        \"\"\"\n        if not self._input_entity:\n            try:\n                self._input_entity = self._client._mb_entity_cache.get(\n                        get_peer_id(self._peer, add_mark=False))._as_input_peer()\n            except AttributeError:\n                pass\n\n        return self._input_entity\n\n    async def get_entity(self):\n        \"\"\"\n        Returns `entity` but will make an API call if necessary.\n        \"\"\"\n        if not self.entity and await self.get_input_entity():\n            try:\n                self._entity =\\\n                    await self._client.get_entity(self._input_entity)\n            except ValueError:\n                pass\n\n        return self._entity\n\n    async def get_input_entity(self):\n        \"\"\"\n        Returns `input_entity` but will make an API call if necessary.\n        \"\"\"\n        # We don't actually have an API call we can make yet\n        # to get more info, but keep this method for consistency.\n        return self.input_entity\n\n    @property\n    def text(self):\n        \"\"\"\n        The markdown text contained in the draft. It will be\n        empty if there is no text (and hence no draft is set).\n        \"\"\"\n        return self._text\n\n    @property\n    def raw_text(self):\n        \"\"\"\n        The raw (text without formatting) contained in the draft.\n        It will be empty if there is no text (thus draft not set).\n        \"\"\"\n        return self._raw_text\n\n    @property\n    def is_empty(self):\n        \"\"\"\n        Convenience bool to determine if the draft is empty or not.\n        \"\"\"\n        return not self._text\n\n    async def set_message(\n            self, text=None, reply_to=0, parse_mode=(),\n            link_preview=None):\n        \"\"\"\n        Changes the draft message on the Telegram servers. The changes are\n        reflected in this object.\n\n        :param str text: New text of the draft.\n                         Preserved if left as None.\n\n        :param int reply_to: Message ID to reply to.\n                             Preserved if left as 0, erased if set to None.\n\n        :param bool link_preview: Whether to attach a web page preview.\n                                  Preserved if left as None.\n\n        :param str parse_mode: The parse mode to be used for the text.\n        :return bool: `True` on success.\n        \"\"\"\n        if text is None:\n            text = self._text\n\n        if reply_to == 0:\n            reply_to = self.reply_to_msg_id\n\n        if link_preview is None:\n            link_preview = self.link_preview\n\n        raw_text, entities =\\\n            await self._client._parse_message_text(text, parse_mode)\n\n        result = await self._client(SaveDraftRequest(\n            peer=self._peer,\n            message=raw_text,\n            no_webpage=not link_preview,\n            reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),\n            entities=entities\n        ))\n\n        if result:\n            self._text = text\n            self._raw_text = raw_text\n            self.link_preview = link_preview\n            self.reply_to_msg_id = reply_to\n            self.date = datetime.datetime.now(tz=datetime.timezone.utc)\n\n        return result\n\n    async def send(self, clear=True, parse_mode=()):\n        \"\"\"\n        Sends the contents of this draft to the dialog. This is just a\n        wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``.\n        \"\"\"\n        await self._client.send_message(\n            self._peer, self.text, reply_to=self.reply_to_msg_id,\n            link_preview=self.link_preview, parse_mode=parse_mode,\n            clear_draft=clear\n        )\n\n    async def delete(self):\n        \"\"\"\n        Deletes this draft, and returns `True` on success.\n        \"\"\"\n        return await self.set_message(text='')\n\n    def to_dict(self):\n        try:\n            entity = self.entity\n        except RPCError as e:\n            entity = e\n\n        return {\n            '_': 'Draft',\n            'text': self.text,\n            'entity': entity,\n            'date': self.date,\n            'link_preview': self.link_preview,\n            'reply_to_msg_id': self.reply_to_msg_id\n        }\n\n    def __str__(self):\n        return TLObject.pretty_format(self.to_dict())\n\n    def stringify(self):\n        return TLObject.pretty_format(self.to_dict(), indent=0)\n"
  },
  {
    "path": "telethon/tl/custom/file.py",
    "content": "import mimetypes\nimport os\n\nfrom ... import utils\nfrom ...tl import types\n\n\nclass File:\n    \"\"\"\n    Convenience class over media like photos or documents, which\n    supports accessing the attributes in a more convenient way.\n\n    If any of the attributes are not present in the current media,\n    the properties will be `None`.\n\n    The original media is available through the ``media`` attribute.\n    \"\"\"\n    def __init__(self, media):\n        self.media = media\n\n    @property\n    def id(self):\n        \"\"\"\n        The old bot-API style ``file_id`` representing this file.\n\n        .. warning::\n\n            This feature has not been maintained for a long time and\n            may not work. It will be removed in future versions.\n\n        .. note::\n\n            This file ID may not work under user accounts,\n            but should still be usable by bot accounts.\n\n            You can, however, still use it to identify\n            a file in for example a database.\n        \"\"\"\n        return utils.pack_bot_file_id(self.media)\n\n    @property\n    def name(self):\n        \"\"\"\n        The file name of this document.\n        \"\"\"\n        return self._from_attr(types.DocumentAttributeFilename, 'file_name')\n\n    @property\n    def ext(self):\n        \"\"\"\n        The extension from the mime type of this file.\n\n        If the mime type is unknown, the extension\n        from the file name (if any) will be used.\n        \"\"\"\n        return (\n            mimetypes.guess_extension(self.mime_type)\n            or os.path.splitext(self.name or '')[-1]\n            or None\n        )\n\n    @property\n    def mime_type(self):\n        \"\"\"\n        The mime-type of this file.\n        \"\"\"\n        if isinstance(self.media, types.Photo):\n            return 'image/jpeg'\n        elif isinstance(self.media, types.Document):\n            return self.media.mime_type\n\n    @property\n    def width(self):\n        \"\"\"\n        The width in pixels of this media if it's a photo or a video.\n        \"\"\"\n        if isinstance(self.media, types.Photo):\n            return max(getattr(s, 'w', 0) for s in self.media.sizes)\n\n        return self._from_attr((\n            types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w')\n\n    @property\n    def height(self):\n        \"\"\"\n        The height in pixels of this media if it's a photo or a video.\n        \"\"\"\n        if isinstance(self.media, types.Photo):\n            return max(getattr(s, 'h', 0) for s in self.media.sizes)\n\n        return self._from_attr((\n           types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h')\n\n    @property\n    def duration(self):\n        \"\"\"\n        The duration in seconds of the audio or video.\n        \"\"\"\n        return self._from_attr((\n            types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration')\n\n    @property\n    def title(self):\n        \"\"\"\n        The title of the song.\n        \"\"\"\n        return self._from_attr(types.DocumentAttributeAudio, 'title')\n\n    @property\n    def performer(self):\n        \"\"\"\n        The performer of the song.\n        \"\"\"\n        return self._from_attr(types.DocumentAttributeAudio, 'performer')\n\n    @property\n    def emoji(self):\n        \"\"\"\n        A string with all emoji that represent the current sticker.\n        \"\"\"\n        return self._from_attr(types.DocumentAttributeSticker, 'alt')\n\n    @property\n    def sticker_set(self):\n        \"\"\"\n        The :tl:`InputStickerSet` to which the sticker file belongs.\n        \"\"\"\n        return self._from_attr(types.DocumentAttributeSticker, 'stickerset')\n\n    @property\n    def size(self):\n        \"\"\"\n        The size in bytes of this file.\n\n        For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions.\n        \"\"\"\n        if isinstance(self.media, types.Photo):\n            return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None)\n        elif isinstance(self.media, types.Document):\n            return self.media.size\n\n    def _from_attr(self, cls, field):\n        if isinstance(self.media, types.Document):\n            for attr in self.media.attributes:\n                if isinstance(attr, cls):\n                    return getattr(attr, field, None)\n"
  },
  {
    "path": "telethon/tl/custom/forward.py",
    "content": "from .chatgetter import ChatGetter\nfrom .sendergetter import SenderGetter\nfrom ... import utils, helpers\nfrom ...tl import types\n\n\nclass Forward(ChatGetter, SenderGetter):\n    \"\"\"\n    Custom class that encapsulates a :tl:`MessageFwdHeader` providing an\n    abstraction to easily access information like the original sender.\n\n    Remember that this class implements `ChatGetter\n    <telethon.tl.custom.chatgetter.ChatGetter>` and `SenderGetter\n    <telethon.tl.custom.sendergetter.SenderGetter>` which means you\n    have access to all their sender and chat properties and methods.\n\n    Attributes:\n\n        original_fwd (:tl:`MessageFwdHeader`):\n            The original :tl:`MessageFwdHeader` instance.\n\n        Any other attribute:\n            Attributes not described here are the same as those available\n            in the original :tl:`MessageFwdHeader`.\n    \"\"\"\n    def __init__(self, client, original, entities):\n        # Copy all the fields, not reference! It would cause memory cycles:\n        #   self.original_fwd.original_fwd.original_fwd.original_fwd\n        # ...would be valid if we referenced.\n        self.__dict__.update(original.__dict__)\n        self.original_fwd = original\n\n        sender_id = sender = input_sender = peer = chat = input_chat = None\n        if original.from_id:\n            ty = helpers._entity_type(original.from_id)\n            if ty == helpers._EntityType.USER:\n                sender_id = utils.get_peer_id(original.from_id)\n                sender, input_sender = utils._get_entity_pair(\n                    sender_id, entities, client._mb_entity_cache)\n\n            elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL):\n                peer = original.from_id\n                chat, input_chat = utils._get_entity_pair(\n                    utils.get_peer_id(peer), entities, client._mb_entity_cache)\n\n        # This call resets the client\n        ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat)\n        SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender)\n        self._client = client\n\n    # TODO We could reload the message\n"
  },
  {
    "path": "telethon/tl/custom/inlinebuilder.py",
    "content": "import hashlib\n\nfrom .. import functions, types\nfrom ... import utils\n\n_TYPE_TO_MIMES = {\n    'gif': ['image/gif'],  # 'video/mp4' too, but that's used for video\n    'article': ['text/html'],\n    'audio': ['audio/mpeg'],\n    'contact': [],\n    'file': ['application/pdf', 'application/zip'],  # actually any\n    'geo': [],\n    'photo': ['image/jpeg'],\n    'sticker': ['image/webp', 'application/x-tgsticker'],\n    'venue': [],\n    'video': ['video/mp4'],  # tdlib includes text/html for some reason\n    'voice': ['audio/ogg'],\n}\n\n\nclass InlineBuilder:\n    \"\"\"\n    Helper class to allow defining `InlineQuery\n    <telethon.events.inlinequery.InlineQuery>` ``results``.\n\n    Common arguments to all methods are\n    explained here to avoid repetition:\n\n        text (`str`, optional):\n            If present, the user will send a text\n            message with this text upon being clicked.\n\n        link_preview (`bool`, optional):\n            Whether to show a link preview in the sent\n            text message or not.\n\n        geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional):\n            If present, it may either be a geo point or a venue.\n\n        period (int, optional):\n            The period in seconds to be used for geo points.\n\n        contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional):\n            If present, it must be the contact information to send.\n\n        game (`bool`, optional):\n            May be `True` to indicate that the game will be sent.\n\n        buttons (`list`, `custom.Button <telethon.tl.custom.button.Button>`, :tl:`KeyboardButton`, optional):\n            Same as ``buttons`` for `client.send_message()\n            <telethon.client.messages.MessageMethods.send_message>`.\n\n        parse_mode (`str`, optional):\n            Same as ``parse_mode`` for `client.send_message()\n            <telethon.client.messageparse.MessageParseMethods.parse_mode>`.\n\n        id (`str`, optional):\n            The string ID to use for this result. If not present, it\n            will be the SHA256 hexadecimal digest of converting the\n            created :tl:`InputBotInlineResult` with empty ID to ``bytes()``,\n            so that the ID will be deterministic for the same input.\n\n            .. note::\n\n                If two inputs are exactly the same, their IDs will be the same\n                too. If you send two articles with the same ID, it will raise\n                ``ResultIdDuplicateError``. Consider giving them an explicit\n                ID if you need to send two results that are the same.\n    \"\"\"\n    def __init__(self, client):\n        self._client = client\n\n    # noinspection PyIncorrectDocstring\n    async def article(\n            self, title, description=None,\n            *, url=None, thumb=None, content=None,\n            id=None, text=None, parse_mode=(), link_preview=True,\n            geo=None, period=60, contact=None, game=False, buttons=None\n    ):\n        \"\"\"\n        Creates new inline result of article type.\n\n        Args:\n            title (`str`):\n                The title to be shown for this result.\n\n            description (`str`, optional):\n                Further explanation of what this result means.\n\n            url (`str`, optional):\n                The URL to be shown for this result.\n\n            thumb (:tl:`InputWebDocument`, optional):\n                The thumbnail to be shown for this result.\n                For now it has to be a :tl:`InputWebDocument` if present.\n\n            content (:tl:`InputWebDocument`, optional):\n                The content to be shown for this result.\n                For now it has to be a :tl:`InputWebDocument` if present.\n\n        Example:\n            .. code-block:: python\n\n                results = [\n                    # Option with title and description sending a message.\n                    builder.article(\n                        title='First option',\n                        description='This is the first option',\n                        text='Text sent after clicking this option',\n                    ),\n                    # Option with title URL to be opened when clicked.\n                    builder.article(\n                        title='Second option',\n                        url='https://example.com',\n                        text='Text sent if the user clicks the option and not the URL',\n                    ),\n                    # Sending a message with buttons.\n                    # You can use a list or a list of lists to include more buttons.\n                    builder.article(\n                        title='Third option',\n                        text='Text sent with buttons below',\n                        buttons=Button.url('https://example.com'),\n                    ),\n                ]\n        \"\"\"\n        # TODO Does 'article' work always?\n        # article, photo, gif, mpeg4_gif, video, audio,\n        # voice, document, location, venue, contact, game\n        result = types.InputBotInlineResult(\n            id=id or '',\n            type='article',\n            send_message=await self._message(\n                text=text, parse_mode=parse_mode, link_preview=link_preview,\n                geo=geo, period=period,\n                contact=contact,\n                game=game,\n                buttons=buttons\n            ),\n            title=title,\n            description=description,\n            url=url,\n            thumb=thumb,\n            content=content\n        )\n        if id is None:\n            result.id = hashlib.sha256(bytes(result)).hexdigest()\n\n        return result\n\n    # noinspection PyIncorrectDocstring\n    async def photo(\n            self, file, *, id=None, include_media=True,\n            text=None, parse_mode=(), link_preview=True,\n            geo=None, period=60, contact=None, game=False, buttons=None\n    ):\n        \"\"\"\n        Creates a new inline result of photo type.\n\n        Args:\n            include_media (`bool`, optional):\n                Whether the photo file used to display the result should be\n                included in the message itself or not. By default, the photo\n                is included, and the text parameter alters the caption.\n\n            file (`obj`, optional):\n                Same as ``file`` for `client.send_file()\n                <telethon.client.uploads.UploadMethods.send_file>`.\n\n        Example:\n            .. code-block:: python\n\n                results = [\n                    # Sending just the photo when the user selects it.\n                    builder.photo('/path/to/photo.jpg'),\n\n                    # Including a caption with some in-memory photo.\n                    photo_bytesio = ...\n                    builder.photo(\n                        photo_bytesio,\n                        text='This will be the caption of the sent photo',\n                    ),\n\n                    # Sending just the message without including the photo.\n                    builder.photo(\n                        photo,\n                        text='This will be a normal text message',\n                        include_media=False,\n                    ),\n                ]\n        \"\"\"\n        try:\n            fh = utils.get_input_photo(file)\n        except TypeError:\n            _, media, _ = await self._client._file_to_media(\n                file, allow_cache=True, as_image=True\n            )\n            if isinstance(media, types.InputPhoto):\n                fh = media\n            else:\n                r = await self._client(functions.messages.UploadMediaRequest(\n                    types.InputPeerSelf(), media=media\n                ))\n                fh = utils.get_input_photo(r.photo)\n\n        result = types.InputBotInlineResultPhoto(\n            id=id or '',\n            type='photo',\n            photo=fh,\n            send_message=await self._message(\n                text=text or '',\n                parse_mode=parse_mode,\n                link_preview=link_preview,\n                media=include_media,\n                geo=geo,\n                period=period,\n                contact=contact,\n                game=game,\n                buttons=buttons\n            )\n        )\n        if id is None:\n            result.id = hashlib.sha256(bytes(result)).hexdigest()\n\n        return result\n\n    # noinspection PyIncorrectDocstring\n    async def document(\n            self, file, title=None, *, description=None, type=None,\n            mime_type=None, attributes=None, force_document=False,\n            voice_note=False, video_note=False, use_cache=True, id=None,\n            text=None, parse_mode=(), link_preview=True,\n            geo=None, period=60, contact=None, game=False, buttons=None,\n            include_media=True\n    ):\n        \"\"\"\n        Creates a new inline result of document type.\n\n        `use_cache`, `mime_type`, `attributes`, `force_document`,\n        `voice_note` and `video_note` are described in `client.send_file\n        <telethon.client.uploads.UploadMethods.send_file>`.\n\n        Args:\n            file (`obj`):\n                Same as ``file`` for `client.send_file()\n                <telethon.client.uploads.UploadMethods.send_file>`.\n\n            title (`str`, optional):\n                The title to be shown for this result.\n\n            description (`str`, optional):\n                Further explanation of what this result means.\n\n            type (`str`, optional):\n                The type of the document. May be one of: article, audio,\n                contact, file, geo, gif, photo, sticker, venue, video, voice.\n                It will be automatically set if ``mime_type`` is specified,\n                and default to ``'file'`` if no matching mime type is found.\n                you may need to pass ``attributes`` in order to use ``type``\n                effectively.\n\n            attributes (`list`, optional):\n                Optional attributes that override the inferred ones, like\n                :tl:`DocumentAttributeFilename` and so on.\n\n            include_media (`bool`, optional):\n                Whether the document file used to display the result should be\n                included in the message itself or not. By default, the document\n                is included, and the text parameter alters the caption.\n\n        Example:\n            .. code-block:: python\n\n                results = [\n                    # Sending just the file when the user selects it.\n                    builder.document('/path/to/file.pdf'),\n\n                    # Including a caption with some in-memory file.\n                    file_bytesio = ...\n                    builder.document(\n                        file_bytesio,\n                        text='This will be the caption of the sent file',\n                    ),\n\n                    # Sending just the message without including the file.\n                    builder.document(\n                        photo,\n                        text='This will be a normal text message',\n                        include_media=False,\n                    ),\n                ]\n        \"\"\"\n        if type is None:\n            if voice_note:\n                type = 'voice'\n            elif mime_type:\n                for ty, mimes in _TYPE_TO_MIMES.items():\n                    for mime in mimes:\n                        if mime_type == mime:\n                            type = ty\n                            break\n\n            if type is None:\n                type = 'file'\n\n        try:\n            fh = utils.get_input_document(file)\n        except TypeError:\n            _, media, _ = await self._client._file_to_media(\n                file,\n                mime_type=mime_type,\n                attributes=attributes,\n                force_document=force_document,\n                voice_note=voice_note,\n                video_note=video_note,\n                allow_cache=use_cache\n            )\n            if isinstance(media, types.InputDocument):\n                fh = media\n            else:\n                r = await self._client(functions.messages.UploadMediaRequest(\n                    types.InputPeerSelf(), media=media\n                ))\n                fh = utils.get_input_document(r.document)\n\n        result = types.InputBotInlineResultDocument(\n            id=id or '',\n            type=type,\n            document=fh,\n            send_message=await self._message(\n                # Empty string for text if there's media but text is None.\n                # We may want to display a document but send text; however\n                # default to sending the media (without text, i.e. stickers).\n                text=text or '',\n                parse_mode=parse_mode,\n                link_preview=link_preview,\n                media=include_media,\n                geo=geo,\n                period=period,\n                contact=contact,\n                game=game,\n                buttons=buttons\n            ),\n            title=title,\n            description=description\n        )\n        if id is None:\n            result.id = hashlib.sha256(bytes(result)).hexdigest()\n\n        return result\n\n    # noinspection PyIncorrectDocstring\n    async def game(\n            self, short_name, *, id=None,\n            text=None, parse_mode=(), link_preview=True,\n            geo=None, period=60, contact=None, game=False, buttons=None\n    ):\n        \"\"\"\n        Creates a new inline result of game type.\n\n        Args:\n            short_name (`str`):\n                The short name of the game to use.\n        \"\"\"\n        result = types.InputBotInlineResultGame(\n            id=id or '',\n            short_name=short_name,\n            send_message=await self._message(\n                text=text, parse_mode=parse_mode, link_preview=link_preview,\n                geo=geo, period=period,\n                contact=contact,\n                game=game,\n                buttons=buttons\n            )\n        )\n        if id is None:\n            result.id = hashlib.sha256(bytes(result)).hexdigest()\n\n        return result\n\n    async def _message(\n            self, *,\n            text=None, parse_mode=(), link_preview=True, media=False,\n            geo=None, period=60, contact=None, game=False, buttons=None\n    ):\n        # Empty strings are valid but false-y; if they're empty use dummy '\\0'\n        args = ('\\0' if text == '' else text, geo, contact, game)\n        if sum(1 for x in args if x is not None and x is not False) != 1:\n            raise ValueError(\n                'Must set exactly one of text, geo, contact or game (set {})'\n                .format(', '.join(x[0] for x in zip(\n                    'text geo contact game'.split(), args) if x[1]) or 'none')\n            )\n\n        markup = self._client.build_reply_markup(buttons)\n        if text is not None:\n            text, msg_entities = await self._client._parse_message_text(\n                text, parse_mode\n            )\n            if media:\n                # \"MediaAuto\" means it will use whatever media the inline\n                # result itself has (stickers, photos, or documents), while\n                # respecting the user's text (caption) and formatting.\n                return types.InputBotInlineMessageMediaAuto(\n                    message=text,\n                    entities=msg_entities,\n                    reply_markup=markup\n                )\n            else:\n                return types.InputBotInlineMessageText(\n                    message=text,\n                    no_webpage=not link_preview,\n                    entities=msg_entities,\n                    reply_markup=markup\n                )\n        elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)):\n            return types.InputBotInlineMessageMediaGeo(\n                geo_point=utils.get_input_geo(geo),\n                period=period,\n                reply_markup=markup\n            )\n        elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)):\n            if isinstance(geo, types.InputMediaVenue):\n                geo_point = geo.geo_point\n            else:\n                geo_point = geo.geo\n\n            return types.InputBotInlineMessageMediaVenue(\n                geo_point=geo_point,\n                title=geo.title,\n                address=geo.address,\n                provider=geo.provider,\n                venue_id=geo.venue_id,\n                venue_type=geo.venue_type,\n                reply_markup=markup\n            )\n        elif isinstance(contact, (\n                types.InputMediaContact, types.MessageMediaContact)):\n            return types.InputBotInlineMessageMediaContact(\n                phone_number=contact.phone_number,\n                first_name=contact.first_name,\n                last_name=contact.last_name,\n                vcard=contact.vcard,\n                reply_markup=markup\n            )\n        elif game:\n            return types.InputBotInlineMessageGame(\n                reply_markup=markup\n            )\n        else:\n            raise ValueError('No text, game or valid geo or contact given')\n"
  },
  {
    "path": "telethon/tl/custom/inlineresult.py",
    "content": "from .. import types, functions\nfrom ... import utils\n\n\nclass InlineResult:\n    \"\"\"\n    Custom class that encapsulates a bot inline result providing\n    an abstraction to easily access some commonly needed features\n    (such as clicking a result to select it).\n\n    Attributes:\n\n        result (:tl:`BotInlineResult`):\n            The original :tl:`BotInlineResult` object.\n    \"\"\"\n    # tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834):\n    # gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice\n    #\n    # However, those documented in https://core.telegram.org/bots/api#inline-mode are different.\n    ARTICLE = 'article'\n    PHOTO = 'photo'\n    GIF = 'gif'\n    VIDEO = 'video'\n    VIDEO_GIF = 'mpeg4_gif'\n    AUDIO = 'audio'\n    DOCUMENT = 'document'\n    LOCATION = 'location'\n    VENUE = 'venue'\n    CONTACT = 'contact'\n    GAME = 'game'\n\n    def __init__(self, client, original, query_id=None, *, entity=None):\n        self._client = client\n        self.result = original\n        self._query_id = query_id\n        self._entity = entity\n\n    @property\n    def type(self):\n        \"\"\"\n        The always-present type of this result. It will be one of:\n        ``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``,\n        ``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``,\n        ``'contact'``, ``'game'``.\n\n        You can access all of these constants through `InlineResult`,\n        such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc.\n        \"\"\"\n        return self.result.type\n\n    @property\n    def message(self):\n        \"\"\"\n        The always-present :tl:`BotInlineMessage` that\n        will be sent if `click` is called on this result.\n        \"\"\"\n        return self.result.send_message\n\n    @property\n    def title(self):\n        \"\"\"\n        The title for this inline result. It may be `None`.\n        \"\"\"\n        return self.result.title\n\n    @property\n    def description(self):\n        \"\"\"\n        The description for this inline result. It may be `None`.\n        \"\"\"\n        return self.result.description\n\n    @property\n    def url(self):\n        \"\"\"\n        The URL present in this inline results. If you want to \"click\"\n        this URL to open it in your browser, you should use Python's\n        `webbrowser.open(url)` for such task.\n        \"\"\"\n        if isinstance(self.result, types.BotInlineResult):\n            return self.result.url\n\n    @property\n    def photo(self):\n        \"\"\"\n        Returns either the :tl:`WebDocument` thumbnail for\n        normal results or the :tl:`Photo` for media results.\n        \"\"\"\n        if isinstance(self.result, types.BotInlineResult):\n            return self.result.thumb\n        elif isinstance(self.result, types.BotInlineMediaResult):\n            return self.result.photo\n\n    @property\n    def document(self):\n        \"\"\"\n        Returns either the :tl:`WebDocument` content for\n        normal results or the :tl:`Document` for media results.\n        \"\"\"\n        if isinstance(self.result, types.BotInlineResult):\n            return self.result.content\n        elif isinstance(self.result, types.BotInlineMediaResult):\n            return self.result.document\n\n    async def click(self, entity=None, reply_to=None, comment_to=None,\n                    silent=False, clear_draft=False, hide_via=False,\n                    background=None):\n        \"\"\"\n        Clicks this result and sends the associated `message`.\n\n        Args:\n            entity (`entity`):\n                The entity to which the message of this result should be sent.\n\n            reply_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):\n                If present, the sent message will reply to this ID or message.\n\n            comment_to (`int` | `Message <telethon.tl.custom.message.Message>`, optional):\n                Similar to ``reply_to``, but replies in the linked group of a\n                broadcast channel instead (effectively leaving a \"comment to\"\n                the specified message).\n\n            silent (`bool`, optional):\n                Whether the message should notify people with sound or not.\n                Defaults to `False` (send with a notification sound unless\n                the person has the chat muted). Set it to `True` to alter\n                this behaviour.\n\n            clear_draft (`bool`, optional):\n                Whether the draft should be removed after sending the\n                message from this result or not. Defaults to `False`.\n\n            hide_via (`bool`, optional):\n                Whether the \"via @bot\" should be hidden or not.\n                Only works with certain bots (like @bing or @gif).\n\n            background (`bool`, optional):\n                Whether the message should be send in background.\n\n        \"\"\"\n        if entity:\n            entity = await self._client.get_input_entity(entity)\n        elif self._entity:\n            entity = self._entity\n        else:\n            raise ValueError('You must provide the entity where the result should be sent to')\n\n        if comment_to:\n            entity, reply_id = await self._client._get_comment_data(entity, comment_to)\n        else:\n            reply_id = None if reply_to is None else utils.get_message_id(reply_to)\n\n        req = functions.messages.SendInlineBotResultRequest(\n            peer=entity,\n            query_id=self._query_id,\n            id=self.result.id,\n            silent=silent,\n            background=background,\n            clear_draft=clear_draft,\n            hide_via=hide_via,\n            reply_to=None if reply_id is None else types.InputReplyToMessage(reply_id)\n        )\n        return self._client._get_response_message(\n            req, await self._client(req), entity)\n\n    async def download_media(self, *args, **kwargs):\n        \"\"\"\n        Downloads the media in this result (if there is a document, the\n        document will be downloaded; otherwise, the photo will if present).\n\n        This is a wrapper around `client.download_media\n        <telethon.client.downloads.DownloadMethods.download_media>`.\n        \"\"\"\n        if self.document or self.photo:\n            return await self._client.download_media(\n                self.document or self.photo, *args, **kwargs)\n"
  },
  {
    "path": "telethon/tl/custom/inlineresults.py",
    "content": "import time\n\nfrom .inlineresult import InlineResult\n\n\nclass InlineResults(list):\n    \"\"\"\n    Custom class that encapsulates :tl:`BotResults` providing\n    an abstraction to easily access some commonly needed features\n    (such as clicking one of the results to select it)\n\n    Note that this is a list of `InlineResult\n    <telethon.tl.custom.inlineresult.InlineResult>`\n    so you can iterate over it or use indices to\n    access its elements. In addition, it has some\n    attributes.\n\n    Attributes:\n        result (:tl:`BotResults`):\n            The original :tl:`BotResults` object.\n\n        query_id (`int`):\n            The random ID that identifies this query.\n\n        cache_time (`int`):\n            For how long the results should be considered\n            valid. You can call `results_valid` at any\n            moment to determine if the results are still\n            valid or not.\n\n        users (:tl:`User`):\n            The users present in this inline query.\n\n        gallery (`bool`):\n            Whether these results should be presented\n            in a grid (as a gallery of images) or not.\n\n        next_offset (`str`, optional):\n            The string to be used as an offset to get\n            the next chunk of results, if any.\n\n        switch_pm (:tl:`InlineBotSwitchPM`, optional):\n            If presents, the results should show a button to\n            switch to a private conversation with the bot using\n            the text in this object.\n    \"\"\"\n    def __init__(self, client, original, *, entity=None):\n        super().__init__(InlineResult(client, x, original.query_id, entity=entity)\n                         for x in original.results)\n\n        self.result = original\n        self.query_id = original.query_id\n        self.cache_time = original.cache_time\n        self._valid_until = time.time() + self.cache_time\n        self.users = original.users\n        self.gallery = bool(original.gallery)\n        self.next_offset = original.next_offset\n        self.switch_pm = original.switch_pm\n\n    def results_valid(self):\n        \"\"\"\n        Returns `True` if the cache time has not expired\n        yet and the results can still be considered valid.\n        \"\"\"\n        return time.time() < self._valid_until\n\n    def _to_str(self, item_function):\n        return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, '\n                'next_offset={}, switch_pm={}]'.format(\n            ', '.join(item_function(x) for x in self),\n            self.query_id,\n            self.cache_time,\n            self.users,\n            self.gallery,\n            self.next_offset,\n            self.switch_pm\n        ))\n\n    def __str__(self):\n        return self._to_str(str)\n\n    def __repr__(self):\n        return self._to_str(repr)\n"
  },
  {
    "path": "telethon/tl/custom/inputsizedfile.py",
    "content": "from ..types import InputFile\n\n\nclass InputSizedFile(InputFile):\n    \"\"\"InputFile class with two extra parameters: md5 (digest) and size\"\"\"\n    def __init__(self, id_, parts, name, md5, size):\n        super().__init__(id_, parts, name, md5.hexdigest())\n        self.md5 = md5.digest()\n        self.size = size\n"
  },
  {
    "path": "telethon/tl/custom/message.py",
    "content": "from typing import Optional, List, TYPE_CHECKING\nfrom datetime import datetime\nfrom .chatgetter import ChatGetter\nfrom .sendergetter import SenderGetter\nfrom .messagebutton import MessageButton\nfrom .forward import Forward\nfrom .file import File\nfrom .. import TLObject, types, functions, alltlobjects\nfrom ... import utils, errors\n\n\n# TODO Figure out a way to have the code generator error on missing fields\n# Maybe parsing the init function alone if that's possible.\nclass Message(ChatGetter, SenderGetter, TLObject):\n    \"\"\"\n    This custom class aggregates both :tl:`Message` and\n    :tl:`MessageService` to ease accessing their members.\n\n    Remember that this class implements `ChatGetter\n    <telethon.tl.custom.chatgetter.ChatGetter>` and `SenderGetter\n    <telethon.tl.custom.sendergetter.SenderGetter>` which means you\n    have access to all their sender and chat properties and methods.\n\n    Members:\n        out (`bool`):\n            Whether the message is outgoing (i.e. you sent it from\n            another session) or incoming (i.e. someone else sent it).\n\n            Note that messages in your own chat are always incoming,\n            but this member will be `True` if you send a message\n            to your own chat. Messages you forward to your chat are\n            *not* considered outgoing, just like official clients\n            display them.\n\n        mentioned (`bool`):\n            Whether you were mentioned in this message or not.\n            Note that replies to your own messages also count\n            as mentions.\n\n        media_unread (`bool`):\n            Whether you have read the media in this message\n            or not, e.g. listened to the voice note media.\n\n        silent (`bool`):\n            Whether the message should notify people with sound or not.\n            Previously used in channels, but since 9 August 2019, it can\n            also be `used in private chats\n            <https://telegram.org/blog/silent-messages-slow-mode>`_.\n\n        post (`bool`):\n            Whether this message is a post in a broadcast\n            channel or not.\n\n        from_scheduled (`bool`):\n            Whether this message was originated from a previously-scheduled\n            message or not.\n\n        legacy (`bool`):\n            Whether this is a legacy message or not.\n\n        edit_hide (`bool`):\n            Whether the edited mark of this message is edited\n            should be hidden (e.g. in GUI clients) or shown.\n\n        pinned (`bool`):\n            Whether this message is currently pinned or not.\n\n        noforwards (`bool`):\n            Whether this message can be forwarded or not.\n\n        invert_media (`bool`):\n            Whether the media in this message should be inverted.\n            \n        offline (`bool`):\n            Whether the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message.\n\n        id (`int`):\n            The ID of this message. This field is *always* present.\n            Any other member is optional and may be `None`.\n\n        from_id (:tl:`Peer`):\n            The peer who sent this message, which is either\n            :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`.\n            This value will be `None` for anonymous messages.\n\n        peer_id (:tl:`Peer`):\n            The peer to which this message was sent, which is either\n            :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This\n            will always be present except for empty messages.\n\n        fwd_from (:tl:`MessageFwdHeader`):\n            The original forward header if this message is a forward.\n            You should probably use the `forward` property instead.\n\n        via_bot_id (`int`):\n            The ID of the bot used to send this message\n            through its inline mode (e.g. \"via @like\").\n\n        reply_to (:tl:`MessageReplyHeader` | :tl:`MessageReplyStoryHeader`):\n            The original reply header if this message is replying to another.\n\n        date (`datetime`):\n            The UTC+0 `datetime` object indicating when this message\n            was sent. This will always be present except for empty\n            messages.\n\n        message (`str`):\n            The string text of the message for `Message\n            <telethon.tl.custom.message.Message>` instances,\n            which will be `None` for other types of messages.\n\n        media (:tl:`MessageMedia`):\n            The media sent with this message if any (such as\n            photos, videos, documents, gifs, stickers, etc.).\n\n            You may want to access the `photo`, `document`\n            etc. properties instead.\n\n            If the media was not present or it was :tl:`MessageMediaEmpty`,\n            this member will instead be `None` for convenience.\n\n        reply_markup (:tl:`ReplyMarkup`):\n            The reply markup for this message (which was sent\n            either via a bot or by a bot). You probably want\n            to access `buttons` instead.\n\n        entities (List[:tl:`MessageEntity`]):\n            The list of markup entities in this message,\n            such as bold, italics, code, hyperlinks, etc.\n\n        views (`int`):\n            The number of views this message from a broadcast\n            channel has. This is also present in forwards.\n\n        forwards (`int`):\n            The number of times this message has been forwarded.\n\n        replies (`int`):\n            The number of times another message has replied to this message.\n\n        edit_date (`datetime`):\n            The date when this message was last edited.\n\n        post_author (`str`):\n            The display name of the message sender to\n            show in messages sent to broadcast channels.\n\n        grouped_id (`int`):\n            If this message belongs to a group of messages\n            (photo albums or video albums), all of them will\n            have the same value here.\n\n        reactions (:tl:`MessageReactions`)\n            Reactions to this message.\n\n        restriction_reason (List[:tl:`RestrictionReason`])\n            An optional list of reasons why this message was restricted.\n            If the list is `None`, this message has not been restricted.\n\n        ttl_period (`int`):\n            The Time To Live period configured for this message.\n            The message should be erased from wherever it's stored (memory, a\n            local database, etc.) when\n            ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``.\n\n        action (:tl:`MessageAction`):\n            The message action object of the message for :tl:`MessageService`\n            instances, which will be `None` for other types of messages.\n\n        saved_peer_id (:tl:`Peer`)\n    \"\"\"\n\n    # region Initialization\n\n    def __init__(\n        self,\n        id: int,\n        peer_id: types.TypePeer,\n        date: Optional[datetime] = None,\n        message: Optional[str] = None,\n        # Copied from Message.__init__ signature\n        out: Optional[bool] = None,\n        mentioned: Optional[bool] = None,\n        media_unread: Optional[bool] = None,\n        silent: Optional[bool] = None,\n        post: Optional[bool] = None,\n        from_scheduled: Optional[bool] = None,\n        legacy: Optional[bool] = None,\n        edit_hide: Optional[bool] = None,\n        pinned: Optional[bool] = None,\n        noforwards: Optional[bool] = None,\n        invert_media: Optional[bool] = None,\n        offline: Optional[bool] = None,\n        video_processing_pending: Optional[bool] = None,\n        paid_suggested_post_stars: Optional[bool] = None,\n        paid_suggested_post_ton: Optional[bool] = None,\n        from_id: Optional[types.TypePeer] = None,\n        from_boosts_applied: Optional[int] = None,\n        saved_peer_id: Optional[types.TypePeer] = None,\n        fwd_from: Optional[types.TypeMessageFwdHeader] = None,\n        via_bot_id: Optional[int] = None,\n        via_business_bot_id: Optional[int] = None,\n        reply_to: Optional[types.TypeMessageReplyHeader] = None,\n        media: Optional[types.TypeMessageMedia] = None,\n        reply_markup: Optional[types.TypeReplyMarkup] = None,\n        entities: Optional[List[types.TypeMessageEntity]] = None,\n        views: Optional[int] = None,\n        forwards: Optional[int] = None,\n        replies: Optional[types.TypeMessageReplies] = None,\n        edit_date: Optional[datetime] = None,\n        post_author: Optional[str] = None,\n        grouped_id: Optional[int] = None,\n        reactions: Optional[types.TypeMessageReactions] = None,\n        restriction_reason: Optional[List[types.TypeRestrictionReason]] = None,\n        ttl_period: Optional[int] = None,\n        quick_reply_shortcut_id: Optional[int] = None,\n        effect: Optional[int] = None,\n        factcheck: Optional[types.TypeFactCheck] = None,\n        report_delivery_until_date: Optional[datetime] = None,\n        paid_message_stars: Optional[int] = None,\n        suggested_post: Optional[types.TypeSuggestedPost] = None,\n        schedule_repeat_period: Optional[int] = None,\n        summary_from_language: Optional[str] = None,\n        # Copied from MessageService.__init__ signature\n        action: Optional[types.TypeMessageAction] = None,\n        reactions_are_possible: Optional[bool] = None,\n    ):\n        # Copied from Message.__init__ body\n        self.id = id\n        self.peer_id = peer_id\n        self.date = date\n        self.message = message\n        self.out = bool(out)\n        self.mentioned = mentioned\n        self.media_unread = media_unread\n        self.silent = silent\n        self.post = post\n        self.from_scheduled = from_scheduled\n        self.legacy = legacy\n        self.edit_hide = edit_hide\n        self.pinned = pinned\n        self.noforwards = noforwards\n        self.invert_media = invert_media\n        self.offline = offline\n        self.video_processing_pending = video_processing_pending\n        self.paid_suggested_post_stars = paid_suggested_post_stars\n        self.paid_suggested_post_ton = paid_suggested_post_ton\n        self.from_id = from_id\n        self.from_boosts_applied = from_boosts_applied\n        self.saved_peer_id = saved_peer_id\n        self.fwd_from = fwd_from\n        self.via_bot_id = via_bot_id\n        self.via_business_bot_id = via_business_bot_id\n        self.reply_to = reply_to\n        self.media = None if isinstance(media, types.MessageMediaEmpty) else media\n        self.reply_markup = reply_markup\n        self.entities = entities\n        self.views = views\n        self.forwards = forwards\n        self.replies = replies\n        self.edit_date = edit_date\n        self.post_author = post_author\n        self.grouped_id = grouped_id\n        self.reactions = reactions\n        self.restriction_reason = restriction_reason\n        self.ttl_period = ttl_period\n        self.quick_reply_shortcut_id = quick_reply_shortcut_id\n        self.effect = effect\n        self.factcheck = factcheck\n        self.report_delivery_until_date = report_delivery_until_date\n        self.paid_message_stars = paid_message_stars\n        self.suggested_post = suggested_post\n        self.schedule_repeat_period = schedule_repeat_period\n        self.summary_from_language = summary_from_language\n        # Copied from MessageService.__init__ body\n        self.action = action\n        self.reactions_are_possible = reactions_are_possible\n\n        # Convenient storage for custom functions\n        # TODO This is becoming a bit of bloat\n        self._client = None\n        self._text = None\n        self._file = None\n        self._reply_message = None\n        self._buttons = None\n        self._buttons_flat = None\n        self._buttons_count = None\n        self._via_bot = None\n        self._via_input_bot = None\n        self._action_entities = None\n        self._linked_chat = None\n\n        sender_id = None\n        if from_id is not None:\n            sender_id = utils.get_peer_id(from_id)\n        elif peer_id:\n            # If the message comes from a Channel, let the sender be it\n            # ...or...\n            # incoming messages in private conversations no longer have from_id\n            # (layer 119+), but the sender can only be the chat we're in.\n            if post or (not out and isinstance(peer_id, types.PeerUser)):\n                sender_id = utils.get_peer_id(peer_id)\n\n        # Note that these calls would reset the client\n        ChatGetter.__init__(self, peer_id, broadcast=post)\n        SenderGetter.__init__(self, sender_id)\n\n        self._forward = None\n        self._reply_to_chat = None\n        self._reply_to_sender = None\n\n    def _finish_init(self, client, entities, input_chat):\n        \"\"\"\n        Finishes the initialization of this message by setting\n        the client that sent the message and making use of the\n        known entities.\n        \"\"\"\n        self._client = client\n\n        # Make messages sent to ourselves outgoing unless they're forwarded.\n        # This makes it consistent with official client's appearance.\n        if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from:\n            self.out = True\n\n        cache = client._mb_entity_cache\n\n        self._sender, self._input_sender = utils._get_entity_pair(\n            self.sender_id, entities, cache)\n\n        self._chat, self._input_chat = utils._get_entity_pair(\n            self.chat_id, entities, cache)\n\n        if input_chat:  # This has priority\n            self._input_chat = input_chat\n\n        if self.via_bot_id:\n            self._via_bot, self._via_input_bot = utils._get_entity_pair(\n                self.via_bot_id, entities, cache)\n\n        if self.fwd_from:\n            self._forward = Forward(self._client, self.fwd_from, entities)\n\n        if self.action:\n            if isinstance(self.action, (types.MessageActionChatAddUser,\n                                        types.MessageActionChatCreate)):\n                self._action_entities = [entities.get(i)\n                                         for i in self.action.users]\n            elif isinstance(self.action, types.MessageActionChatDeleteUser):\n                self._action_entities = [entities.get(self.action.user_id)]\n            elif isinstance(self.action, types.MessageActionChatJoinedByLink):\n                self._action_entities = [entities.get(self.action.inviter_id)]\n            elif isinstance(self.action, types.MessageActionChatMigrateTo):\n                self._action_entities = [entities.get(utils.get_peer_id(\n                    types.PeerChannel(self.action.channel_id)))]\n            elif isinstance(\n                    self.action, types.MessageActionChannelMigrateFrom):\n                self._action_entities = [entities.get(utils.get_peer_id(\n                    types.PeerChat(self.action.chat_id)))]\n\n        if self.replies and self.replies.channel_id:\n            self._linked_chat = entities.get(utils.get_peer_id(\n                    types.PeerChannel(self.replies.channel_id)))\n        \n        if isinstance(self.reply_to, types.MessageReplyHeader):\n            if self.reply_to.reply_to_peer_id:\n                self._reply_to_chat = entities.get(utils.get_peer_id(self.reply_to.reply_to_peer_id))\n            if self.reply_to.reply_from:\n                if self.reply_to.reply_from.from_id:\n                    self._reply_to_sender = entities.get(utils.get_peer_id(self.reply_to.reply_from.from_id))\n\n\n\n    # endregion Initialization\n\n    # region Public Properties\n\n    @property\n    def client(self):\n        \"\"\"\n        Returns the `TelegramClient <telethon.client.telegramclient.TelegramClient>`\n        that *patched* this message. This will only be present if you\n        **use the friendly methods**, it won't be there if you invoke\n        raw API methods manually, in which case you should only access\n        members, not properties.\n        \"\"\"\n        return self._client\n\n    @property\n    def text(self):\n        \"\"\"\n        The message text, formatted using the client's default\n        parse mode. Will be `None` for :tl:`MessageService`.\n        \"\"\"\n        if self._text is None and self._client:\n            if not self._client.parse_mode:\n                self._text = self.message\n            else:\n                self._text = self._client.parse_mode.unparse(\n                    self.message, self.entities)\n\n        return self._text\n\n    @text.setter\n    def text(self, value):\n        self._text = value\n        if self._client and self._client.parse_mode:\n            self.message, self.entities = self._client.parse_mode.parse(value)\n        else:\n            self.message, self.entities = value, []\n\n    @property\n    def raw_text(self):\n        \"\"\"\n        The raw message text, ignoring any formatting.\n        Will be `None` for :tl:`MessageService`.\n\n        Setting a value to this field will erase the\n        `entities`, unlike changing the `message` member.\n        \"\"\"\n        return self.message\n\n    @raw_text.setter\n    def raw_text(self, value):\n        self.message = value\n        self.entities = []\n        self._text = None\n\n    @property\n    def is_reply(self):\n        \"\"\"\n        `True` if the message is a reply to some other message or story.\n\n        Remember that if the replied-to is a message, \n        you can access the ID of the message this one is \n        replying to through `reply_to.reply_to_msg_id`,\n        and the `Message` object with `get_reply_message()`.\n        \"\"\"\n        return self.reply_to is not None\n\n    @property\n    def forward(self):\n        \"\"\"\n        The `Forward <telethon.tl.custom.forward.Forward>`\n        information if this message is a forwarded message.\n        \"\"\"\n        return self._forward\n\n    @property\n    def reply_to_chat(self):\n        \"\"\"\n        The :tl:`Channel` in which the replied-to message was sent,\n        if this message is a reply in another chat\n        \"\"\"\n        return self._reply_to_chat\n\n    @property\n    def reply_to_sender(self):\n        \"\"\"\n        The :tl:`User`, :tl:`Channel`, or whatever other entity that\n        sent the replied-to message, if this message is a reply in another chat.\n        \"\"\"\n        return self._reply_to_sender\n\n    @property\n    def buttons(self):\n        \"\"\"\n        Returns a list of lists of `MessageButton\n        <telethon.tl.custom.messagebutton.MessageButton>`,\n        if any.\n\n        Otherwise, it returns `None`.\n        \"\"\"\n        if self._buttons is None and self.reply_markup:\n            if not self.input_chat:\n                return\n            try:\n                bot = self._needed_markup_bot()\n            except ValueError:\n                return\n            else:\n                self._set_buttons(self._input_chat, bot)\n\n        return self._buttons\n\n    async def get_buttons(self):\n        \"\"\"\n        Returns `buttons` when that property fails (this is rarely needed).\n        \"\"\"\n        if not self.buttons and self.reply_markup:\n            chat = await self.get_input_chat()\n            if not chat:\n                return\n            try:\n                bot = self._needed_markup_bot()\n            except ValueError:\n                await self._reload_message()\n                bot = self._needed_markup_bot()  # TODO use via_input_bot\n\n            self._set_buttons(chat, bot)\n\n        return self._buttons\n\n    @property\n    def button_count(self):\n        \"\"\"\n        Returns the total button count (sum of all `buttons` rows).\n        \"\"\"\n        if self._buttons_count is None:\n            if isinstance(self.reply_markup, (\n                    types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)):\n                self._buttons_count = sum(\n                    len(row.buttons) for row in self.reply_markup.rows)\n            else:\n                self._buttons_count = 0\n\n        return self._buttons_count\n\n    @property\n    def file(self):\n        \"\"\"\n        Returns a `File <telethon.tl.custom.file.File>` wrapping the\n        `photo` or `document` in this message. If the media type is different\n        (polls, games, none, etc.), this property will be `None`.\n\n        This instance lets you easily access other properties, such as\n        `file.id <telethon.tl.custom.file.File.id>`,\n        `file.name <telethon.tl.custom.file.File.name>`,\n        etc., without having to manually inspect the ``document.attributes``.\n        \"\"\"\n        if not self._file:\n            media = self.photo or self.document\n            if media:\n                self._file = File(media)\n\n        return self._file\n\n    @property\n    def photo(self):\n        \"\"\"\n        The :tl:`Photo` media in this message, if any.\n\n        This will also return the photo for :tl:`MessageService` if its\n        action is :tl:`MessageActionChatEditPhoto`, or if the message has\n        a web preview with a photo.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaPhoto):\n            if isinstance(self.media.photo, types.Photo):\n                return self.media.photo\n        elif isinstance(self.action, types.MessageActionChatEditPhoto):\n            return self.action.photo\n        else:\n            web = self.web_preview\n            if web and isinstance(web.photo, types.Photo):\n                return web.photo\n\n    @property\n    def document(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if any.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaDocument):\n            if isinstance(self.media.document, types.Document):\n                return self.media.document\n        else:\n            web = self.web_preview\n            if web and isinstance(web.document, types.Document):\n                return web.document\n\n    @property\n    def web_preview(self):\n        \"\"\"\n        The :tl:`WebPage` media in this message, if any.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaWebPage):\n            if isinstance(self.media.webpage, types.WebPage):\n                return self.media.webpage\n\n    @property\n    def audio(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's an audio file.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeAudio,\n                                           lambda attr: not attr.voice)\n\n    @property\n    def voice(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's a voice note.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeAudio,\n                                           lambda attr: attr.voice)\n\n    @property\n    def video(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's a video.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeVideo)\n\n    @property\n    def video_note(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's a video note.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeVideo,\n                                           lambda attr: attr.round_message)\n\n    @property\n    def gif(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's a \"gif\".\n\n        \"Gif\" files by Telegram are normally ``.mp4`` video files without\n        sound, the so called \"animated\" media. However, it may be the actual\n        gif format if the file is too large.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeAnimated)\n\n    @property\n    def sticker(self):\n        \"\"\"\n        The :tl:`Document` media in this message, if it's a sticker.\n        \"\"\"\n        return self._document_by_attribute(types.DocumentAttributeSticker)\n\n    @property\n    def contact(self):\n        \"\"\"\n        The :tl:`MessageMediaContact` in this message, if it's a contact.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaContact):\n            return self.media\n\n    @property\n    def game(self):\n        \"\"\"\n        The :tl:`Game` media in this message, if it's a game.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaGame):\n            return self.media.game\n\n    @property\n    def geo(self):\n        \"\"\"\n        The :tl:`GeoPoint` media in this message, if it has a location.\n        \"\"\"\n        if isinstance(self.media, (types.MessageMediaGeo,\n                                   types.MessageMediaGeoLive,\n                                   types.MessageMediaVenue)):\n            return self.media.geo\n\n    @property\n    def invoice(self):\n        \"\"\"\n        The :tl:`MessageMediaInvoice` in this message, if it's an invoice.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaInvoice):\n            return self.media\n\n    @property\n    def poll(self):\n        \"\"\"\n        The :tl:`MessageMediaPoll` in this message, if it's a poll.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaPoll):\n            return self.media\n\n    @property\n    def venue(self):\n        \"\"\"\n        The :tl:`MessageMediaVenue` in this message, if it's a venue.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaVenue):\n            return self.media\n\n    @property\n    def dice(self):\n        \"\"\"\n        The :tl:`MessageMediaDice` in this message, if it's a dice roll.\n        \"\"\"\n        if isinstance(self.media, types.MessageMediaDice):\n            return self.media\n\n    @property\n    def action_entities(self):\n        \"\"\"\n        Returns a list of entities that took part in this action.\n\n        Possible cases for this are :tl:`MessageActionChatAddUser`,\n        :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`,\n        :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo`\n        and :tl:`MessageActionChannelMigrateFrom`.\n\n        If the action is neither of those, the result will be `None`.\n        If some entities could not be retrieved, the list may contain\n        some `None` items in it.\n        \"\"\"\n        return self._action_entities\n\n    @property\n    def via_bot(self):\n        \"\"\"\n        The bot :tl:`User` if the message was sent via said bot.\n\n        This will only be present if `via_bot_id` is not `None` and\n        the entity is known.\n        \"\"\"\n        return self._via_bot\n\n    @property\n    def via_input_bot(self):\n        \"\"\"\n        Returns the input variant of `via_bot`.\n        \"\"\"\n        return self._via_input_bot\n\n    @property\n    def reply_to_msg_id(self):\n        \"\"\"\n        Returns the message ID this message is replying to, if any.\n        This is equivalent to accessing ``.reply_to.reply_to_msg_id``.\n        \"\"\"\n        return (\n            self.reply_to.reply_to_msg_id\n            if isinstance(self.reply_to, types.MessageReplyHeader)\n            else None\n        )\n\n    @property\n    def to_id(self):\n        \"\"\"\n        Returns the peer to which this message was sent to. This used to exist\n        to infer the ``.peer_id``.\n        \"\"\"\n        # If the client wasn't set we can't emulate the behaviour correctly,\n        # so as a best-effort simply return the chat peer.\n        if self._client and not self.out and self.is_private:\n            return types.PeerUser(self._client._self_id)\n\n        return self.peer_id\n\n    # endregion Public Properties\n\n    # region Public Methods\n\n    def get_entities_text(self, cls=None):\n        \"\"\"\n        Returns a list of ``(markup entity, inner text)``\n        (like bold or italics).\n\n        The markup entity is a :tl:`MessageEntity` that represents bold,\n        italics, etc., and the inner text is the `str` inside that markup\n        entity.\n\n        For example:\n\n        .. code-block:: python\n\n            print(repr(message.text))  # shows: 'Hello **world**!'\n\n            for ent, txt in message.get_entities_text():\n                print(ent)  # shows: MessageEntityBold(offset=6, length=5)\n                print(txt)  # shows: world\n\n        Args:\n            cls (`type`):\n                Returns entities matching this type only. For example,\n                the following will print the text for all ``code`` entities:\n\n                >>> from telethon.tl.types import MessageEntityCode\n                >>>\n                >>> m = ...  # get the message\n                >>> for _, inner_text in m.get_entities_text(MessageEntityCode):\n                >>>     print(inner_text)\n        \"\"\"\n        ent = self.entities\n        if not ent:\n            return []\n\n        if cls:\n            ent = [c for c in ent if isinstance(c, cls)]\n\n        texts = utils.get_inner_text(self.message, ent)\n        return list(zip(ent, texts))\n\n    async def get_reply_message(self):\n        \"\"\"\n        The `Message` that this message is replying to, or `None`.\n\n        The result will be cached after its first use.\n        \"\"\"\n        if self._reply_message is None and self._client:\n            if not isinstance(self.reply_to, types.MessageReplyHeader):\n                return None\n\n            # Bots cannot access other bots' messages by their ID.\n            # However they can access them through replies...\n            self._reply_message = await self._client.get_messages(\n                await self.get_input_chat() if self.is_channel else None,\n                ids=types.InputMessageReplyTo(self.id)\n            )\n            if not self._reply_message:\n                # ...unless the current message got deleted.\n                #\n                # If that's the case, give it a second chance accessing\n                # directly by its ID.\n                self._reply_message = await self._client.get_messages(\n                    self._input_chat if self.is_channel else None,\n                    ids=self.reply_to.reply_to_msg_id\n                )\n\n        return self._reply_message\n\n    async def respond(self, *args, **kwargs):\n        \"\"\"\n        Responds to the message (not as a reply). Shorthand for\n        `telethon.client.messages.MessageMethods.send_message`\n        with ``entity`` already set.\n        \"\"\"\n        if self._client:\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n    async def reply(self, *args, **kwargs):\n        \"\"\"\n        Replies to the message (as a reply). Shorthand for\n        `telethon.client.messages.MessageMethods.send_message`\n        with both ``entity`` and ``reply_to`` already set.\n        \"\"\"\n        if self._client:\n            kwargs['reply_to'] = self.id\n            return await self._client.send_message(\n                await self.get_input_chat(), *args, **kwargs)\n\n    async def forward_to(self, *args, **kwargs):\n        \"\"\"\n        Forwards the message. Shorthand for\n        `telethon.client.messages.MessageMethods.forward_messages`\n        with both ``messages`` and ``from_peer`` already set.\n\n        If you need to forward more than one message at once, don't use\n        this `forward_to` method. Use a\n        `telethon.client.telegramclient.TelegramClient` instance directly.\n        \"\"\"\n        if self._client:\n            kwargs['messages'] = self.id\n            kwargs['from_peer'] = await self.get_input_chat()\n            return await self._client.forward_messages(*args, **kwargs)\n\n    async def edit(self, *args, **kwargs):\n        \"\"\"\n        Edits the message if it's outgoing. Shorthand for\n        `telethon.client.messages.MessageMethods.edit_message`\n        with both ``entity`` and ``message`` already set.\n\n        Returns\n            The edited `Message <telethon.tl.custom.message.Message>`,\n            unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which\n            case this method returns a boolean.\n\n        Raises\n            ``MessageAuthorRequiredError`` if you're not the author of the\n            message but tried editing it anyway.\n\n            ``MessageNotModifiedError`` if the contents of the message were\n            not modified at all.\n\n            ``MessageIdInvalidError`` if the ID of the message is invalid\n            (the ID itself may be correct, but the message with that ID\n            cannot be edited). For example, when trying to edit messages\n            with a reply markup (or clear markup) this error will be raised.\n\n        .. note::\n\n            This is different from `client.edit_message\n            <telethon.client.messages.MessageMethods.edit_message>`\n            and **will respect** the previous state of the message.\n            For example, if the message didn't have a link preview,\n            the edit won't add one by default, and you should force\n            it by setting it to `True` if you want it.\n\n            This is generally the most desired and convenient behaviour,\n            and will work for link previews and message buttons.\n        \"\"\"\n        if 'link_preview' not in kwargs:\n            kwargs['link_preview'] = bool(self.web_preview)\n\n        if 'buttons' not in kwargs:\n            kwargs['buttons'] = self.reply_markup\n\n        return await self._client.edit_message(\n            await self.get_input_chat(), self.id,\n            *args, **kwargs\n        )\n\n    async def delete(self, *args, **kwargs):\n        \"\"\"\n        Deletes the message. You're responsible for checking whether you\n        have the permission to do so, or to except the error otherwise.\n        Shorthand for\n        `telethon.client.messages.MessageMethods.delete_messages` with\n        ``entity`` and ``message_ids`` already set.\n\n        If you need to delete more than one message at once, don't use\n        this `delete` method. Use a\n        `telethon.client.telegramclient.TelegramClient` instance directly.\n        \"\"\"\n        if self._client:\n            return await self._client.delete_messages(\n                await self.get_input_chat(), [self.id],\n                *args, **kwargs\n            )\n\n    async def download_media(self, *args, **kwargs):\n        \"\"\"\n        Downloads the media contained in the message, if any. Shorthand\n        for `telethon.client.downloads.DownloadMethods.download_media`\n        with the ``message`` already set.\n        \"\"\"\n        if self._client:\n            # Passing the entire message is important, in case it has to be\n            # refetched for a fresh file reference.\n            return await self._client.download_media(self, *args, **kwargs)\n\n    async def click(self, i=None, j=None,\n                    *, text=None, filter=None, data=None, share_phone=None,\n                    share_geo=None, password=None, open_url=None):\n        \"\"\"\n        Calls :tl:`SendVote` with the specified poll option\n        or `button.click <telethon.tl.custom.messagebutton.MessageButton.click>`\n        on the specified button.\n\n        Does nothing if the message is not a poll or has no buttons.\n\n        Args:\n            i (`int` | `list`):\n                Clicks the i'th button or poll option (starting from the index 0).\n                For multiple-choice polls, a list with the indices should be used.\n                Will ``raise IndexError`` if out of bounds. Example:\n\n                >>> message = ...  # get the message somehow\n                >>> # Clicking the 3rd button\n                >>> # [button1] [button2]\n                >>> # [     button3     ]\n                >>> # [button4] [button5]\n                >>> await message.click(2)  # index\n\n            j (`int`):\n                Clicks the button at position (i, j), these being the\n                indices for the (row, column) respectively. Example:\n\n                >>> # Clicking the 2nd button on the 1st row.\n                >>> # [button1] [button2]\n                >>> # [     button3     ]\n                >>> # [button4] [button5]\n                >>> await message.click(0, 1)  # (row, column)\n\n                This is equivalent to ``message.buttons[0][1].click()``.\n\n            text (`str` | `callable`):\n                Clicks the first button or poll option with the text \"text\". This may\n                also be a callable, like a ``re.compile(...).match``,\n                and the text will be passed to it.\n\n                If you need to select multiple options in a poll,\n                pass a list of indices to the ``i`` parameter.\n\n            filter (`callable`):\n                Clicks the first button or poll option for which the callable\n                returns `True`. The callable should accept a single\n                `MessageButton <telethon.tl.custom.messagebutton.MessageButton>`\n                or `PollAnswer <telethon.tl.types.PollAnswer>` argument.\n\n                If you need to select multiple options in a poll,\n                pass a list of indices to the ``i`` parameter.\n\n            data (`bytes`):\n                This argument overrides the rest and will not search any\n                buttons. Instead, it will directly send the request to\n                behave as if it clicked a button with said data. Note\n                that if the message does not have this data, it will\n                ``raise DataInvalidError``.\n\n            share_phone (`bool` | `str` | tl:`InputMediaContact`):\n                When clicking on a keyboard button requesting a phone number\n                (:tl:`KeyboardButtonRequestPhone`), this argument must be\n                explicitly set to avoid accidentally sharing the number.\n\n                It can be `True` to automatically share the current user's\n                phone, a string to share a specific phone number, or a contact\n                media to specify all details.\n\n                If the button is pressed without this, `ValueError` is raised.\n\n            share_geo (`tuple` | `list` | tl:`InputMediaGeoPoint`):\n                When clicking on a keyboard button requesting a geo location\n                (:tl:`KeyboardButtonRequestGeoLocation`), this argument must\n                be explicitly set to avoid accidentally sharing the location.\n\n                It must be a `tuple` of `float` as ``(longitude, latitude)``,\n                or a :tl:`InputGeoPoint` instance to avoid accidentally using\n                the wrong roder.\n\n                If the button is pressed without this, `ValueError` is raised.\n\n            password (`str`):\n                When clicking certain buttons (such as BotFather's confirmation\n                button to transfer ownership), if your account has 2FA enabled,\n                you need to provide your account's password. Otherwise,\n                `teltehon.errors.PasswordHashInvalidError` is raised.\n            \n            open_url (`bool`):\n                When clicking on an inline keyboard URL button :tl:`KeyboardButtonUrl`\n                By default it will return URL of the button, passing ``click(open_url=True)``\n                will lunch the default browser with given URL of the button and \n                return `True` on success.\n                \n            Example:\n\n                .. code-block:: python\n\n                    # Click the first button\n                    await message.click(0)\n\n                    # Click some row/column\n                    await message.click(row, column)\n\n                    # Click by text\n                    await message.click(text='👍')\n\n                    # Click by data\n                    await message.click(data=b'payload')\n\n                    # Click on a button requesting a phone\n                    await message.click(0, share_phone=True)\n        \"\"\"\n        if not self._client:\n            return\n\n        if data:\n            chat = await self.get_input_chat()\n            if not chat:\n                return None\n\n            but = types.KeyboardButtonCallback('', data)\n            return await MessageButton(self._client, but, chat, None, self.id).click(\n                share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)\n\n        if sum(int(x is not None) for x in (i, text, filter)) >= 2:\n            raise ValueError('You can only set either of i, text or filter')\n\n        # Finding the desired poll options and sending them\n        if self.poll is not None:\n            def find_options():\n                answers = self.poll.poll.answers\n                if i is not None:\n                    if utils.is_list_like(i):\n                        return [answers[idx].option for idx in i]\n                    return [answers[i].option]\n                if text is not None:\n                    if callable(text):\n                        for answer in answers:\n                            if text(answer.text):\n                                return [answer.option]\n                    else:\n                        for answer in answers:\n                            if answer.text == text:\n                                return [answer.option]\n                    return\n\n                if filter is not None:\n                    for answer in answers:\n                        if filter(answer):\n                            return [answer.option]\n                    return\n\n            options = find_options()\n            if options is None:\n                options = []\n            return await self._client(\n                functions.messages.SendVoteRequest(\n                    peer=self._input_chat,\n                    msg_id=self.id,\n                    options=options\n                )\n            )\n\n        if not await self.get_buttons():\n            return  # Accessing the property sets self._buttons[_flat]\n\n        def find_button():\n            nonlocal i\n            if text is not None:\n                if callable(text):\n                    for button in self._buttons_flat:\n                        if text(button.text):\n                            return button\n                else:\n                    for button in self._buttons_flat:\n                        if button.text == text:\n                            return button\n                return\n\n            if filter is not None:\n                for button in self._buttons_flat:\n                    if filter(button):\n                        return button\n                return\n\n            if i is None:\n                i = 0\n            if j is None:\n                return self._buttons_flat[i]\n            else:\n                return self._buttons[i][j]\n\n        button = find_button()\n        if button:\n            return await button.click(\n                share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)\n\n    async def mark_read(self):\n        \"\"\"\n        Marks the message as read. Shorthand for\n        `client.send_read_acknowledge()\n        <telethon.client.messages.MessageMethods.send_read_acknowledge>`\n        with both ``entity`` and ``message`` already set.\n        \"\"\"\n        if self._client:\n            await self._client.send_read_acknowledge(\n                await self.get_input_chat(), max_id=self.id)\n\n    async def pin(self, *, notify=False, pm_oneside=False):\n        \"\"\"\n        Pins the message. Shorthand for\n        `telethon.client.messages.MessageMethods.pin_message`\n        with both ``entity`` and ``message`` already set.\n        \"\"\"\n        # TODO Constantly checking if client is a bit annoying,\n        #      maybe just make it illegal to call messages from raw API?\n        #      That or figure out a way to always set it directly.\n        if self._client:\n            return await self._client.pin_message(\n                await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside)\n\n    async def unpin(self):\n        \"\"\"\n        Unpins the message. Shorthand for\n        `telethon.client.messages.MessageMethods.unpin_message`\n        with both ``entity`` and ``message`` already set.\n        \"\"\"\n        if self._client:\n            return await self._client.unpin_message(\n                await self.get_input_chat(), self.id)\n\n    # endregion Public Methods\n\n    # region Private Methods\n\n    async def _reload_message(self):\n        \"\"\"\n        Re-fetches this message to reload the sender and chat entities,\n        along with their input versions.\n        \"\"\"\n        if not self._client:\n            return\n\n        try:\n            chat = await self.get_input_chat() if self.is_channel else None\n            msg = await self._client.get_messages(chat, ids=self.id)\n        except ValueError:\n            return  # We may not have the input chat/get message failed\n        if not msg:\n            return  # The message may be deleted and it will be None\n\n        self._sender = msg._sender\n        self._input_sender = msg._input_sender\n        self._chat = msg._chat\n        self._input_chat = msg._input_chat\n        self._via_bot = msg._via_bot\n        self._via_input_bot = msg._via_input_bot\n        self._forward = msg._forward\n        self._action_entities = msg._action_entities\n\n    async def _refetch_sender(self):\n        await self._reload_message()\n\n    def _set_buttons(self, chat, bot):\n        \"\"\"\n        Helper methods to set the buttons given the input sender and chat.\n        \"\"\"\n        if self._client and isinstance(self.reply_markup, (\n                types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)):\n            self._buttons = [[\n                MessageButton(self._client, button, chat, bot, self.id)\n                for button in row.buttons\n            ] for row in self.reply_markup.rows]\n            self._buttons_flat = [x for row in self._buttons for x in row]\n\n    def _needed_markup_bot(self):\n        \"\"\"\n        Returns the input peer of the bot that's needed for the reply markup.\n\n        This is necessary for :tl:`KeyboardButtonSwitchInline` since we need\n        to know what bot we want to start. Raises ``ValueError`` if the bot\n        cannot be found but is needed. Returns `None` if it's not needed.\n        \"\"\"\n        if self._client and not isinstance(self.reply_markup, (\n                types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)):\n            return None\n\n        for row in self.reply_markup.rows:\n            for button in row.buttons:\n                if isinstance(button, types.KeyboardButtonSwitchInline):\n                    # no via_bot_id means the bot sent the message itself (#1619)\n                    if button.same_peer or not self.via_bot_id:\n                        bot = self.input_sender\n                        if not bot:\n                            raise ValueError('No input sender')\n                        return bot\n                    else:\n                        try:\n                            return self._client._mb_entity_cache.get(\n                                utils.resolve_id(self.via_bot_id)[0])._as_input_peer()\n                        except AttributeError:\n                            raise ValueError('No input sender') from None\n\n    def _document_by_attribute(self, kind, condition=None):\n        \"\"\"\n        Helper method to return the document only if it has an attribute\n        that's an instance of the given kind, and passes the condition.\n        \"\"\"\n        doc = self.document\n        if doc:\n            for attr in doc.attributes:\n                if isinstance(attr, kind):\n                    if not condition or condition(attr):\n                        return doc\n                    return None\n\n    # endregion Private Methods\n"
  },
  {
    "path": "telethon/tl/custom/messagebutton.py",
    "content": "from .. import types, functions\nfrom ... import password as pwd_mod\nfrom ...errors import BotResponseTimeoutError\ntry:\n    import webbrowser\nexcept ModuleNotFoundError:\n    pass\nimport sys\nimport os\n\n\nclass MessageButton:\n    \"\"\"\n    .. note::\n\n        `Message.buttons <telethon.tl.custom.message.Message.buttons>`\n        are instances of this type. If you want to **define** a reply\n        markup for e.g. sending messages, refer to `Button\n        <telethon.tl.custom.button.Button>` instead.\n\n    Custom class that encapsulates a message button providing\n    an abstraction to easily access some commonly needed features\n    (such as clicking the button itself).\n\n    Attributes:\n\n        button (:tl:`KeyboardButton`):\n            The original :tl:`KeyboardButton` object.\n    \"\"\"\n    def __init__(self, client, original, chat, bot, msg_id):\n        self.button = original\n        self._bot = bot\n        self._chat = chat\n        self._msg_id = msg_id\n        self._client = client\n\n    @property\n    def client(self):\n        \"\"\"\n        Returns the `telethon.client.telegramclient.TelegramClient`\n        instance that created this instance.\n        \"\"\"\n        return self._client\n\n    @property\n    def text(self):\n        \"\"\"The text string of the button.\"\"\"\n        return self.button.text\n\n    @property\n    def data(self):\n        \"\"\"The `bytes` data for :tl:`KeyboardButtonCallback` objects.\"\"\"\n        if isinstance(self.button, types.KeyboardButtonCallback):\n            return self.button.data\n\n    @property\n    def inline_query(self):\n        \"\"\"The query `str` for :tl:`KeyboardButtonSwitchInline` objects.\"\"\"\n        if isinstance(self.button, types.KeyboardButtonSwitchInline):\n            return self.button.query\n\n    @property\n    def url(self):\n        \"\"\"The url `str` for :tl:`KeyboardButtonUrl` objects.\"\"\"\n        if isinstance(self.button, types.KeyboardButtonUrl):\n            return self.button.url\n\n    async def click(self, share_phone=None, share_geo=None, *, password=None, open_url=None):\n        \"\"\"\n        Emulates the behaviour of clicking this button.\n\n        If it's a normal :tl:`KeyboardButton` with text, a message will be\n        sent, and the sent `Message <telethon.tl.custom.message.Message>` returned.\n\n        If it's an inline :tl:`KeyboardButtonCallback` with text and data,\n        it will be \"clicked\" and the :tl:`BotCallbackAnswer` returned.\n\n        If it's an inline :tl:`KeyboardButtonSwitchInline` button, the\n        :tl:`StartBotRequest` will be invoked and the resulting updates\n        returned.\n\n        If it's a :tl:`KeyboardButtonUrl`, the ``URL`` of the button will\n        be returned. If you pass ``open_url=True`` the URL of the button will\n        be passed to ``webbrowser.open`` and return `True` on success.\n\n        If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you\n        want to ``share_phone=True`` in order to share it. Sharing it is not a\n        default because it is a privacy concern and could happen accidentally.\n\n        You may also use ``share_phone=phone`` to share a specific number, in\n        which case either `str` or :tl:`InputMediaContact` should be used.\n\n        If it's a :tl:`KeyboardButtonRequestGeoLocation`, you must pass a\n        tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems\n        to have some heuristics to determine impossible locations, so changing\n        this value a lot quickly may not work as expected. You may also pass a\n        :tl:`InputGeoPoint` if you find the order confusing.\n        \"\"\"\n        if isinstance(self.button, types.KeyboardButton):\n            return await self._client.send_message(\n                self._chat, self.button.text, parse_mode=None)\n        elif isinstance(self.button, types.KeyboardButtonCallback):\n            if password is not None:\n                pwd = await self._client(functions.account.GetPasswordRequest())\n                password = pwd_mod.compute_check(pwd, password)\n\n            req = functions.messages.GetBotCallbackAnswerRequest(\n                peer=self._chat, msg_id=self._msg_id, data=self.button.data,\n                password=password\n            )\n            try:\n                return await self._client(req)\n            except BotResponseTimeoutError:\n                return None\n        elif isinstance(self.button, types.KeyboardButtonSwitchInline):\n            return await self._client(functions.messages.StartBotRequest(\n                bot=self._bot, peer=self._chat, start_param=self.button.query\n            ))\n        elif isinstance(self.button, types.KeyboardButtonUrl):\n            if open_url:\n                if \"webbrowser\" in sys.modules:\n                    return webbrowser.open(self.button.url)\n            return self.button.url\n        elif isinstance(self.button, types.KeyboardButtonGame):\n            req = functions.messages.GetBotCallbackAnswerRequest(\n                peer=self._chat, msg_id=self._msg_id, game=True\n            )\n            try:\n                return await self._client(req)\n            except BotResponseTimeoutError:\n                return None\n        elif isinstance(self.button, types.KeyboardButtonRequestPhone):\n            if not share_phone:\n                raise ValueError('cannot click on phone buttons unless share_phone=True')\n\n            if share_phone == True or isinstance(share_phone, str):\n                me = await self._client.get_me()\n                share_phone = types.InputMediaContact(\n                    phone_number=me.phone if share_phone == True else share_phone,\n                    first_name=me.first_name or '',\n                    last_name=me.last_name or '',\n                    vcard=''\n                )\n\n            return await self._client.send_file(self._chat, share_phone)\n        elif isinstance(self.button, types.KeyboardButtonRequestGeoLocation):\n            if not share_geo:\n                raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)')\n\n            if isinstance(share_geo, (tuple, list)):\n                long, lat = share_geo\n                share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long))\n\n            return await self._client.send_file(self._chat, share_geo)\n"
  },
  {
    "path": "telethon/tl/custom/participantpermissions.py",
    "content": "from .. import types\n\n\ndef _admin_prop(field_name, doc):\n    \"\"\"\n    Helper method to build properties that return `True` if the user is an\n    administrator of a normal chat, or otherwise return `True` if the user\n    has a specific permission being an admin of a channel.\n    \"\"\"\n    def fget(self):\n        if not self.is_admin:\n            return False\n        if self.is_chat:\n            return True\n\n        return getattr(self.participant.admin_rights, field_name)\n\n    return {'fget': fget, 'doc': doc}\n\n\nclass ParticipantPermissions:\n    \"\"\"\n    Participant permissions information.\n\n    The properties in this objects are boolean values indicating whether the\n    user has the permission or not.\n\n    Example\n        .. code-block:: python\n\n            permissions = ...\n\n            if permissions.is_banned:\n                \"this user is banned\"\n            elif permissions.is_admin:\n                \"this user is an administrator\"\n    \"\"\"\n    def __init__(self, participant, chat: bool):\n        self.participant = participant\n        self.is_chat = chat\n\n    @property\n    def is_admin(self):\n        \"\"\"\n        Whether the user is an administrator of the chat or not. The creator\n        also counts as begin an administrator, since they have all permissions.\n        \"\"\"\n        return self.is_creator or isinstance(self.participant, (\n            types.ChannelParticipantAdmin,\n            types.ChatParticipantAdmin\n        ))\n\n    @property\n    def is_creator(self):\n        \"\"\"\n        Whether the user is the creator of the chat or not.\n        \"\"\"\n        return isinstance(self.participant, (\n            types.ChannelParticipantCreator,\n            types.ChatParticipantCreator\n        ))\n\n    @property\n    def has_default_permissions(self):\n        \"\"\"\n        Whether the user is a normal user of the chat (not administrator, but\n        not banned either, and has no restrictions applied).\n        \"\"\"\n        return isinstance(self.participant, (\n            types.ChannelParticipant,\n            types.ChatParticipant,\n            types.ChannelParticipantSelf\n        ))\n\n    @property\n    def is_banned(self):\n        \"\"\"\n        Whether the user is banned in the chat.\n        \"\"\"\n        return isinstance(self.participant, types.ChannelParticipantBanned)\n\n    @property\n    def has_left(self):\n        \"\"\"\n        Whether the user left the chat.\n        \"\"\"\n        return isinstance(self.participant, types.ChannelParticipantLeft)\n    \n    @property\n    def add_admins(self):\n        \"\"\"\n        Whether the administrator can add new administrators with the same or\n        less permissions than them.\n        \"\"\"\n        if not self.is_admin:\n            return False\n\n        if self.is_chat:\n            return self.is_creator\n\n        return self.participant.admin_rights.add_admins\n\n    ban_users = property(**_admin_prop('ban_users', \"\"\"\n        Whether the administrator can ban other users or not.\n    \"\"\"))\n\n    pin_messages = property(**_admin_prop('pin_messages', \"\"\"\n        Whether the administrator can pin messages or not.\n    \"\"\"))\n\n    invite_users = property(**_admin_prop('invite_users', \"\"\"\n        Whether the administrator can add new users to the chat.\n    \"\"\"))\n\n    delete_messages = property(**_admin_prop('delete_messages', \"\"\"\n        Whether the administrator can delete messages from other participants.\n    \"\"\"))\n\n    edit_messages = property(**_admin_prop('edit_messages', \"\"\"\n        Whether the administrator can edit messages.\n    \"\"\"))\n\n    post_messages = property(**_admin_prop('post_messages', \"\"\"\n        Whether the administrator can post messages in the broadcast channel.\n    \"\"\"))\n\n    change_info = property(**_admin_prop('change_info', \"\"\"\n        Whether the administrator can change the information about the chat,\n        such as title or description.\n    \"\"\"))\n\n    anonymous = property(**_admin_prop('anonymous', \"\"\"\n        Whether the administrator will remain anonymous when sending messages.\n    \"\"\"))\n    \n    manage_call = property(**_admin_prop('manage_call', \"\"\"\n        Whether the user will be able to manage group calls.\n    \"\"\"))\n"
  },
  {
    "path": "telethon/tl/custom/qrlogin.py",
    "content": "import asyncio\nimport base64\nimport datetime\n\nfrom .. import types, functions\nfrom ... import events\n\n\nclass QRLogin:\n    \"\"\"\n    QR login information.\n\n    Most of the time, you will present the `url` as a QR code to the user,\n    and while it's being shown, call `wait`.\n    \"\"\"\n    def __init__(self, client, ignored_ids):\n        self._client = client\n        self._request = functions.auth.ExportLoginTokenRequest(\n            self._client.api_id, self._client.api_hash, ignored_ids)\n        self._resp = None\n\n    async def recreate(self):\n        \"\"\"\n        Generates a new token and URL for a new QR code, useful if the code\n        has expired before it was imported.\n        \"\"\"\n        self._resp = await self._client(self._request)\n\n    @property\n    def token(self) -> bytes:\n        \"\"\"\n        The binary data representing the token.\n\n        It can be used by a previously-authorized client in a call to\n        :tl:`auth.importLoginToken` to log the client that originally\n        requested the QR login.\n        \"\"\"\n        return self._resp.token\n\n    @property\n    def url(self) -> str:\n        \"\"\"\n        The ``tg://login`` URI with the token. When opened by a Telegram\n        application where the user is logged in, it will import the login\n        token.\n\n        If you want to display a QR code to the user, this is the URL that\n        should be launched when the QR code is scanned (the URL that should\n        be contained in the QR code image you generate).\n\n        Whether you generate the QR code image or not is up to you, and the\n        library can't do this for you due to the vast ways of generating and\n        displaying the QR code that exist.\n\n        The URL simply consists of `token` base64-encoded.\n        \"\"\"\n        return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('='))\n\n    @property\n    def expires(self) -> datetime.datetime:\n        \"\"\"\n        The `datetime` at which the QR code will expire.\n\n        If you want to try again, you will need to call `recreate`.\n        \"\"\"\n        return self._resp.expires\n\n    async def wait(self, timeout: float = None):\n        \"\"\"\n        Waits for the token to be imported by a previously-authorized client,\n        either by scanning the QR, launching the URL directly, or calling the\n        import method.\n\n        This method **must** be called before the QR code is scanned, and\n        must be executing while the QR code is being scanned. Otherwise, the\n        login will not complete.\n\n        Will raise `asyncio.TimeoutError` if the login doesn't complete on\n        time.\n\n        Arguments\n            timeout (float):\n                The timeout, in seconds, to wait before giving up. By default\n                the library will wait until the token expires, which is often\n                what you want.\n\n        Returns\n            On success, an instance of :tl:`User`. On failure it will raise.\n        \"\"\"\n        if timeout is None:\n            timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()\n\n        event = asyncio.Event()\n\n        async def handler(_update):\n            event.set()\n\n        self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken))\n\n        try:\n            # Will raise timeout error if it doesn't complete quick enough,\n            # which we want to let propagate\n            await asyncio.wait_for(event.wait(), timeout=timeout)\n        finally:\n            self._client.remove_event_handler(handler)\n\n        # We got here without it raising timeout error, so we can proceed\n        resp = await self._client(self._request)\n        if isinstance(resp, types.auth.LoginTokenMigrateTo):\n            await self._client._switch_dc(resp.dc_id)\n            resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token))\n            # resp should now be auth.loginTokenSuccess\n\n        if isinstance(resp, types.auth.LoginTokenSuccess):\n            user = resp.authorization.user\n            await self._client._on_login(user)\n            return user\n\n        raise TypeError('Login token response was unexpected: {}'.format(resp))\n"
  },
  {
    "path": "telethon/tl/custom/sendergetter.py",
    "content": "import abc\n\nfrom ... import utils\n\n\nclass SenderGetter(abc.ABC):\n    \"\"\"\n    Helper base class that introduces the `sender`, `input_sender`\n    and `sender_id` properties and `get_sender` and `get_input_sender`\n    methods.\n    \"\"\"\n    def __init__(self, sender_id=None, *, sender=None, input_sender=None):\n        self._sender_id = sender_id\n        self._sender = sender\n        self._input_sender = input_sender\n        self._client = None\n\n    @property\n    def sender(self):\n        \"\"\"\n        Returns the :tl:`User` or :tl:`Channel` that sent this object.\n        It may be `None` if Telegram didn't send the sender.\n\n        If you only need the ID, use `sender_id` instead.\n\n        If you need to call a method which needs\n        this chat, use `input_sender` instead.\n\n        If you're using `telethon.events`, use `get_sender()` instead.\n        \"\"\"\n        return self._sender\n\n    async def get_sender(self):\n        \"\"\"\n        Returns `sender`, but will make an API call to find the\n        sender unless it's already cached.\n\n        If you only need the ID, use `sender_id` instead.\n\n        If you need to call a method which needs\n        this sender, use `get_input_sender()` instead.\n        \"\"\"\n        # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`.\n        # It's a flag that will be set if only minimal information is\n        # available (such as display name, but username may be missing),\n        # in which case we want to force fetch the entire thing because\n        # the user explicitly called a method. If the user is okay with\n        # cached information, they may use the property instead.\n        if (self._sender is None or getattr(self._sender, 'min', None)) \\\n                and await self.get_input_sender():\n            # self.get_input_sender may refresh in which case the sender may no longer be min\n            # However it could still incur a cost so the cheap check is done twice instead.\n            if self._sender is None or getattr(self._sender, 'min', None):\n                try:\n                    self._sender =\\\n                        await self._client.get_entity(self._input_sender)\n                except ValueError:\n                    await self._refetch_sender()\n        return self._sender\n\n    @property\n    def input_sender(self):\n        \"\"\"\n        This :tl:`InputPeer` is the input version of the user/channel who\n        sent the message. Similarly to `input_chat\n        <telethon.tl.custom.chatgetter.ChatGetter.input_chat>`, this doesn't\n        have things like username or similar, but still useful in some cases.\n\n        Note that this might not be available if the library can't\n        find the input chat, or if the message a broadcast on a channel.\n        \"\"\"\n        if self._input_sender is None and self._sender_id and self._client:\n            try:\n                self._input_sender = self._client._mb_entity_cache.get(\n                        utils.resolve_id(self._sender_id)[0])._as_input_peer()\n            except AttributeError:\n                pass\n        return self._input_sender\n\n    async def get_input_sender(self):\n        \"\"\"\n        Returns `input_sender`, but will make an API call to find the\n        input sender unless it's already cached.\n        \"\"\"\n        if self.input_sender is None and self._sender_id and self._client:\n            await self._refetch_sender()\n        return self._input_sender\n\n    @property\n    def sender_id(self):\n        \"\"\"\n        Returns the marked sender integer ID, if present.\n\n        If there is a sender in the object, `sender_id` will *always* be set,\n        which is why you should use it instead of `sender.id <sender>`.\n        \"\"\"\n        return self._sender_id\n\n    async def _refetch_sender(self):\n        \"\"\"\n        Re-fetches sender information through other means.\n        \"\"\"\n"
  },
  {
    "path": "telethon/tl/patched/__init__.py",
    "content": "from .. import types, alltlobjects\nfrom ..custom.message import Message as _Message\n\nclass MessageEmpty(_Message, types.MessageEmpty):\n    pass\n\ntypes.MessageEmpty = MessageEmpty\nalltlobjects.tlobjects[MessageEmpty.CONSTRUCTOR_ID] = MessageEmpty\n\nclass MessageService(_Message, types.MessageService):\n    pass\n\ntypes.MessageService = MessageService\nalltlobjects.tlobjects[MessageService.CONSTRUCTOR_ID] = MessageService\n\nclass Message(_Message, types.Message):\n    pass\n\ntypes.Message = Message\nalltlobjects.tlobjects[Message.CONSTRUCTOR_ID] = Message\n"
  },
  {
    "path": "telethon/tl/tlobject.py",
    "content": "import base64\nimport json\nimport struct\nfrom datetime import datetime, date, timedelta, timezone\nimport time\n\n_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6])\n_EPOCH_NAIVE_LOCAL = datetime(*time.localtime(0)[:6])\n_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc)\n\n\ndef _datetime_to_timestamp(dt):\n    # If no timezone is specified, it is assumed to be in utc zone\n    if dt.tzinfo is None:\n        dt = dt.replace(tzinfo=timezone.utc)\n    # We use .total_seconds() method instead of simply dt.timestamp(), \n    # because on Windows the latter raises OSError on datetimes ~< datetime(1970,1,1)\n    secs = int((dt - _EPOCH).total_seconds())\n    # Make sure it's a valid signed 32 bit integer, as used by Telegram.\n    # This does make very large dates wrap around, but it's the best we\n    # can do with Telegram's limitations.\n    return struct.unpack('i', struct.pack('I', secs & 0xffffffff))[0]\n\n\ndef _json_default(value):\n    if isinstance(value, bytes):\n        return base64.b64encode(value).decode('ascii')\n    elif isinstance(value, datetime):\n        return value.isoformat()\n    else:\n        return repr(value)\n\n\nclass TLObject:\n    CONSTRUCTOR_ID = None\n    SUBCLASS_OF_ID = None\n\n    @staticmethod\n    def pretty_format(obj, indent=None):\n        \"\"\"\n        Pretty formats the given object as a string which is returned.\n        If indent is None, a single line will be returned.\n        \"\"\"\n        if indent is None:\n            if isinstance(obj, TLObject):\n                obj = obj.to_dict()\n\n            if isinstance(obj, dict):\n                return '{}({})'.format(obj.get('_', 'dict'), ', '.join(\n                    '{}={}'.format(k, TLObject.pretty_format(v))\n                    for k, v in obj.items() if k != '_'\n                ))\n            elif isinstance(obj, str) or isinstance(obj, bytes):\n                return repr(obj)\n            elif hasattr(obj, '__iter__'):\n                return '[{}]'.format(\n                    ', '.join(TLObject.pretty_format(x) for x in obj)\n                )\n            else:\n                return repr(obj)\n        else:\n            result = []\n            if isinstance(obj, TLObject):\n                obj = obj.to_dict()\n\n            if isinstance(obj, dict):\n                result.append(obj.get('_', 'dict'))\n                result.append('(')\n                if obj:\n                    result.append('\\n')\n                    indent += 1\n                    for k, v in obj.items():\n                        if k == '_':\n                            continue\n                        result.append('\\t' * indent)\n                        result.append(k)\n                        result.append('=')\n                        result.append(TLObject.pretty_format(v, indent))\n                        result.append(',\\n')\n                    result.pop()  # last ',\\n'\n                    indent -= 1\n                    result.append('\\n')\n                    result.append('\\t' * indent)\n                result.append(')')\n\n            elif isinstance(obj, str) or isinstance(obj, bytes):\n                result.append(repr(obj))\n\n            elif hasattr(obj, '__iter__'):\n                result.append('[\\n')\n                indent += 1\n                for x in obj:\n                    result.append('\\t' * indent)\n                    result.append(TLObject.pretty_format(x, indent))\n                    result.append(',\\n')\n                indent -= 1\n                result.append('\\t' * indent)\n                result.append(']')\n\n            else:\n                result.append(repr(obj))\n\n            return ''.join(result)\n\n    @staticmethod\n    def serialize_bytes(data):\n        \"\"\"Write bytes by using Telegram guidelines\"\"\"\n        if not isinstance(data, bytes):\n            if isinstance(data, str):\n                data = data.encode('utf-8')\n            else:\n                raise TypeError(\n                    'bytes or str expected, not {}'.format(type(data)))\n\n        r = []\n        if len(data) < 254:\n            padding = (len(data) + 1) % 4\n            if padding != 0:\n                padding = 4 - padding\n\n            r.append(bytes([len(data)]))\n            r.append(data)\n\n        else:\n            padding = len(data) % 4\n            if padding != 0:\n                padding = 4 - padding\n\n            r.append(bytes([\n                254,\n                len(data) % 256,\n                (len(data) >> 8) % 256,\n                (len(data) >> 16) % 256\n            ]))\n            r.append(data)\n\n        r.append(bytes(padding))\n        return b''.join(r)\n\n    @staticmethod\n    def serialize_datetime(dt):\n        if not dt and not isinstance(dt, timedelta):\n            return b'\\0\\0\\0\\0'\n\n        if isinstance(dt, datetime):\n            dt = _datetime_to_timestamp(dt)\n        elif isinstance(dt, date):\n            dt = _datetime_to_timestamp(datetime(dt.year, dt.month, dt.day))\n        elif isinstance(dt, float):\n            dt = int(dt)\n        elif isinstance(dt, timedelta):\n            # Timezones are tricky. datetime.utcnow() + ... timestamp() works\n            dt = _datetime_to_timestamp(datetime.utcnow() + dt)\n\n        if isinstance(dt, int):\n            return struct.pack('<i', dt)\n\n        raise TypeError('Cannot interpret \"{}\" as a date.'.format(dt))\n\n    def __eq__(self, o):\n        return isinstance(o, type(self)) and self.to_dict() == o.to_dict()\n\n    def __ne__(self, o):\n        return not isinstance(o, type(self)) or self.to_dict() != o.to_dict()\n\n    def __str__(self):\n        return TLObject.pretty_format(self)\n\n    def stringify(self):\n        return TLObject.pretty_format(self, indent=0)\n\n    def to_dict(self):\n        raise NotImplementedError\n\n    def to_json(self, fp=None, default=_json_default, **kwargs):\n        \"\"\"\n        Represent the current `TLObject` as JSON.\n\n        If ``fp`` is given, the JSON will be dumped to said\n        file pointer, otherwise a JSON string will be returned.\n\n        Note that bytes and datetimes cannot be represented\n        in JSON, so if those are found, they will be base64\n        encoded and ISO-formatted, respectively, by default.\n        \"\"\"\n        d = self.to_dict()\n        if fp:\n            return json.dump(d, fp, default=default, **kwargs)\n        else:\n            return json.dumps(d, default=default, **kwargs)\n\n    def __bytes__(self):\n        try:\n            return self._bytes()\n        except AttributeError:\n            # If a type is wrong (e.g. expected `TLObject` but `int` was\n            # provided) it will try to access `._bytes()`, which will fail\n            # with `AttributeError`. This occurs in fact because the type\n            # was wrong, so raise the correct error type.\n            raise TypeError('a TLObject was expected but found something else')\n\n    # Custom objects will call `(...)._bytes()` and not `bytes(...)` so that\n    # if the wrong type is used (e.g. `int`) we won't try allocating a huge\n    # amount of data, which would cause a `MemoryError`.\n    def _bytes(self):\n        raise NotImplementedError\n\n    @classmethod\n    def from_reader(cls, reader):\n        raise NotImplementedError\n\n\nclass TLRequest(TLObject):\n    \"\"\"\n    Represents a content-related `TLObject` (a request that can be sent).\n    \"\"\"\n    @staticmethod\n    def read_result(reader):\n        return reader.tgread_object()\n\n    async def resolve(self, client, utils):\n        pass\n"
  },
  {
    "path": "telethon/types.py",
    "content": "from .tl.types import *\n"
  },
  {
    "path": "telethon/utils.py",
    "content": "\"\"\"\nUtilities for working with the Telegram API itself (such as handy methods\nto convert between an entity like a User, Chat, etc. into its Input version)\n\"\"\"\nimport base64\nimport binascii\nimport inspect\nimport io\nimport itertools\nimport logging\nimport math\nimport mimetypes\nimport os\nimport pathlib\nimport re\nimport struct\nimport warnings\nfrom collections import namedtuple\nfrom mimetypes import guess_extension\nfrom types import GeneratorType\n\nfrom .extensions import markdown, html\nfrom .helpers import add_surrogate, del_surrogate, strip_text\nfrom .tl import types\n\ntry:\n    import hachoir\n    import hachoir.metadata\n    import hachoir.parser\nexcept ImportError:\n    hachoir = None\n\n# Register some of the most common mime-types to avoid any issues.\n# See https://github.com/LonamiWebs/Telethon/issues/1096.\nmimetypes.add_type('image/png', '.png')\nmimetypes.add_type('image/jpeg', '.jpeg')\nmimetypes.add_type('image/webp', '.webp')\nmimetypes.add_type('image/gif', '.gif')\nmimetypes.add_type('image/bmp', '.bmp')\nmimetypes.add_type('image/x-tga', '.tga')\nmimetypes.add_type('image/tiff', '.tiff')\nmimetypes.add_type('image/vnd.adobe.photoshop', '.psd')\n\nmimetypes.add_type('video/mp4', '.mp4')\nmimetypes.add_type('video/quicktime', '.mov')\nmimetypes.add_type('video/avi', '.avi')\n\nmimetypes.add_type('audio/mpeg', '.mp3')\nmimetypes.add_type('audio/m4a', '.m4a')\nmimetypes.add_type('audio/aac', '.aac')\nmimetypes.add_type('audio/ogg', '.ogg')\nmimetypes.add_type('audio/flac', '.flac')\n\nmimetypes.add_type('application/x-tgsticker', '.tgs')\n\nUSERNAME_RE = re.compile(\n    r'@|(?:https?://)?(?:www\\.)?(?:telegram\\.(?:me|dog)|t\\.me)/(@|\\+|joinchat/)?'\n)\nTG_JOIN_RE = re.compile(\n    r'tg://(join)\\?invite='\n)\n\nVALID_USERNAME_RE = re.compile(\n    r'^[a-z](?:(?!__)\\w){1,30}[a-z\\d]$',\n    re.IGNORECASE\n)\n\n_FileInfo = namedtuple('FileInfo', 'dc_id location size')\n\n_log = logging.getLogger(__name__)\n\n\ndef chunks(iterable, size=100):\n    \"\"\"\n    Turns the given iterable into chunks of the specified size,\n    which is 100 by default since that's what Telegram uses the most.\n    \"\"\"\n    it = iter(iterable)\n    size -= 1\n    for head in it:\n        yield itertools.chain([head], itertools.islice(it, size))\n\n\ndef get_display_name(entity):\n    \"\"\"\n    Gets the display name for the given :tl:`User`,\n    :tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise.\n    \"\"\"\n    if isinstance(entity, types.User):\n        if entity.last_name and entity.first_name:\n            return '{} {}'.format(entity.first_name, entity.last_name)\n        elif entity.first_name:\n            return entity.first_name\n        elif entity.last_name:\n            return entity.last_name\n        else:\n            return ''\n\n    elif isinstance(entity, (\n            types.Chat, types.ChatForbidden, types.Channel, types.ChannelForbidden)):\n        return entity.title\n\n    return ''\n\n\ndef get_extension(media):\n    \"\"\"Gets the corresponding extension for any Telegram media.\"\"\"\n\n    # Photos are always compressed as .jpg by Telegram\n    try:\n        get_input_photo(media)\n        return '.jpg'\n    except TypeError:\n        # These cases are not handled by input photo because it can't\n        if isinstance(media, (types.UserProfilePhoto, types.ChatPhoto)):\n            return '.jpg'\n\n    # Documents will come with a mime type\n    if isinstance(media, types.MessageMediaDocument):\n        media = media.document\n    if isinstance(media, (\n            types.Document, types.WebDocument, types.WebDocumentNoProxy)):\n        if media.mime_type == 'application/octet-stream':\n            # Octet stream are just bytes, which have no default extension\n            return ''\n        else:\n            return guess_extension(media.mime_type) or ''\n\n    return ''\n\n\ndef _raise_cast_fail(entity, target):\n    raise TypeError('Cannot cast {} to any kind of {}.'.format(\n        type(entity).__name__, target))\n\n\ndef get_input_peer(entity, allow_self=True, check_hash=True):\n    \"\"\"\n    Gets the input peer for the given \"entity\" (user, chat or channel).\n\n    A ``TypeError`` is raised if the given entity isn't a supported type\n    or if ``check_hash is True`` but the entity's ``access_hash is None``\n    *or* the entity contains ``min`` information. In this case, the hash\n    cannot be used for general purposes, and thus is not returned to avoid\n    any issues which can derive from invalid access hashes.\n\n    Note that ``check_hash`` **is ignored** if an input peer is already\n    passed since in that case we assume the user knows what they're doing.\n    This is key to getting entities by explicitly passing ``hash = 0``.\n    \"\"\"\n    # NOTE: It is important that this method validates the access hashes,\n    #       because it is used when we *require* a valid general-purpose\n    #       access hash. This includes caching, which relies on this method.\n    #       Further, when resolving raw methods, they do e.g.,\n    #           utils.get_input_channel(client.get_input_peer(...))\n    #\n    #       ...which means that the client's method verifies the hashes.\n    #\n    # Excerpt from a conversation with official developers (slightly edited):\n    #     > We send new access_hash for Channel with min flag since layer 102.\n    #     > Previously, we omitted it.\n    #     > That one works just to download the profile picture.\n    #\n    #     < So, min hashes only work for getting files,\n    #     < but the non-min hash is required for any other operation?\n    #\n    #     > Yes.\n    #\n    # More information: https://core.telegram.org/api/min\n    try:\n        if entity.SUBCLASS_OF_ID == 0xc91c90b6:  # crc32(b'InputPeer')\n            return entity\n    except AttributeError:\n        # e.g. custom.Dialog (can't cyclic import).\n        if allow_self and hasattr(entity, 'input_entity'):\n            return entity.input_entity\n        elif hasattr(entity, 'entity'):\n            return get_input_peer(entity.entity)\n        else:\n            _raise_cast_fail(entity, 'InputPeer')\n\n    if isinstance(entity, types.User):\n        if entity.is_self and allow_self:\n            return types.InputPeerSelf()\n        elif (entity.access_hash is not None and not entity.min) or not check_hash:\n            return types.InputPeerUser(entity.id, entity.access_hash)\n        else:\n            raise TypeError('User without access_hash or min info cannot be input')\n\n    if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)):\n        return types.InputPeerChat(entity.id)\n\n    if isinstance(entity, types.Channel):\n        if (entity.access_hash is not None and not entity.min) or not check_hash:\n            return types.InputPeerChannel(entity.id, entity.access_hash)\n        else:\n            raise TypeError('Channel without access_hash or min info cannot be input')\n    if isinstance(entity, types.ChannelForbidden):\n        # \"channelForbidden are never min\", and since their hash is\n        # also not optional, we assume that this truly is the case.\n        return types.InputPeerChannel(entity.id, entity.access_hash)\n\n    if isinstance(entity, types.InputUser):\n        return types.InputPeerUser(entity.user_id, entity.access_hash)\n\n    if isinstance(entity, types.InputChannel):\n        return types.InputPeerChannel(entity.channel_id, entity.access_hash)\n\n    if isinstance(entity, types.InputUserSelf):\n        return types.InputPeerSelf()\n\n    if isinstance(entity, types.InputUserFromMessage):\n        return types.InputPeerUserFromMessage(entity.peer, entity.msg_id, entity.user_id)\n\n    if isinstance(entity, types.InputChannelFromMessage):\n        return types.InputPeerChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id)\n\n    if isinstance(entity, types.UserEmpty):\n        return types.InputPeerEmpty()\n\n    if isinstance(entity, types.UserFull):\n        return get_input_peer(entity.user)\n\n    if isinstance(entity, types.ChatFull):\n        return types.InputPeerChat(entity.id)\n\n    if isinstance(entity, types.PeerChat):\n        return types.InputPeerChat(entity.chat_id)\n\n    _raise_cast_fail(entity, 'InputPeer')\n\n\ndef get_input_channel(entity):\n    \"\"\"\n    Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.\n\n    .. important::\n\n        This method does not validate for invalid general-purpose access\n        hashes, unlike `get_input_peer`. Consider using instead:\n        ``get_input_channel(get_input_peer(channel))``.\n    \"\"\"\n    try:\n        if entity.SUBCLASS_OF_ID == 0x40f202fd:  # crc32(b'InputChannel')\n            return entity\n    except AttributeError:\n        _raise_cast_fail(entity, 'InputChannel')\n\n    if isinstance(entity, (types.Channel, types.ChannelForbidden)):\n        return types.InputChannel(entity.id, entity.access_hash or 0)\n\n    if isinstance(entity, types.InputPeerChannel):\n        return types.InputChannel(entity.channel_id, entity.access_hash)\n\n    if isinstance(entity, types.InputPeerChannelFromMessage):\n        return types.InputChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id)\n\n    _raise_cast_fail(entity, 'InputChannel')\n\n\ndef get_input_user(entity):\n    \"\"\"\n    Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.\n\n    .. important::\n\n        This method does not validate for invalid general-purpose access\n        hashes, unlike `get_input_peer`. Consider using instead:\n        ``get_input_channel(get_input_peer(channel))``.\n    \"\"\"\n    try:\n        if entity.SUBCLASS_OF_ID == 0xe669bf46:  # crc32(b'InputUser'):\n            return entity\n    except AttributeError:\n        _raise_cast_fail(entity, 'InputUser')\n\n    if isinstance(entity, types.User):\n        if entity.is_self:\n            return types.InputUserSelf()\n        else:\n            return types.InputUser(entity.id, entity.access_hash or 0)\n\n    if isinstance(entity, types.InputPeerSelf):\n        return types.InputUserSelf()\n\n    if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)):\n        return types.InputUserEmpty()\n\n    if isinstance(entity, types.UserFull):\n        return get_input_user(entity.user)\n\n    if isinstance(entity, types.InputPeerUser):\n        return types.InputUser(entity.user_id, entity.access_hash)\n\n    if isinstance(entity, types.InputPeerUserFromMessage):\n        return types.InputUserFromMessage(entity.peer, entity.msg_id, entity.user_id)\n\n    _raise_cast_fail(entity, 'InputUser')\n\n\ndef get_input_dialog(dialog):\n    \"\"\"Similar to :meth:`get_input_peer`, but for dialogs\"\"\"\n    try:\n        if dialog.SUBCLASS_OF_ID == 0xa21c9795:  # crc32(b'InputDialogPeer')\n            return dialog\n        if dialog.SUBCLASS_OF_ID == 0xc91c90b6:  # crc32(b'InputPeer')\n            return types.InputDialogPeer(dialog)\n    except AttributeError:\n        _raise_cast_fail(dialog, 'InputDialogPeer')\n\n    try:\n        return types.InputDialogPeer(get_input_peer(dialog))\n    except TypeError:\n        pass\n\n    _raise_cast_fail(dialog, 'InputDialogPeer')\n\n\ndef get_input_document(document):\n    \"\"\"Similar to :meth:`get_input_peer`, but for documents\"\"\"\n    try:\n        if document.SUBCLASS_OF_ID == 0xf33fdb68:  # crc32(b'InputDocument'):\n            return document\n    except AttributeError:\n        _raise_cast_fail(document, 'InputDocument')\n\n    if isinstance(document, types.Document):\n        return types.InputDocument(\n            id=document.id, access_hash=document.access_hash,\n            file_reference=document.file_reference)\n\n    if isinstance(document, types.DocumentEmpty):\n        return types.InputDocumentEmpty()\n\n    if isinstance(document, types.MessageMediaDocument):\n        return get_input_document(document.document)\n\n    if isinstance(document, types.Message):\n        return get_input_document(document.media)\n\n    _raise_cast_fail(document, 'InputDocument')\n\n\ndef get_input_photo(photo):\n    \"\"\"Similar to :meth:`get_input_peer`, but for photos\"\"\"\n    try:\n        if photo.SUBCLASS_OF_ID == 0x846363e0:  # crc32(b'InputPhoto'):\n            return photo\n    except AttributeError:\n        _raise_cast_fail(photo, 'InputPhoto')\n\n    if isinstance(photo, types.Message):\n        photo = photo.media\n\n    if isinstance(photo, (types.photos.Photo, types.MessageMediaPhoto)):\n        photo = photo.photo\n\n    if isinstance(photo, types.Photo):\n        return types.InputPhoto(id=photo.id, access_hash=photo.access_hash,\n                                file_reference=photo.file_reference)\n\n    if isinstance(photo, types.PhotoEmpty):\n        return types.InputPhotoEmpty()\n\n    if isinstance(photo, types.messages.ChatFull):\n        photo = photo.full_chat\n\n    if isinstance(photo, types.ChannelFull):\n        return get_input_photo(photo.chat_photo)\n    elif isinstance(photo, types.UserFull):\n        return get_input_photo(photo.profile_photo)\n    elif isinstance(photo, (types.Channel, types.Chat, types.User)):\n        return get_input_photo(photo.photo)\n\n    if isinstance(photo, (types.UserEmpty, types.ChatEmpty,\n                          types.ChatForbidden, types.ChannelForbidden)):\n        return types.InputPhotoEmpty()\n\n    _raise_cast_fail(photo, 'InputPhoto')\n\n\ndef get_input_chat_photo(photo):\n    \"\"\"Similar to :meth:`get_input_peer`, but for chat photos\"\"\"\n    try:\n        if photo.SUBCLASS_OF_ID == 0xd4eb2d74:  # crc32(b'InputChatPhoto')\n            return photo\n        elif photo.SUBCLASS_OF_ID == 0xe7655f1f:  # crc32(b'InputFile'):\n            return types.InputChatUploadedPhoto(photo)\n    except AttributeError:\n        _raise_cast_fail(photo, 'InputChatPhoto')\n\n    photo = get_input_photo(photo)\n    if isinstance(photo, types.InputPhoto):\n        return types.InputChatPhoto(photo)\n    elif isinstance(photo, types.InputPhotoEmpty):\n        return types.InputChatPhotoEmpty()\n\n    _raise_cast_fail(photo, 'InputChatPhoto')\n\n\ndef get_input_geo(geo):\n    \"\"\"Similar to :meth:`get_input_peer`, but for geo points\"\"\"\n    try:\n        if geo.SUBCLASS_OF_ID == 0x430d225:  # crc32(b'InputGeoPoint'):\n            return geo\n    except AttributeError:\n        _raise_cast_fail(geo, 'InputGeoPoint')\n\n    if isinstance(geo, types.GeoPoint):\n        return types.InputGeoPoint(lat=geo.lat, long=geo.long)\n\n    if isinstance(geo, types.GeoPointEmpty):\n        return types.InputGeoPointEmpty()\n\n    if isinstance(geo, types.MessageMediaGeo):\n        return get_input_geo(geo.geo)\n\n    if isinstance(geo, types.Message):\n        return get_input_geo(geo.media)\n\n    _raise_cast_fail(geo, 'InputGeoPoint')\n\n\ndef get_input_media(\n        media, *,\n        is_photo=False, attributes=None, force_document=False,\n        voice_note=False, video_note=False, supports_streaming=False,\n        ttl=None\n):\n    \"\"\"\n    Similar to :meth:`get_input_peer`, but for media.\n\n    If the media is :tl:`InputFile` and ``is_photo`` is known to be `True`,\n    it will be treated as an :tl:`InputMediaUploadedPhoto`. Else, the rest\n    of parameters will indicate how to treat it.\n    \"\"\"\n    try:\n        if media.SUBCLASS_OF_ID == 0xfaf846f4:  # crc32(b'InputMedia')\n            return media\n        elif media.SUBCLASS_OF_ID == 0x846363e0:  # crc32(b'InputPhoto')\n            return types.InputMediaPhoto(media, ttl_seconds=ttl)\n        elif media.SUBCLASS_OF_ID == 0xf33fdb68:  # crc32(b'InputDocument')\n            return types.InputMediaDocument(media, ttl_seconds=ttl)\n    except AttributeError:\n        _raise_cast_fail(media, 'InputMedia')\n\n    if isinstance(media, types.MessageMediaPhoto):\n        return types.InputMediaPhoto(\n            id=get_input_photo(media.photo),\n            spoiler=media.spoiler,\n            ttl_seconds=ttl or media.ttl_seconds\n        )\n\n    if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)):\n        return types.InputMediaPhoto(\n            id=get_input_photo(media),\n            ttl_seconds=ttl\n        )\n\n    if isinstance(media, types.MessageMediaDocument):\n        return types.InputMediaDocument(\n            id=get_input_document(media.document),\n            spoiler=media.spoiler,\n            ttl_seconds=ttl or media.ttl_seconds\n        )\n\n    if isinstance(media, (types.Document, types.DocumentEmpty)):\n        return types.InputMediaDocument(\n            id=get_input_document(media),\n            ttl_seconds=ttl\n        )\n\n    if isinstance(media, (types.InputFile, types.InputFileBig)):\n        if is_photo:\n            return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl)\n        else:\n            attrs, mime = get_attributes(\n                media,\n                attributes=attributes,\n                force_document=force_document,\n                voice_note=voice_note,\n                video_note=video_note,\n                supports_streaming=supports_streaming\n            )\n            return types.InputMediaUploadedDocument(\n                file=media, mime_type=mime, attributes=attrs, force_file=force_document,\n                ttl_seconds=ttl)\n\n    if isinstance(media, types.MessageMediaGame):\n        return types.InputMediaGame(id=types.InputGameID(\n            id=media.game.id,\n            access_hash=media.game.access_hash\n        ))\n\n    if isinstance(media, types.MessageMediaContact):\n        return types.InputMediaContact(\n            phone_number=media.phone_number,\n            first_name=media.first_name,\n            last_name=media.last_name,\n            vcard=''\n        )\n\n    if isinstance(media, types.MessageMediaGeo):\n        return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo))\n\n    if isinstance(media, types.MessageMediaGeoLive):\n        return types.InputMediaGeoLive(\n            geo_point=get_input_geo(media.geo),\n            period=media.period,\n            heading=media.heading,\n            proximity_notification_radius=media.proximity_notification_radius,\n        )\n\n    if isinstance(media, types.MessageMediaVenue):\n        return types.InputMediaVenue(\n            geo_point=get_input_geo(media.geo),\n            title=media.title,\n            address=media.address,\n            provider=media.provider,\n            venue_id=media.venue_id,\n            venue_type=''\n        )\n\n    if isinstance(media, types.MessageMediaDice):\n        return types.InputMediaDice(media.emoticon)\n\n    if isinstance(media, (\n            types.MessageMediaEmpty, types.MessageMediaUnsupported,\n            types.ChatPhotoEmpty, types.UserProfilePhotoEmpty,\n            types.ChatPhoto, types.UserProfilePhoto)):\n        return types.InputMediaEmpty()\n\n    if isinstance(media, types.Message):\n        return get_input_media(media.media, is_photo=is_photo, ttl=ttl)\n\n    if isinstance(media, types.MessageMediaPoll):\n        if media.poll.quiz:\n            if not media.results.results:\n                # A quiz has correct answers, which we don't know until answered.\n                # If the quiz hasn't been answered we can't reconstruct it properly.\n                raise TypeError('Cannot cast unanswered quiz to any kind of InputMedia.')\n\n            correct_answers = [r.option for r in media.results.results if r.correct]\n        else:\n            correct_answers = None\n\n        return types.InputMediaPoll(\n            poll=media.poll,\n            correct_answers=correct_answers,\n            solution=media.results.solution,\n            solution_entities=media.results.solution_entities,\n        )\n\n    if isinstance(media, types.Poll):\n        return types.InputMediaPoll(media)\n\n    _raise_cast_fail(media, 'InputMedia')\n\n\ndef get_input_message(message):\n    \"\"\"Similar to :meth:`get_input_peer`, but for input messages.\"\"\"\n    try:\n        if isinstance(message, int):  # This case is really common too\n            return types.InputMessageID(message)\n        elif message.SUBCLASS_OF_ID == 0x54b6bcc5:  # crc32(b'InputMessage'):\n            return message\n        elif message.SUBCLASS_OF_ID == 0x790009e3:  # crc32(b'Message'):\n            return types.InputMessageID(message.id)\n    except AttributeError:\n        pass\n\n    _raise_cast_fail(message, 'InputMedia')\n\n\ndef get_input_group_call(call):\n    \"\"\"Similar to :meth:`get_input_peer`, but for input calls.\"\"\"\n    try:\n        if call.SUBCLASS_OF_ID == 0x58611ab1:  # crc32(b'InputGroupCall')\n            return call\n        elif call.SUBCLASS_OF_ID == 0x20b4f320:  # crc32(b'GroupCall')\n            return types.InputGroupCall(id=call.id, access_hash=call.access_hash)\n    except AttributeError:\n        _raise_cast_fail(call, 'InputGroupCall')\n\n\ndef _get_entity_pair(entity_id, entities, cache,\n                     get_input_peer=get_input_peer):\n    \"\"\"\n    Returns ``(entity, input_entity)`` for the given entity ID.\n    \"\"\"\n    if not entity_id:\n        return None, None\n\n    entity = entities.get(entity_id)\n    try:\n        input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer()\n    except AttributeError:\n        # AttributeError is unlikely, so another TypeError won't hurt\n        try:\n            input_entity = get_input_peer(entity)\n        except TypeError:\n            input_entity = None\n\n    return entity, input_entity\n\n\ndef get_message_id(message):\n    \"\"\"Similar to :meth:`get_input_peer`, but for message IDs.\"\"\"\n    if message is None:\n        return None\n\n    if isinstance(message, int):\n        return message\n\n    if isinstance(message, types.InputMessageID):\n        return message.id\n\n    try:\n        if message.SUBCLASS_OF_ID == 0x790009e3:\n            # hex(crc32(b'Message')) = 0x790009e3\n            return message.id\n    except AttributeError:\n        pass\n\n    raise TypeError('Invalid message type: {}'.format(type(message)))\n\n\ndef _get_metadata(file):\n    if not hachoir:\n        return\n\n    stream = None\n    close_stream = True\n    seekable = True\n\n    # The parser may fail and we don't want to crash if\n    # the extraction process fails.\n    try:\n        # Note: aiofiles are intentionally left out for simplicity.\n        # `helpers._FileStream` is async only for simplicity too, so can't\n        # reuse it here.\n        if isinstance(file, str):\n            stream = open(file, 'rb')\n        elif isinstance(file, bytes):\n            stream = io.BytesIO(file)\n        else:\n            stream = file\n            close_stream = False\n            if getattr(file, 'seekable', None):\n                seekable = file.seekable()\n            else:\n                seekable = False\n\n        if not seekable:\n            return None\n\n        pos = stream.tell()\n        filename = getattr(file, 'name', '')\n\n        parser = hachoir.parser.guess.guessParser(hachoir.stream.InputIOStream(\n            stream,\n            source='file:' + filename,\n            tags=[],\n            filename=filename\n        ))\n\n        return hachoir.metadata.extractMetadata(parser)\n\n    except Exception as e:\n        _log.warning('Failed to analyze %s: %s %s', file, e.__class__, e)\n\n    finally:\n        if stream and close_stream:\n            stream.close()\n        elif stream and seekable:\n            stream.seek(pos)\n\n\ndef get_attributes(file, *, attributes=None, mime_type=None,\n                   force_document=False, voice_note=False, video_note=False,\n                   supports_streaming=False, thumb=None):\n    \"\"\"\n    Get a list of attributes for the given file and\n    the mime type as a tuple ([attribute], mime_type).\n    \"\"\"\n    # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` streams\n    name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed')\n    if mime_type is None:\n        mime_type = mimetypes.guess_type(name)[0]\n\n    attr_dict = {types.DocumentAttributeFilename:\n        types.DocumentAttributeFilename(os.path.basename(name))}\n\n    if is_audio(file):\n        m = _get_metadata(file)\n        if m:\n            if m.has('author'):\n                performer = m.get('author')\n            elif m.has('artist'):\n                performer = m.get('artist')\n            else:\n                performer = None\n\n            attr_dict[types.DocumentAttributeAudio] = \\\n                types.DocumentAttributeAudio(\n                    voice=voice_note,\n                    title=m.get('title') if m.has('title') else None,\n                    performer=performer,\n                    duration=int(m.get('duration').seconds\n                                 if m.has('duration') else 0)\n                )\n\n    if not force_document and is_video(file):\n        m = _get_metadata(file)\n        if m:\n            doc = types.DocumentAttributeVideo(\n                round_message=video_note,\n                w=m.get('width') if m.has('width') else 1,\n                h=m.get('height') if m.has('height') else 1,\n                duration=int(m.get('duration').seconds\n                             if m.has('duration') else 1),\n                supports_streaming=supports_streaming\n            )\n        elif thumb:\n            t_m = _get_metadata(thumb)\n            width = 1\n            height = 1\n            if t_m and t_m.has(\"width\"):\n                width = t_m.get(\"width\")\n            if t_m and t_m.has(\"height\"):\n                height = t_m.get(\"height\")\n\n            doc = types.DocumentAttributeVideo(\n                0, width, height, round_message=video_note,\n                supports_streaming=supports_streaming)\n        else:\n            doc = types.DocumentAttributeVideo(\n                0, 1, 1, round_message=video_note,\n                supports_streaming=supports_streaming)\n\n        attr_dict[types.DocumentAttributeVideo] = doc\n\n    if voice_note:\n        if types.DocumentAttributeAudio in attr_dict:\n            attr_dict[types.DocumentAttributeAudio].voice = True\n        else:\n            attr_dict[types.DocumentAttributeAudio] = \\\n                types.DocumentAttributeAudio(0, voice=True)\n\n    # Now override the attributes if any. As we have a dict of\n    # {cls: instance}, we can override any class with the list\n    # of attributes provided by the user easily.\n    if attributes:\n        for a in attributes:\n            attr_dict[type(a)] = a\n\n    # Ensure we have a mime type, any; but it cannot be None\n    # 'The \"octet-stream\" subtype is used to indicate that a body\n    # contains arbitrary binary data.'\n    if not mime_type:\n        mime_type = 'application/octet-stream'\n\n    return list(attr_dict.values()), mime_type\n\n\ndef sanitize_parse_mode(mode):\n    \"\"\"\n    Converts the given parse mode into an object with\n    ``parse`` and ``unparse`` callable properties.\n    \"\"\"\n    if not mode:\n        return None\n\n    if (all(hasattr(mode, x) for x in ('parse', 'unparse'))\n          and all(callable(x) for x in (mode.parse, mode.unparse))):\n        return mode\n    elif callable(mode):\n        class CustomMode:\n            @staticmethod\n            def unparse(text, entities):\n                raise NotImplementedError\n\n        CustomMode.parse = mode\n        return CustomMode\n    elif isinstance(mode, str):\n        try:\n            return {\n                'md': markdown,\n                'markdown': markdown,\n                'htm': html,\n                'html': html\n            }[mode.lower()]\n        except KeyError:\n            raise ValueError('Unknown parse mode {}'.format(mode))\n    else:\n        raise TypeError('Invalid parse mode type {}'.format(mode))\n\n\ndef get_input_location(location):\n    \"\"\"\n    Similar to :meth:`get_input_peer`, but for input messages.\n\n    Note that this returns a tuple ``(dc_id, location)``, the\n    ``dc_id`` being present if known.\n    \"\"\"\n    info = _get_file_info(location)\n    return info.dc_id, info.location\n\n\ndef _get_file_info(location):\n    try:\n        if location.SUBCLASS_OF_ID == 0x1523d462:\n            return _FileInfo(None, location, None)  # crc32(b'InputFileLocation'):\n    except AttributeError:\n        _raise_cast_fail(location, 'InputFileLocation')\n\n    if isinstance(location, types.Message):\n        location = location.media\n\n    if isinstance(location, types.MessageMediaDocument):\n        location = location.document\n    elif isinstance(location, types.MessageMediaPhoto):\n        location = location.photo\n\n    if isinstance(location, types.Document):\n        return _FileInfo(location.dc_id, types.InputDocumentFileLocation(\n            id=location.id,\n            access_hash=location.access_hash,\n            file_reference=location.file_reference,\n            thumb_size=''  # Presumably to download one of its thumbnails\n        ), location.size)\n    elif isinstance(location, types.Photo):\n        return _FileInfo(location.dc_id, types.InputPhotoFileLocation(\n            id=location.id,\n            access_hash=location.access_hash,\n            file_reference=location.file_reference,\n            thumb_size=location.sizes[-1].type\n        ), _photo_size_byte_count(location.sizes[-1]))\n\n    _raise_cast_fail(location, 'InputFileLocation')\n\n\ndef _get_extension(file):\n    \"\"\"\n    Gets the extension for the given file, which can be either a\n    str or an ``open()``'ed file (which has a ``.name`` attribute).\n    \"\"\"\n    if isinstance(file, str):\n        return os.path.splitext(file)[-1]\n    elif isinstance(file, pathlib.Path):\n        return file.suffix\n    elif getattr(file, 'name', None):\n        # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase`\n        return _get_extension(file.name)\n    else:\n        # Maybe it's a Telegram media\n        return get_extension(file)\n\n\ndef is_image(file):\n    \"\"\"\n    Returns `True` if the file extension looks like an image file to Telegram.\n    \"\"\"\n    match = re.match(r'\\.(png|jpe?g)', _get_extension(file), re.IGNORECASE)\n    if match:\n        return True\n    else:\n        return isinstance(resolve_bot_file_id(file), types.Photo)\n\n\ndef is_gif(file):\n    \"\"\"\n    Returns `True` if the file extension looks like a gif file to Telegram.\n    \"\"\"\n    return re.match(r'\\.gif', _get_extension(file), re.IGNORECASE)\n\n\ndef is_audio(file):\n    \"\"\"Returns `True` if the file has an audio mime type.\"\"\"\n    ext = _get_extension(file)\n    if not ext:\n        metadata = _get_metadata(file)\n        if metadata and metadata.has('mime_type'):\n            return metadata.get('mime_type').startswith('audio/')\n        else:\n            return False\n    else:\n        file = 'a' + ext\n        return (mimetypes.guess_type(file)[0] or '').startswith('audio/')\n\n\ndef is_video(file):\n    \"\"\"Returns `True` if the file has a video mime type.\"\"\"\n    ext = _get_extension(file)\n    if not ext:\n        metadata = _get_metadata(file)\n        if metadata and metadata.has('mime_type'):\n            return metadata.get('mime_type').startswith('video/')\n        else:\n            return False\n    else:\n        file = 'a' + ext\n        return (mimetypes.guess_type(file)[0] or '').startswith('video/')\n\n\ndef is_list_like(obj):\n    \"\"\"\n    Returns `True` if the given object looks like a list.\n\n    Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not\n    enough. Things like ``open()`` are also iterable (and probably many\n    other things), so just support the commonly known list-like objects.\n    \"\"\"\n    return isinstance(obj, (list, tuple, set, dict, range, GeneratorType))\n\n\ndef parse_phone(phone):\n    \"\"\"Parses the given phone, or returns `None` if it's invalid.\"\"\"\n    if isinstance(phone, int):\n        return str(phone)\n    else:\n        phone = re.sub(r'[+()\\s-]', '', str(phone))\n        if phone.isdigit():\n            return phone\n\n\ndef parse_username(username):\n    \"\"\"\n    Parses the given username or channel access hash, given\n    a string, username or URL. Returns a tuple consisting of\n    both the stripped, lowercase username and whether it is\n    a joinchat/ hash (in which case is not lowercase'd).\n\n    Returns ``(None, False)`` if the ``username`` or link is not valid.\n    \"\"\"\n    username = username.strip()\n    m = USERNAME_RE.match(username) or TG_JOIN_RE.match(username)\n    if m:\n        username = username[m.end():]\n        is_invite = bool(m.group(1))\n        if is_invite:\n            return username, True\n        else:\n            username = username.rstrip('/')\n\n    if VALID_USERNAME_RE.match(username):\n        return username.lower(), False\n    else:\n        return None, False\n\n\ndef get_inner_text(text, entities):\n    \"\"\"\n    Gets the inner text that's surrounded by the given entities.\n    For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'.\n\n    :param text:     the original text.\n    :param entities: the entity or entities that must be matched.\n    :return: a single result or a list of the text surrounded by the entities.\n    \"\"\"\n    text = add_surrogate(text)\n    result = []\n    for e in entities:\n        start = e.offset\n        end = e.offset + e.length\n        result.append(del_surrogate(text[start:end]))\n\n    return result\n\n\ndef get_peer(peer):\n    try:\n        if isinstance(peer, int):\n            pid, cls = resolve_id(peer)\n            return cls(pid)\n        elif peer.SUBCLASS_OF_ID == 0x2d45687:\n            return peer\n        elif isinstance(peer, (\n                types.contacts.ResolvedPeer, types.InputNotifyPeer,\n                types.TopPeer, types.Dialog, types.DialogPeer)):\n            return peer.peer\n        elif isinstance(peer, types.ChannelFull):\n            return types.PeerChannel(peer.id)\n        elif isinstance(peer, types.UserEmpty):\n            return types.PeerUser(peer.id)\n        elif isinstance(peer, types.ChatEmpty):\n            return types.PeerChat(peer.id)\n\n        if peer.SUBCLASS_OF_ID in (0x7d7c6f86, 0xd9c7fc18):\n            # ChatParticipant, ChannelParticipant\n            return types.PeerUser(peer.user_id)\n\n        peer = get_input_peer(peer, allow_self=False, check_hash=False)\n        if isinstance(peer, (types.InputPeerUser, types.InputPeerUserFromMessage)):\n            return types.PeerUser(peer.user_id)\n        elif isinstance(peer, types.InputPeerChat):\n            return types.PeerChat(peer.chat_id)\n        elif isinstance(peer, (types.InputPeerChannel, types.InputPeerChannelFromMessage)):\n            return types.PeerChannel(peer.channel_id)\n    except (AttributeError, TypeError):\n        pass\n    _raise_cast_fail(peer, 'Peer')\n\n\ndef get_peer_id(peer, add_mark=True):\n    \"\"\"\n    Convert the given peer into its marked ID by default.\n\n    This \"mark\" comes from the \"bot api\" format, and with it the peer type\n    can be identified back. User ID is left unmodified, chat ID is negated,\n    and channel ID is \"prefixed\" with -100:\n\n    * ``user_id``\n    * ``-chat_id``\n    * ``-100channel_id``\n\n    The original ID and the peer type class can be returned with\n    a call to :meth:`resolve_id(marked_id)`.\n    \"\"\"\n    # First we assert it's a Peer TLObject, or early return for integers\n    if isinstance(peer, int):\n        return peer if add_mark else resolve_id(peer)[0]\n\n    # Tell the user to use their client to resolve InputPeerSelf if we got one\n    if isinstance(peer, types.InputPeerSelf):\n        _raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)')\n\n    try:\n        peer = get_peer(peer)\n    except TypeError:\n        _raise_cast_fail(peer, 'int')\n\n    if isinstance(peer, types.PeerUser):\n        return peer.user_id\n    elif isinstance(peer, types.PeerChat):\n        # Check in case the user mixed things up to avoid blowing up\n        if not (0 < peer.chat_id <= 9999999999):\n            peer.chat_id = resolve_id(peer.chat_id)[0]\n\n        return -peer.chat_id if add_mark else peer.chat_id\n    else:  # if isinstance(peer, types.PeerChannel):\n        # Check in case the user mixed things up to avoid blowing up\n        if not (0 < peer.channel_id <= 9999999999):\n            peer.channel_id = resolve_id(peer.channel_id)[0]\n\n        if not add_mark:\n            return peer.channel_id\n\n        # Growing backwards from -100_0000_000_000 indicates it's a channel\n        return -(1000000000000 + peer.channel_id)\n\n\ndef resolve_id(marked_id):\n    \"\"\"Given a marked ID, returns the original ID and its :tl:`Peer` type.\"\"\"\n    if marked_id >= 0:\n        return marked_id, types.PeerUser\n\n    marked_id = -marked_id\n    if marked_id > 1000000000000:\n        marked_id -= 1000000000000\n        return marked_id, types.PeerChannel\n    else:\n        return marked_id, types.PeerChat\n\n\ndef _rle_decode(data):\n    \"\"\"\n    Decodes run-length-encoded `data`.\n    \"\"\"\n    if not data:\n        return data\n\n    new = b''\n    last = b''\n    for cur in data:\n        if last == b'\\0':\n            new += last * cur\n            last = b''\n        else:\n            new += last\n            last = bytes([cur])\n\n    return new + last\n\n\ndef _rle_encode(string):\n    new = b''\n    count = 0\n    for cur in string:\n        if not cur:\n            count += 1\n        else:\n            if count:\n                new += b'\\0' + bytes([count])\n                count = 0\n\n            new += bytes([cur])\n    if count != 0:\n        new += b'\\0' + bytes([count])\n    return new\n\n\ndef _decode_telegram_base64(string):\n    \"\"\"\n    Decodes a url-safe base64-encoded string into its bytes\n    by first adding the stripped necessary padding characters.\n\n    This is the way Telegram shares binary data as strings,\n    such as Bot API-style file IDs or invite links.\n\n    Returns `None` if the input string was not valid.\n    \"\"\"\n    try:\n        return base64.urlsafe_b64decode(string + '=' * (len(string) % 4))\n    except (binascii.Error, ValueError, TypeError):\n        return None  # not valid base64, not valid ascii, not a string\n\n\ndef _encode_telegram_base64(string):\n    \"\"\"\n    Inverse for `_decode_telegram_base64`.\n    \"\"\"\n    try:\n        return base64.urlsafe_b64encode(string).rstrip(b'=').decode('ascii')\n    except (binascii.Error, ValueError, TypeError):\n        return None  # not valid base64, not valid ascii, not a string\n\n\ndef resolve_bot_file_id(file_id):\n    \"\"\"\n    Given a Bot API-style `file_id <telethon.tl.custom.file.File.id>`,\n    returns the media it represents. If the `file_id <telethon.tl.custom.file.File.id>`\n    is not valid, `None` is returned instead.\n\n    Note that the `file_id <telethon.tl.custom.file.File.id>` does not have information\n    such as image dimensions or file size, so these will be zero if present.\n\n    For thumbnails, the photo ID and hash will always be zero.\n    \"\"\"\n    data = _rle_decode(_decode_telegram_base64(file_id))\n    if not data:\n        return None\n\n    # This isn't officially documented anywhere, but\n    # we assume the last byte is some kind of \"version\".\n    data, version = data[:-1], data[-1]\n    if version not in (2, 4):\n        return None\n\n    if (version == 2 and len(data) == 24) or (version == 4 and len(data) == 25):\n        if version == 2:\n            file_type, dc_id, media_id, access_hash = struct.unpack('<iiqq', data)\n        # elif version == 4:\n        else:\n            # TODO Figure out what the extra byte means\n            file_type, dc_id, media_id, access_hash, _ = struct.unpack('<iiqqb', data)\n\n        if not (1 <= dc_id <= 5):\n            # Valid `file_id`'s must have valid DC IDs. Since this method is\n            # called when sending a file and the user may have entered a path\n            # they believe is correct but the file doesn't exist, this method\n            # may detect a path as \"valid\" bot `file_id` even when it's not.\n            # By checking the `dc_id`, we greatly reduce the chances of this\n            # happening.\n            return None\n\n        attributes = []\n        if file_type == 3 or file_type == 9:\n            attributes.append(types.DocumentAttributeAudio(\n                duration=0,\n                voice=file_type == 3\n            ))\n        elif file_type == 4 or file_type == 13:\n            attributes.append(types.DocumentAttributeVideo(\n                duration=0,\n                w=0,\n                h=0,\n                round_message=file_type == 13\n            ))\n        # elif file_type == 5:  # other, cannot know which\n        elif file_type == 8:\n            attributes.append(types.DocumentAttributeSticker(\n                alt='',\n                stickerset=types.InputStickerSetEmpty()\n            ))\n        elif file_type == 10:\n            attributes.append(types.DocumentAttributeAnimated())\n\n        return types.Document(\n            id=media_id,\n            access_hash=access_hash,\n            date=None,\n            mime_type='',\n            size=0,\n            thumbs=None,\n            dc_id=dc_id,\n            attributes=attributes,\n            file_reference=b''\n        )\n    elif (version == 2 and len(data) == 44) or (version == 4 and len(data) in (49, 77)):\n        if version == 2:\n            (file_type, dc_id, media_id, access_hash,\n                volume_id, secret, local_id) = struct.unpack('<iiqqqqi', data)\n        # else version == 4:\n        elif len(data) == 49:\n            # TODO Figure out what the extra five bytes mean\n            (file_type, dc_id, media_id, access_hash,\n                volume_id, secret, local_id, _) = struct.unpack('<iiqqqqi5s', data)\n        elif len(data) == 77:\n            # See #1613.\n            (file_type, dc_id, _, media_id, access_hash, volume_id, _, local_id, _) = struct.unpack('<ii28sqqq12sib', data)\n        else:\n            return None\n\n        if not (1 <= dc_id <= 5):\n            return None\n\n        # Thumbnails (small) always have ID 0; otherwise size 'x'\n        photo_size = 's' if media_id or access_hash else 'x'\n        return types.Photo(\n            id=media_id,\n            access_hash=access_hash,\n            file_reference=b'',\n            date=None,\n            sizes=[types.PhotoSize(\n                type=photo_size,\n                w=0,\n                h=0,\n                size=0\n            )],\n            dc_id=dc_id,\n            has_stickers=None\n        )\n\n\ndef pack_bot_file_id(file):\n    \"\"\"\n    Inverse operation for `resolve_bot_file_id`.\n\n    The only parameters this method will accept are :tl:`Document` and\n    :tl:`Photo`, and it will return a variable-length ``file_id`` string.\n\n    If an invalid parameter is given, it will ``return None``.\n    \"\"\"\n    if isinstance(file, types.MessageMediaDocument):\n        file = file.document\n    elif isinstance(file, types.MessageMediaPhoto):\n        file = file.photo\n\n    if isinstance(file, types.Document):\n        file_type = 5\n        for attribute in file.attributes:\n            if isinstance(attribute, types.DocumentAttributeAudio):\n                file_type = 3 if attribute.voice else 9\n            elif isinstance(attribute, types.DocumentAttributeVideo):\n                file_type = 13 if attribute.round_message else 4\n            elif isinstance(attribute, types.DocumentAttributeSticker):\n                file_type = 8\n            elif isinstance(attribute, types.DocumentAttributeAnimated):\n                file_type = 10\n            else:\n                continue\n            break\n\n        return _encode_telegram_base64(_rle_encode(struct.pack(\n            '<iiqqb', file_type, file.dc_id, file.id, file.access_hash, 2)))\n\n    elif isinstance(file, types.Photo):\n        size = next((x for x in reversed(file.sizes) if isinstance(\n            x, (types.PhotoSize, types.PhotoCachedSize))), None)\n\n        if not size:\n            return None\n\n        size = size.location\n        return _encode_telegram_base64(_rle_encode(struct.pack(\n            '<iiqqqqib', 2, file.dc_id, file.id, file.access_hash,\n            size.volume_id, 0, size.local_id, 2  # 0 = old `secret`\n        )))\n    else:\n        return None\n\n\ndef resolve_invite_link(link):\n    \"\"\"\n    Resolves the given invite link. Returns a tuple of\n    ``(link creator user id, global chat id, random int)``.\n\n    Note that for broadcast channels or with the newest link format, the link\n    creator user ID will be zero to protect their identity. Normal chats and\n    megagroup channels will have such ID.\n\n    Note that the chat ID may not be accurate for chats with a link that were\n    upgraded to megagroup, since the link can remain the same, but the chat\n    ID will be correct once a new link is generated.\n    \"\"\"\n    link_hash, is_link = parse_username(link)\n    if not is_link:\n        # Perhaps the user passed the link hash directly\n        link_hash = link\n\n    # Little known fact, but invite links with a\n    # hex-string of bytes instead of base64 also works.\n    if re.match(r'[a-fA-F\\d]+', link_hash) and len(link_hash) in (24, 32):\n        payload = bytes.fromhex(link_hash)\n    else:\n        payload = _decode_telegram_base64(link_hash)\n\n    try:\n        if len(payload) == 12:\n            return (0, *struct.unpack('>LQ', payload))\n        elif len(payload) == 16:\n            return struct.unpack('>LLQ', payload)\n        else:\n            pass\n    except (struct.error, TypeError):\n        pass\n    return None, None, None\n\n\ndef resolve_inline_message_id(inline_msg_id):\n    \"\"\"\n    Resolves an inline message ID. Returns a tuple of\n    ``(message id, peer, dc id, access hash)``\n\n    The ``peer`` may either be a :tl:`PeerUser` referencing\n    the user who sent the message via the bot in a private\n    conversation or small group chat, or a :tl:`PeerChannel`\n    if the message was sent in a channel.\n\n    The ``access_hash`` does not have any use yet.\n    \"\"\"\n    try:\n        dc_id, message_id, pid, access_hash = \\\n            struct.unpack('<iiiq', _decode_telegram_base64(inline_msg_id))\n        peer = types.PeerChannel(-pid) if pid < 0 else types.PeerUser(pid)\n        return message_id, peer, dc_id, access_hash\n    except (struct.error, TypeError):\n        return None, None, None, None\n\n\ndef get_appropriated_part_size(file_size):\n    \"\"\"\n    Gets the appropriated part size when uploading or downloading files,\n    given an initial file size.\n    \"\"\"\n    if file_size <= 104857600:  # 100MB\n        return 128\n    if file_size <= 786432000:  # 750MB\n        return 256\n    return 512\n\n\ndef encode_waveform(waveform):\n    \"\"\"\n    Encodes the input `bytes` into a 5-bit byte-string\n    to be used as a voice note's waveform. See `decode_waveform`\n    for the reverse operation.\n\n    Example\n        .. code-block:: python\n\n            chat = ...\n            file = 'my.ogg'\n\n            # Send 'my.ogg' with a ascending-triangle waveform\n            await client.send_file(chat, file, attributes=[types.DocumentAttributeAudio(\n                duration=7,\n                voice=True,\n                waveform=utils.encode_waveform(bytes(range(2 ** 5))  # 2**5 because 5-bit\n            )]\n\n            # Send 'my.ogg' with a square waveform\n            await client.send_file(chat, file, attributes=[types.DocumentAttributeAudio(\n                duration=7,\n                voice=True,\n                waveform=utils.encode_waveform(bytes((31, 31, 15, 15, 15, 15, 31, 31)) * 4)\n            )]\n    \"\"\"\n    bits_count = len(waveform) * 5\n    bytes_count = (bits_count + 7) // 8\n    result = bytearray(bytes_count + 1)\n\n    for i in range(len(waveform)):\n        byte_index, bit_shift = divmod(i * 5, 8)\n        value = (waveform[i] & 0b00011111) << bit_shift\n\n        or_what = struct.unpack('<H', (result[byte_index:byte_index + 2]))[0]\n        or_what |= value\n        result[byte_index:byte_index + 2] = struct.pack('<H', or_what)\n\n    return bytes(result[:bytes_count])\n\n\ndef decode_waveform(waveform):\n    \"\"\"\n    Inverse operation of `encode_waveform`.\n    \"\"\"\n    bit_count = len(waveform) * 8\n    value_count = bit_count // 5\n    if value_count == 0:\n        return b''\n\n    result = bytearray(value_count)\n    for i in range(value_count - 1):\n        byte_index, bit_shift = divmod(i * 5, 8)\n        value = struct.unpack('<H', waveform[byte_index:byte_index + 2])[0]\n        result[i] = (value >> bit_shift) & 0b00011111\n\n    byte_index, bit_shift = divmod(value_count - 1, 8)\n    if byte_index == len(waveform) - 1:\n        value = waveform[byte_index]\n    else:\n        value = struct.unpack('<H', waveform[byte_index:byte_index + 2])[0]\n\n    result[value_count - 1] = (value >> bit_shift) & 0b00011111\n    return bytes(result)\n\n\ndef split_text(text, entities, *, limit=4096, max_entities=100, split_at=(r'\\n', r'\\s', '.')):\n    \"\"\"\n    Split a message text and entities into multiple messages, each with their\n    own set of entities. This allows sending a very large message as multiple\n    messages while respecting the formatting.\n\n    Arguments\n        text (`str`):\n            The message text.\n\n        entities (List[:tl:`MessageEntity`])\n            The formatting entities.\n\n        limit (`int`):\n            The maximum message length of each individual message.\n\n        max_entities (`int`):\n            The maximum amount of entities that will be present in each\n            individual message.\n\n        split_at (Tuplel[`str`]):\n            The list of regular expressions that will determine where to split\n            the text. By default, a newline is searched. If no newline is\n            present, a space is searched. If no space is found, the split will\n            be made at any character.\n\n            The last expression should always match a character, or else the\n            text will stop being splitted and the resulting text may be larger\n            than the limit.\n\n    Yields\n        Pairs of ``(str, entities)`` with the split message.\n\n    Example\n        .. code-block:: python\n\n            from telethon import utils\n            from telethon.extensions import markdown\n\n            very_long_markdown_text = \"...\"\n            text, entities = markdown.parse(very_long_markdown_text)\n\n            for text, entities in utils.split_text(text, entities):\n                await client.send_message(chat, text, formatting_entities=entities)\n    \"\"\"\n    # TODO add test cases (multiple entities beyond cutoff, at cutoff, splitting at emoji)\n    # TODO try to optimize this a bit more? (avoid new_ent, smarter update method)\n    def update(ent, **updates):\n        kwargs = ent.to_dict()\n        del kwargs['_']\n        kwargs.update(updates)\n        return ent.__class__(**kwargs)\n\n    text = add_surrogate(text)\n    split_at = tuple(map(re.compile, split_at))\n\n    while True:\n        if len(entities) > max_entities:\n            last_ent = entities[max_entities - 1]\n            cur_limit = min(limit, last_ent.offset + last_ent.length)\n        else:\n            cur_limit = limit\n\n        if len(text) <= cur_limit:\n            break\n\n        for split in split_at:\n            for i in reversed(range(cur_limit)):\n                m = split.match(text, pos=i)\n                if m:\n                    cur_text, new_text = text[:m.end()], text[m.end():]\n                    cur_ent, new_ent = [], []\n                    for ent in entities:\n                        if ent.offset < m.end():\n                            if ent.offset + ent.length > m.end():\n                                cur_ent.append(update(ent, length=m.end() - ent.offset))\n                                new_ent.append(update(ent, offset=0, length=ent.offset + ent.length - m.end()))\n                            else:\n                                cur_ent.append(ent)\n                        else:\n                            new_ent.append(update(ent, offset=ent.offset - m.end()))\n\n                    yield del_surrogate(cur_text), cur_ent\n                    text, entities = new_text, new_ent\n                    break\n            else:\n                continue\n            break\n        else:\n            # Can't find where to split, just return the remaining text and entities\n            break\n\n    yield del_surrogate(text), entities\n\n\nclass AsyncClassWrapper:\n    def __init__(self, wrapped):\n        self.wrapped = wrapped\n\n    def __getattr__(self, item):\n        w = getattr(self.wrapped, item)\n        async def wrapper(*args, **kwargs):\n            val = w(*args, **kwargs)\n            return await val if inspect.isawaitable(val) else val\n\n        if callable(w):\n            return wrapper\n        else:\n            return w\n\n\ndef stripped_photo_to_jpg(stripped):\n    \"\"\"\n    Adds the JPG header and footer to a stripped image.\n\n    Ported from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225\n    \"\"\"\n    # NOTE: Changes here should update _photo_size_byte_count\n    if len(stripped) < 3 or stripped[0] != 1:\n        return stripped\n\n    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<dA<77<{X]Id\\x91\\x80\\x99\\x96\\x8f\\x80\\x8c\\x8a\\xa0\\xb4\\xe6\\xc3\\xa0\\xaa\\xda\\xad\\x8a\\x8c\\xc8\\xff\\xcb\\xda\\xee\\xf5\\xff\\xff\\xff\\x9b\\xc1\\xff\\xff\\xff\\xfa\\xff\\xe6\\xfd\\xff\\xf8\\xff\\xdb\\x00C\\x01+--<5<vAAv\\xf8\\xa5\\x8c\\xa5\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xf8\\xff\\xc0\\x00\\x11\\x08\\x00\\x00\\x00\\x00\\x03\\x01\"\\x00\\x02\\x11\\x01\\x03\\x11\\x01\\xff\\xc4\\x00\\x1f\\x00\\x00\\x01\\x05\\x01\\x01\\x01\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\xff\\xc4\\x00\\xb5\\x10\\x00\\x02\\x01\\x03\\x03\\x02\\x04\\x03\\x05\\x05\\x04\\x04\\x00\\x00\\x01}\\x01\\x02\\x03\\x00\\x04\\x11\\x05\\x12!1A\\x06\\x13Qa\\x07\"q\\x142\\x81\\x91\\xa1\\x08#B\\xb1\\xc1\\x15R\\xd1\\xf0$3br\\x82\\t\\n\\x16\\x17\\x18\\x19\\x1a%&\\'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xf1\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xff\\xc4\\x00\\x1f\\x01\\x00\\x03\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\xff\\xc4\\x00\\xb5\\x11\\x00\\x02\\x01\\x02\\x04\\x04\\x03\\x04\\x07\\x05\\x04\\x04\\x00\\x01\\x02w\\x00\\x01\\x02\\x03\\x11\\x04\\x05!1\\x06\\x12AQ\\x07aq\\x13\"2\\x81\\x08\\x14B\\x91\\xa1\\xb1\\xc1\\t#3R\\xf0\\x15br\\xd1\\n\\x16$4\\xe1%\\xf1\\x17\\x18\\x19\\x1a&\\'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz\\x82\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x92\\x93\\x94\\x95\\x96\\x97\\x98\\x99\\x9a\\xa2\\xa3\\xa4\\xa5\\xa6\\xa7\\xa8\\xa9\\xaa\\xb2\\xb3\\xb4\\xb5\\xb6\\xb7\\xb8\\xb9\\xba\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc8\\xc9\\xca\\xd2\\xd3\\xd4\\xd5\\xd6\\xd7\\xd8\\xd9\\xda\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xf2\\xf3\\xf4\\xf5\\xf6\\xf7\\xf8\\xf9\\xfa\\xff\\xda\\x00\\x0c\\x03\\x01\\x00\\x02\\x11\\x03\\x11\\x00?\\x00')\n    footer = b\"\\xff\\xd9\"\n    header[164] = stripped[1]\n    header[166] = stripped[2]\n    return bytes(header) + stripped[3:] + footer\n\n\ndef _photo_size_byte_count(size):\n    if isinstance(size, types.PhotoSize):\n        return size.size\n    elif isinstance(size, types.PhotoStrippedSize):\n        if len(size.bytes) < 3 or size.bytes[0] != 1:\n            return len(size.bytes)\n\n        return len(size.bytes) + 622\n    elif isinstance(size, types.PhotoCachedSize):\n        return len(size.bytes)\n    elif isinstance(size, types.PhotoSizeEmpty):\n        return 0\n    elif isinstance(size, types.PhotoSizeProgressive):\n        return max(size.sizes)\n    else:\n        return None\n\n\nasync def maybe_async(coro):\n    result = coro\n    if inspect.isawaitable(result):\n        warnings.warn('Using async sessions support is an experimental feature')\n        result = await result\n    return result\n"
  },
  {
    "path": "telethon/version.py",
    "content": "# Versions should comply with PEP440.\n# This line is parsed in setup.py:\n__version__ = '1.42.0'\n"
  },
  {
    "path": "telethon_examples/LICENSE",
    "content": "CC0 1.0 Universal\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator and\nsubsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for the\npurpose of contributing to a commons of creative, cultural and scientific\nworks (\"Commons\") that the public can reliably and without fear of later\nclaims of infringement build upon, modify, incorporate in other works, reuse\nand redistribute as freely as possible in any form whatsoever and for any\npurposes, including without limitation commercial purposes. These owners may\ncontribute to the Commons to promote the ideal of a free culture and the\nfurther production of creative, cultural and scientific works, or to gain\nreputation or greater distribution for their Work in part through the use and\nefforts of others.\n\nFor these and/or other purposes and motivations, and without any expectation\nof additional consideration or compensation, the person associating CC0 with a\nWork (the \"Affirmer\"), to the extent that he or she is an owner of Copyright\nand Related Rights in the Work, voluntarily elects to apply CC0 to the Work\nand publicly distribute the Work under its terms, with knowledge of his or her\nCopyright and Related Rights in the Work and the meaning and intended legal\neffect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not limited\nto, the following:\n\n  i. the right to reproduce, adapt, distribute, perform, display, communicate,\n  and translate a Work;\n\n  ii. moral rights retained by the original author(s) and/or performer(s);\n\n  iii. publicity and privacy rights pertaining to a person's image or likeness\n  depicted in a Work;\n\n  iv. rights protecting against unfair competition in regards to a Work,\n  subject to the limitations in paragraph 4(a), below;\n\n  v. rights protecting the extraction, dissemination, use and reuse of data in\n  a Work;\n\n  vi. database rights (such as those arising under Directive 96/9/EC of the\n  European Parliament and of the Council of 11 March 1996 on the legal\n  protection of databases, and under any national implementation thereof,\n  including any amended or successor version of such directive); and\n\n  vii. other similar, equivalent or corresponding rights throughout the world\n  based on applicable law or treaty, and any national implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention of,\napplicable law, Affirmer hereby overtly, fully, permanently, irrevocably and\nunconditionally waives, abandons, and surrenders all of Affirmer's Copyright\nand Related Rights and associated claims and causes of action, whether now\nknown or unknown (including existing as well as future claims and causes of\naction), in the Work (i) in all territories worldwide, (ii) for the maximum\nduration provided by applicable law or treaty (including future time\nextensions), (iii) in any current or future medium and for any number of\ncopies, and (iv) for any purpose whatsoever, including without limitation\ncommercial, advertising or promotional purposes (the \"Waiver\"). Affirmer makes\nthe Waiver for the benefit of each member of the public at large and to the\ndetriment of Affirmer's heirs and successors, fully intending that such Waiver\nshall not be subject to revocation, rescission, cancellation, termination, or\nany other legal or equitable action to disrupt the quiet enjoyment of the Work\nby the public as contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason be\njudged legally invalid or ineffective under applicable law, then the Waiver\nshall be preserved to the maximum extent permitted taking into account\nAffirmer's express Statement of Purpose. In addition, to the extent the Waiver\nis so judged Affirmer hereby grants to each affected person a royalty-free,\nnon transferable, non sublicensable, non exclusive, irrevocable and\nunconditional license to exercise Affirmer's Copyright and Related Rights in\nthe Work (i) in all territories worldwide, (ii) for the maximum duration\nprovided by applicable law or treaty (including future time extensions), (iii)\nin any current or future medium and for any number of copies, and (iv) for any\npurpose whatsoever, including without limitation commercial, advertising or\npromotional purposes (the \"License\"). The License shall be deemed effective as\nof the date CC0 was applied by Affirmer to the Work. Should any part of the\nLicense for any reason be judged legally invalid or ineffective under\napplicable law, such partial invalidity or ineffectiveness shall not\ninvalidate the remainder of the License, and in such case Affirmer hereby\naffirms that he or she will not (i) exercise any of his or her remaining\nCopyright and Related Rights in the Work or (ii) assert any associated claims\nand causes of action with respect to the Work, in either case contrary to\nAffirmer's express Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n  a. No trademark or patent rights held by Affirmer are waived, abandoned,\n  surrendered, licensed or otherwise affected by this document.\n\n  b. Affirmer offers the Work as-is and makes no representations or warranties\n  of any kind concerning the Work, express, implied, statutory or otherwise,\n  including without limitation warranties of title, merchantability, fitness\n  for a particular purpose, non infringement, or the absence of latent or\n  other defects, accuracy, or the present or absence of errors, whether or not\n  discoverable, all to the greatest extent permissible under applicable law.\n\n  c. Affirmer disclaims responsibility for clearing rights of other persons\n  that may apply to the Work or any use thereof, including without limitation\n  any person's Copyright and Related Rights in the Work. Further, Affirmer\n  disclaims responsibility for obtaining any necessary consents, permissions\n  or other rights required for any use of the Work.\n\n  d. Affirmer understands and acknowledges that Creative Commons is not a\n  party to this document and has no duty or obligation with respect to this\n  CC0 or use of the Work.\n\nFor more information, please see\n<http://creativecommons.org/publicdomain/zero/1.0/>\n"
  },
  {
    "path": "telethon_examples/README.md",
    "content": "# Examples\n\nThis folder contains several single-file examples using [Telethon].\n\n## Requisites\n\nYou should have the `telethon` library installed with `pip`.\nRun `python3 -m pip install --upgrade telethon --user` if you don't\nhave it installed yet (this is the most portable way to install it).\n\nThe scripts will ask you for your API ID, hash, etc. through standard input.\nYou can also define the following environment variables to avoid doing so:\n\n* `TG_API_ID`, this is your API ID from https://my.telegram.org.\n* `TG_API_HASH`, this is your API hash from https://my.telegram.org.\n* `TG_TOKEN`, this is your bot token from [@BotFather] for bot examples.\n* `TG_SESSION`, this is the name of the `*.session` file to use.\n\n## Downloading Examples\n\nYou may download all and run any example by typing in a terminal:\n```sh\ngit clone https://github.com/LonamiWebs/Telethon.git\ncd Telethon\ncd telethon_examples\npython3 gui.py\n```\n\nYou can also right-click the title of any example and use \"Save Link As…\" to\ndownload only a particular example.\n\nAll examples are licensed under the [CC0 License], so you can use\nthem as the base for your own code without worrying about copyright.\n\n## Available Examples\n\n### [`print_updates.py`]\n\n* Usable as: **user and bot**.\n* Difficulty: **easy**.\n\nTrivial example that just prints all the updates Telegram originally\nsends. Your terminal should support UTF-8, or Python may fail to print\nsome characters on screen.\n\n### [`print_messages.py`]\n\n* Usable as: **user and bot**.\n* Difficulty: **easy**.\n\nThis example uses the different `@client.on` syntax to register event\nhandlers, and uses the `pattern=` variable to filter only some messages.\n\nThere are a lot other things you can do, but you should refer to the\ndocumentation of [`events.NewMessage`] since this is only a simple example.\n\n### [`replier.py`]\n\n* Usable as: **user and bot**.\n* Difficulty: **easy**.\n\nThis example showcases a third way to add event handlers (using decorators\nbut without the client; you should use the one you prefer) and will also\nreply to some messages with different reactions, or to your commands.\n\nIt also shows how to enable `logging`, which you should always do, but was\nnot really needed for the previous two trivial examples.\n\n### [`assistant.py`]\n\n* Usable as a: **bot**.\n* Difficulty: **medium**.\n\nThis example is the core of the actual bot account [@TelethonianBot] running\nin the [official Telethon's chat] to help people out. It showcases how to\ncreate an extremely simple \"plugins\" system with Telethon, but you're free\nto borrow ideas from it and make it as fancy as you like (perhaps you want\nto add hot reloading?).\n\nThe plugins are a separate Python file each which get loaded dynamically and\ncan be found at <https://github.com/Lonami/TelethonianBotExt>. To use them,\nclone the repository into a `plugins` folder next to `assistant.py` and then\nrun `assistant.py`.\n\nThe content of the plugins or how they work is not really relevant. You can\ndisable them by moving them elsewhere or deleting the file entirely. The point\nis to learn how you can build fancy things with your own code and Telethon.\n\n### [`interactive_telegram_client.py`]\n\n* Usable as: **user**.\n* Difficulty: **medium**.\n\nInteractive terminal client that you can use to list your dialogs,\nsend messages, delete them, and download media. The code is a bit\nlong which may make it harder to follow, and requires saving some\nstate in order for downloads to work later.\n\n### [`quart_login.py`]\n\n* Usable as: **user**.\n* Difficulty: **medium**.\n\nWeb-based application using [Quart](https://pgjones.gitlab.io/quart/index.html)\n(an `asyncio` alternative to [Flask](http://flask.pocoo.org/)) and Telethon\ntogether.\n\nThe example should work as a base for Quart applications *with a single\nglobal client*, and it should be easy to adapt for multiple clients by\nfollowing the comments in the code.\n\nIt showcases how to login manually (ask for phone, code, and login),\nand once the user is logged in, some messages and photos will be shown\nin the page.\n\nThere is nothing special about Quart. It was chosen because it's a\ndrop-in replacement for Flask, the most popular option for web-apps.\nYou can use any `asyncio` library with Telethon just as well,\nlike [Sanic](https://sanic.readthedocs.io/en/latest/index.html) or\n[aiohttp](https://docs.aiohttp.org/en/stable/). You can even use Flask,\nif you learn how to use `threading` and `asyncio` together.\n\n### [`gui.py`]\n\n* Usable as: **user and bot**.\n* Difficulty: **high**.\n\nThis is a simple GUI written with [`tkinter`] which becomes more complicated\nwhen there's a need to use [`asyncio`] (although it's only a bit of additional\nsetup). The code to deal with the interface and the commands the GUI supports\nalso complicate the code further and require knowledge and careful reading.\n\nThis example is the actual bot account [@TelethonianBot] running in the\n[official Telethon's chat] to help people out. The file is a bit big and\nassumes some [`asyncio`] knowledge, but otherwise is easy to follow.\n\n![Screenshot of the tkinter GUI][tkinter GUI]\n\n### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/payment.py)\n\n* Usable as: **bot**.\n* Difficulty: **medium**.\n\nThis 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.\n\nYou'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.\n\n\nIt 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.\n\n\n[Telethon]: https://github.com/LonamiWebs/Telethon\n[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_examples/LICENSE\n[@BotFather]: https://t.me/BotFather\n[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/assistant.py\n[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/quart_login.py\n[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/gui.py\n[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/interactive_telegram_client.py\n[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_messages.py\n[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_updates.py\n[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/replier.py\n[@TelethonianBot]: https://t.me/TelethonianBot\n[official Telethon's chat]: https://t.me/TelethonChat\n[`asyncio`]: https://docs.python.org/3/library/asyncio.html\n[`tkinter`]: https://docs.python.org/3/library/tkinter.html\n[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/screenshot-gui.jpg\n[`events.NewMessage`]: https://docs.telethon.dev/en/stable/modules/events.html#telethon.events.newmessage.NewMessage\n"
  },
  {
    "path": "telethon_examples/assistant.py",
    "content": "\"\"\"\nThis file is only the \"core\" of the bot. It is responsible for loading the\nplugins module and initializing it. You may obtain the plugins by running:\n\n    git clone https://github.com/Lonami/TelethonianBotExt plugins\n\nIn the same folder where this file lives. As a result, the directory should\nlook like the following:\n\n    assistant.py\n    plugins/\n        ...\n\"\"\"\nimport asyncio\nimport os\nimport sys\nimport time\n\nfrom telethon import TelegramClient\n\ntry:\n    # Standalone script assistant.py with folder plugins/\n    import plugins\nexcept ImportError:\n    try:\n        # Running as a module with `python -m assistant` and structure:\n        #\n        #     assistant/\n        #         __main__.py (this file)\n        #         plugins/    (cloned)\n        from . import plugins\n    except ImportError:\n        print('could not load the plugins module, does the directory exist '\n              'in the correct location?', file=sys.stderr)\n\n        exit(1)\n\n\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\nAPI_ID = get_env('TG_API_ID', 'Enter your API ID: ', int)\nAPI_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')\nTOKEN = get_env('TG_TOKEN', 'Enter the bot token: ')\nNAME = TOKEN.split(':')[0]\n\n\nasync def main():\n    bot = TelegramClient(NAME, API_ID, API_HASH)\n\n    await bot.start(bot_token=TOKEN)\n\n    try:\n        await plugins.init(bot)\n        await bot.run_until_disconnected()\n    finally:\n        await bot.disconnect()\n\n\nif __name__ == '__main__':\n    asyncio.run(main())\n"
  },
  {
    "path": "telethon_examples/gui.py",
    "content": "import asyncio\nimport collections\nimport functools\nimport inspect\nimport os\nimport re\nimport sys\nimport time\nimport tkinter\nimport tkinter.constants\nimport tkinter.scrolledtext\nimport tkinter.ttk\n\nfrom telethon import TelegramClient, events, utils\n\n# Some configuration for the app\nTITLE = 'Telethon GUI'\nSIZE = '640x280'\nREPLY = re.compile(r'\\.r\\s*(\\d+)\\s*(.+)', re.IGNORECASE)\nDELETE = re.compile(r'\\.d\\s*(\\d+)', re.IGNORECASE)\nEDIT = re.compile(r'\\.s(.+?[^\\\\])/(.*)', re.IGNORECASE)\n\n\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\n# Session name, API ID and hash to use; loaded from environmental variables\nSESSION = os.environ.get('TG_SESSION', 'gui')\nAPI_ID = get_env('TG_API_ID', 'Enter your API ID: ', int)\nAPI_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')\n\n\ndef sanitize_str(string):\n    return ''.join(x if ord(x) <= 0xffff else\n                   '{{{:x}ū}}'.format(ord(x)) for x in string)\n\n\ndef callback(func):\n    \"\"\"\n    This decorator turns `func` into a callback for Tkinter\n    to be able to use, even if `func` is an awaitable coroutine.\n    \"\"\"\n    @functools.wraps(func)\n    def wrapped(*args, **kwargs):\n        result = func(*args, **kwargs)\n        if inspect.iscoroutine(result):\n            asyncio.create_task(result)\n\n    return wrapped\n\n\ndef allow_copy(widget):\n    \"\"\"\n    This helper makes `widget` readonly but allows copying with ``Ctrl+C``.\n    \"\"\"\n    widget.bind('<Control-c>', lambda e: None)\n    widget.bind('<Key>', lambda e: \"break\")\n\n\nclass App(tkinter.Tk):\n    \"\"\"\n    Our main GUI application; we subclass `tkinter.Tk`\n    so the `self` instance can be the root widget.\n\n    One must be careful when assigning members or\n    defining methods since those may interfer with\n    the root widget.\n\n    You may prefer to have ``App.root = tkinter.Tk()``\n    and create widgets with ``self.root`` as parent.\n    \"\"\"\n    def __init__(self, client, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.cl = client\n        self.me = None\n\n        self.title(TITLE)\n        self.geometry(SIZE)\n\n        # Signing in row; the entry supports phone and bot token\n        self.sign_in_label = tkinter.Label(self, text='Loading...')\n        self.sign_in_label.grid(row=0, column=0)\n        self.sign_in_entry = tkinter.Entry(self)\n        self.sign_in_entry.grid(row=0, column=1, sticky=tkinter.EW)\n        self.sign_in_entry.bind('<Return>', self.sign_in)\n        self.sign_in_button = tkinter.Button(self, text='...',\n                                             command=self.sign_in)\n        self.sign_in_button.grid(row=0, column=2)\n        self.code = None\n\n        # The chat where to send and show messages from\n        tkinter.Label(self, text='Target chat:').grid(row=1, column=0)\n        self.chat = tkinter.Entry(self)\n        self.chat.grid(row=1, column=1, columnspan=2, sticky=tkinter.EW)\n        self.columnconfigure(1, weight=1)\n        self.chat.bind('<Return>', self.check_chat)\n        self.chat.bind('<FocusOut>', self.check_chat)\n        self.chat.focus()\n        self.chat_id = None\n\n        # Message log (incoming and outgoing); we configure it as readonly\n        self.log = tkinter.scrolledtext.ScrolledText(self)\n        allow_copy(self.log)\n        self.log.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW)\n        self.rowconfigure(2, weight=1)\n        self.cl.add_event_handler(self.on_message, events.NewMessage)\n\n        # Save shown message IDs to support replying with \".rN reply\"\n        # For instance to reply to the last message \".r1 this is a reply\"\n        # Deletion also works with \".dN\".\n        self.message_ids = []\n\n        # Save the sent texts to allow editing with \".s text/replacement\"\n        # For instance to edit the last \"hello\" with \"bye\" \".s hello/bye\"\n        self.sent_text = collections.deque(maxlen=10)\n\n        # Sending messages\n        tkinter.Label(self, text='Message:').grid(row=3, column=0)\n        self.message = tkinter.Entry(self)\n        self.message.grid(row=3, column=1, sticky=tkinter.EW)\n        self.message.bind('<Return>', self.send_message)\n        tkinter.Button(self, text='Send',\n                       command=self.send_message).grid(row=3, column=2)\n\n        # Post-init (async, connect client)\n        self.cl.loop.create_task(self.post_init())\n\n    async def post_init(self):\n        \"\"\"\n        Completes the initialization of our application.\n        Since `__init__` cannot be `async` we use this.\n        \"\"\"\n        if await self.cl.is_user_authorized():\n            self.set_signed_in(await self.cl.get_me())\n        else:\n            # User is not logged in, configure the button to ask them to login\n            self.sign_in_button.configure(text='Sign in')\n            self.sign_in_label.configure(\n                text='Sign in (phone/token):')\n\n    async def on_message(self, event):\n        \"\"\"\n        Event handler that will add new messages to the message log.\n        \"\"\"\n        # We want to show only messages sent to this chat\n        if event.chat_id != self.chat_id:\n            return\n\n        # Save the message ID so we know which to reply to\n        self.message_ids.append(event.id)\n\n        # Decide a prefix (\">> \" for our messages, \"<user>\" otherwise)\n        if event.out:\n            text = '>> '\n        else:\n            sender = await event.get_sender()\n            text = '<{}> '.format(sanitize_str(\n                utils.get_display_name(sender)))\n\n        # If the message has media show \"(MediaType) \"\n        if event.media:\n            text += '({}) '.format(event.media.__class__.__name__)\n\n        text += sanitize_str(event.text)\n        text += '\\n'\n\n        # Append the text to the end with a newline, and scroll to the end\n        self.log.insert(tkinter.END, text)\n        self.log.yview(tkinter.END)\n\n    # noinspection PyUnusedLocal\n    @callback\n    async def sign_in(self, event=None):\n        \"\"\"\n        Note the `event` argument. This is required since this callback\n        may be called from a ``widget.bind`` (such as ``'<Return>'``),\n        which sends information about the event we don't care about.\n\n        This callback logs out if authorized, signs in if a code was\n        sent or a bot token is input, or sends the code otherwise.\n        \"\"\"\n        self.sign_in_label.configure(text='Working...')\n        self.sign_in_entry.configure(state=tkinter.DISABLED)\n        if await self.cl.is_user_authorized():\n            await self.cl.log_out()\n            self.destroy()\n            return\n\n        value = self.sign_in_entry.get().strip()\n        if self.code:\n            self.set_signed_in(await self.cl.sign_in(code=value))\n        elif ':' in value:\n            self.set_signed_in(await self.cl.sign_in(bot_token=value))\n        else:\n            self.code = await self.cl.send_code_request(value)\n            self.sign_in_label.configure(text='Code:')\n            self.sign_in_entry.configure(state=tkinter.NORMAL)\n            self.sign_in_entry.delete(0, tkinter.END)\n            self.sign_in_entry.focus()\n            return\n\n    def set_signed_in(self, me):\n        \"\"\"\n        Configures the application as \"signed in\" (displays user's\n        name and disables the entry to input phone/bot token/code).\n        \"\"\"\n        self.me = me\n        self.sign_in_label.configure(text='Signed in')\n        self.sign_in_entry.configure(state=tkinter.NORMAL)\n        self.sign_in_entry.delete(0, tkinter.END)\n        self.sign_in_entry.insert(tkinter.INSERT, utils.get_display_name(me))\n        self.sign_in_entry.configure(state=tkinter.DISABLED)\n        self.sign_in_button.configure(text='Log out')\n        self.chat.focus()\n\n    # noinspection PyUnusedLocal\n    @callback\n    async def send_message(self, event=None):\n        \"\"\"\n        Sends a message. Does nothing if the client is not connected.\n        \"\"\"\n        if not self.cl.is_connected():\n            return\n\n        # The user needs to configure a chat where the message should be sent.\n        #\n        # If the chat ID does not exist, it was not valid and the user must\n        # configure one; hint them by changing the background to red.\n        if not self.chat_id:\n            self.chat.configure(bg='red')\n            self.chat.focus()\n            return\n\n        # Get the message, clear the text field and focus it again\n        text = self.message.get().strip()\n        self.message.delete(0, tkinter.END)\n        self.message.focus()\n        if not text:\n            return\n\n        # NOTE: This part is optional but supports editing messages\n        #       You can remove it if you find it too complicated.\n        #\n        # Check if the edit matches any text\n        m = EDIT.match(text)\n        if m:\n            find = re.compile(m.group(1).lstrip())\n            # Cannot reversed(enumerate(...)), use index\n            for i in reversed(range(len(self.sent_text))):\n                msg_id, msg_text = self.sent_text[i]\n                if find.search(msg_text):\n                    # Found text to replace, so replace it and edit\n                    new = find.sub(m.group(2), msg_text)\n                    self.sent_text[i] = (msg_id, new)\n                    await self.cl.edit_message(self.chat_id, msg_id, new)\n\n                    # Notify that a replacement was made\n                    self.log.insert(tkinter.END, '(message edited: {} -> {})\\n'\n                                    .format(msg_text, new))\n                    self.log.yview(tkinter.END)\n                    return\n\n        # Check if we want to delete the message\n        m = DELETE.match(text)\n        if m:\n            try:\n                delete = self.message_ids.pop(-int(m.group(1)))\n            except IndexError:\n                pass\n            else:\n                await self.cl.delete_messages(self.chat_id, delete)\n                # Notify that a message was deleted\n                self.log.insert(tkinter.END, '(message deleted)\\n')\n                self.log.yview(tkinter.END)\n                return\n\n        # Check if we want to reply to some message\n        reply_to = None\n        m = REPLY.match(text)\n        if m:\n            text = m.group(2)\n            try:\n                reply_to = self.message_ids[-int(m.group(1))]\n            except IndexError:\n                pass\n\n        # NOTE: This part is no longer optional. It sends the message.\n        # Send the message text and get back the sent message object\n        message = await self.cl.send_message(self.chat_id, text,\n                                             reply_to=reply_to)\n\n        # Save the sent message ID and text to allow edits\n        self.sent_text.append((message.id, text))\n\n        # Process the sent message as if it were an event\n        await self.on_message(message)\n\n    # noinspection PyUnusedLocal\n    @callback\n    async def check_chat(self, event=None):\n        \"\"\"\n        Checks the input chat where to send and listen messages from.\n        \"\"\"\n        if self.me is None:\n            return  # Not logged in yet\n\n        chat = self.chat.get().strip()\n        try:\n            chat = int(chat)\n        except ValueError:\n            pass\n\n        try:\n            old = self.chat_id\n            # Valid chat ID, set it and configure the colour back to white\n            self.chat_id = await self.cl.get_peer_id(chat)\n            self.chat.configure(bg='white')\n\n            # If the chat ID changed, clear the\n            # messages that we could edit or reply\n            if self.chat_id != old:\n                self.message_ids.clear()\n                self.sent_text.clear()\n                self.log.delete('1.0', tkinter.END)\n                if not self.me.bot:\n                    for msg in reversed(\n                            await self.cl.get_messages(self.chat_id, 100)):\n                        await self.on_message(msg)\n        except ValueError:\n            # Invalid chat ID, let the user know with a yellow background\n            self.chat_id = None\n            self.chat.configure(bg='yellow')\n\n\nasync def main(interval=0.05):\n    client = TelegramClient(SESSION, API_ID, API_HASH)\n    try:\n        await client.connect()\n    except Exception as e:\n        print('Failed to connect', e, file=sys.stderr)\n        return\n\n    app = App(client)\n    try:\n        while True:\n            # We want to update the application but get back\n            # to asyncio's event loop. For this we sleep a\n            # short time so the event loop can run.\n            #\n            # https://www.reddit.com/r/Python/comments/33ecpl\n            app.update()\n            await asyncio.sleep(interval)\n    except KeyboardInterrupt:\n        pass\n    except tkinter.TclError as e:\n        if 'application has been destroyed' not in e.args[0]:\n            raise\n    finally:\n        await app.cl.disconnect()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "telethon_examples/interactive_telegram_client.py",
    "content": "import asyncio\nimport os\nimport sys\nimport time\nfrom getpass import getpass\n\nfrom telethon import TelegramClient, events\nfrom telethon.errors import SessionPasswordNeededError\nfrom telethon.network import ConnectionTcpAbridged\nfrom telethon.utils import get_display_name\n\n\ndef sprint(string, *args, **kwargs):\n    \"\"\"Safe Print (handle UnicodeEncodeErrors on some terminals)\"\"\"\n    try:\n        print(string, *args, **kwargs)\n    except UnicodeEncodeError:\n        string = string.encode('utf-8', errors='ignore')\\\n                       .decode('ascii', errors='ignore')\n        print(string, *args, **kwargs)\n\n\ndef print_title(title):\n    \"\"\"Helper function to print titles to the console more nicely\"\"\"\n    sprint('\\n')\n    sprint('=={}=='.format('=' * len(title)))\n    sprint('= {} ='.format(title))\n    sprint('=={}=='.format('=' * len(title)))\n\n\ndef bytes_to_string(byte_count):\n    \"\"\"Converts a byte count to a string (in KB, MB...)\"\"\"\n    suffix_index = 0\n    while byte_count >= 1024:\n        byte_count /= 1024\n        suffix_index += 1\n\n    return '{:.2f}{}'.format(\n        byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index]\n    )\n\n\nasync def async_input(prompt):\n    \"\"\"\n    Python's ``input()`` is blocking, which means the event loop we set\n    above can't be running while we're blocking there. This method will\n    let the loop run while we wait for input.\n    \"\"\"\n    print(prompt, end='', flush=True)\n    return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip()\n\n\ndef get_env(name, message, cast=str):\n    \"\"\"Helper to get environment variables interactively\"\"\"\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\nclass InteractiveTelegramClient(TelegramClient):\n    \"\"\"Full featured Telegram client, meant to be used on an interactive\n       session to see what Telethon is capable off -\n\n       This client allows the user to perform some basic interaction with\n       Telegram through Telethon, such as listing dialogs (open chats),\n       talking to people, downloading media, and receiving updates.\n    \"\"\"\n\n    def __init__(self, session_user_id, api_id, api_hash,\n                 proxy=None):\n        \"\"\"\n        Initializes the InteractiveTelegramClient.\n        :param session_user_id: Name of the *.session file.\n        :param api_id: Telegram's api_id acquired through my.telegram.org.\n        :param api_hash: Telegram's api_hash.\n        :param proxy: Optional proxy tuple/dictionary.\n        \"\"\"\n        print_title('Initialization')\n\n        print('Initializing interactive example...')\n\n        # The first step is to initialize the TelegramClient, as we are\n        # subclassing it, we need to call super().__init__(). On a more\n        # normal case you would want 'client = TelegramClient(...)'\n        super().__init__(\n            # These parameters should be passed always, session name and API\n            session_user_id, api_id, api_hash,\n\n            # You can optionally change the connection mode by passing a\n            # type or an instance of it. This changes how the sent packets\n            # look (low-level concept you normally shouldn't worry about).\n            # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged.\n            connection=ConnectionTcpAbridged,\n\n            # If you're using a proxy, set it here.\n            proxy=proxy\n        )\n\n        # Store {message.id: message} map here so that we can download\n        # media known the message ID, for every message having media.\n        self.found_media = {}\n\n    async def init(self):\n        # Calling .connect() may raise a connection error False, so you need\n        # to except those before continuing. Otherwise you may want to retry\n        # as done here.\n        print('Connecting to Telegram servers...')\n        try:\n            await self.connect()\n        except IOError:\n            print('Initial connection failed. Retrying...')\n            await self.connect()\n\n        # If the user hasn't called .sign_in() yet, they won't\n        # be authorized. The first thing you must do is authorize. Calling\n        # .sign_in() should only be done once as the information is saved on\n        # the *.session file so you don't need to enter the code every time.\n        if not await self.is_user_authorized():\n            print('First run. Sending code request...')\n            user_phone = input('Enter your phone: ')\n            await self.sign_in(user_phone)\n\n            self_user = None\n            while self_user is None:\n                code = input('Enter the code you just received: ')\n                try:\n                    self_user = await self.sign_in(code=code)\n\n                # Two-step verification may be enabled, and .sign_in will\n                # raise this error. If that's the case ask for the password.\n                # Note that getpass() may not work on PyCharm due to a bug,\n                # if that's the case simply change it for input().\n                except SessionPasswordNeededError:\n                    pw = getpass('Two step verification is enabled. '\n                                 'Please enter your password: ')\n\n                    self_user = await self.sign_in(password=pw)\n\n    async def run(self):\n        \"\"\"Main loop of the TelegramClient, will wait for user action\"\"\"\n\n        # Once everything is ready, we can add an event handler.\n        #\n        # Events are an abstraction over Telegram's \"Updates\" and\n        # are much easier to use.\n        self.add_event_handler(self.message_handler, events.NewMessage)\n\n        # Enter a while loop to chat as long as the user wants\n        while True:\n            # Retrieve the top dialogs. You can set the limit to None to\n            # retrieve all of them if you wish, but beware that may take\n            # a long time if you have hundreds of them.\n            dialog_count = 15\n\n            # Entities represent the user, chat or channel\n            # corresponding to the dialog on the same index.\n            dialogs = await self.get_dialogs(limit=dialog_count)\n\n            i = None\n            while i is None:\n                print_title('Dialogs window')\n\n                # Display them so the user can choose\n                for i, dialog in enumerate(dialogs, start=1):\n                    sprint('{}. {}'.format(i, get_display_name(dialog.entity)))\n\n                # Let the user decide who they want to talk to\n                print()\n                print('> Who do you want to send messages to?')\n                print('> Available commands:')\n                print('  !q: Quits the dialogs window and exits.')\n                print('  !l: Logs out, terminating this session.')\n                print()\n                i = await async_input('Enter dialog ID or a command: ')\n                if i == '!q':\n                    return\n                if i == '!l':\n                    # Logging out will cause the user to need to reenter the\n                    # code next time they want to use the library, and will\n                    # also delete the *.session file off the filesystem.\n                    #\n                    # This is not the same as simply calling .disconnect(),\n                    # which simply shuts down everything gracefully.\n                    await self.log_out()\n                    return\n\n                try:\n                    i = int(i if i else 0) - 1\n                    # Ensure it is inside the bounds, otherwise retry\n                    if not 0 <= i < dialog_count:\n                        i = None\n                except ValueError:\n                    i = None\n\n            # Retrieve the selected user (or chat, or channel)\n            entity = dialogs[i].entity\n\n            # Show some information\n            print_title('Chat with \"{}\"'.format(get_display_name(entity)))\n            print('Available commands:')\n            print('  !q:  Quits the current chat.')\n            print('  !Q:  Quits the current chat and exits.')\n            print('  !h:  prints the latest messages (message History).')\n            print('  !up  <path>: Uploads and sends the Photo from path.')\n            print('  !uf  <path>: Uploads and sends the File from path.')\n            print('  !d   <msg-id>: Deletes a message by its id')\n            print('  !dm  <msg-id>: Downloads the given message Media (if any).')\n            print('  !dp: Downloads the current dialog Profile picture.')\n            print('  !i:  Prints information about this chat..')\n            print()\n\n            # And start a while loop to chat\n            while True:\n                msg = await async_input('Enter a message: ')\n                # Quit\n                if msg == '!q':\n                    break\n                elif msg == '!Q':\n                    return\n\n                # History\n                elif msg == '!h':\n                    # First retrieve the messages and some information\n                    messages = await self.get_messages(entity, limit=10)\n\n                    # Iterate over all (in reverse order so the latest appear\n                    # the last in the console) and print them with format:\n                    # \"[hh:mm] Sender: Message\"\n                    for msg in reversed(messages):\n                        # Note how we access .sender here. Since we made an\n                        # API call using the self client, it will always have\n                        # information about the sender. This is different to\n                        # events, where Telegram may not always send the user.\n                        name = get_display_name(msg.sender)\n\n                        # Format the message content\n                        if getattr(msg, 'media', None):\n                            self.found_media[msg.id] = msg\n                            content = '<{}> {}'.format(\n                                type(msg.media).__name__, msg.message)\n\n                        elif hasattr(msg, 'message'):\n                            content = msg.message\n                        elif hasattr(msg, 'action'):\n                            content = str(msg.action)\n                        else:\n                            # Unknown message, simply print its class name\n                            content = type(msg).__name__\n\n                        # And print it to the user\n                        sprint('[{}:{}] (ID={}) {}: {}'.format(\n                            msg.date.hour, msg.date.minute, msg.id, name, content))\n\n                # Send photo\n                elif msg.startswith('!up '):\n                    # Slice the message to get the path\n                    path = msg[len('!up '):]\n                    await self.send_photo(path=path, entity=entity)\n\n                # Send file (document)\n                elif msg.startswith('!uf '):\n                    # Slice the message to get the path\n                    path = msg[len('!uf '):]\n                    await self.send_document(path=path, entity=entity)\n\n                # Delete messages\n                elif msg.startswith('!d '):\n                    # Slice the message to get message ID\n                    msg = msg[len('!d '):]\n                    deleted_msg = await self.delete_messages(entity, msg)\n                    print('Deleted {}'.format(deleted_msg))\n\n                # Download media\n                elif msg.startswith('!dm '):\n                    # Slice the message to get message ID\n                    await self.download_media_by_id(msg[len('!dm '):])\n\n                # Download profile photo\n                elif msg == '!dp':\n                    print('Downloading profile picture to usermedia/...')\n                    os.makedirs('usermedia', exist_ok=True)\n                    output = await self.download_profile_photo(entity,\n                                                               'usermedia')\n                    if output:\n                        print('Profile picture downloaded to', output)\n                    else:\n                        print('No profile picture found for this user!')\n\n                elif msg == '!i':\n                    attributes = list(entity.to_dict().items())\n                    pad = max(len(x) for x, _ in attributes)\n                    for name, val in attributes:\n                        print(\"{:<{width}} : {}\".format(name, val, width=pad))\n\n                # Send chat message (if any)\n                elif msg:\n                    await self.send_message(entity, msg, link_preview=False)\n\n    async def send_photo(self, path, entity):\n        \"\"\"Sends the file located at path to the desired entity as a photo\"\"\"\n        await self.send_file(\n            entity, path,\n            progress_callback=self.upload_progress_callback\n        )\n        print('Photo sent!')\n\n    async def send_document(self, path, entity):\n        \"\"\"Sends the file located at path to the desired entity as a document\"\"\"\n        await self.send_file(\n            entity, path,\n            force_document=True,\n            progress_callback=self.upload_progress_callback\n        )\n        print('Document sent!')\n\n    async def download_media_by_id(self, media_id):\n        \"\"\"Given a message ID, finds the media this message contained and\n           downloads it.\n        \"\"\"\n        try:\n            msg = self.found_media[int(media_id)]\n        except (ValueError, KeyError):\n            # ValueError when parsing, KeyError when accessing dictionary\n            print('Invalid media ID given or message not found!')\n            return\n\n        print('Downloading media to usermedia/...')\n        os.makedirs('usermedia', exist_ok=True)\n        output = await self.download_media(\n            msg.media,\n            file='usermedia/',\n            progress_callback=self.download_progress_callback\n        )\n        print('Media downloaded to {}!'.format(output))\n\n    @staticmethod\n    def download_progress_callback(downloaded_bytes, total_bytes):\n        InteractiveTelegramClient.print_progress(\n            'Downloaded', downloaded_bytes, total_bytes\n        )\n\n    @staticmethod\n    def upload_progress_callback(uploaded_bytes, total_bytes):\n        InteractiveTelegramClient.print_progress(\n            'Uploaded', uploaded_bytes, total_bytes\n        )\n\n    @staticmethod\n    def print_progress(progress_type, downloaded_bytes, total_bytes):\n        print('{} {} out of {} ({:.2%})'.format(\n            progress_type, bytes_to_string(downloaded_bytes),\n            bytes_to_string(total_bytes), downloaded_bytes / total_bytes)\n        )\n\n    async def message_handler(self, event):\n        \"\"\"Callback method for received events.NewMessage\"\"\"\n\n        # Note that message_handler is called when a Telegram update occurs\n        # and an event is created. Telegram may not always send information\n        # about the ``.sender`` or the ``.chat``, so if you *really* want it\n        # you should use ``get_chat()`` and ``get_sender()`` while working\n        # with events. Since they are methods, you know they may make an API\n        # call, which can be expensive.\n        chat = await event.get_chat()\n        if event.is_group:\n            if event.out:\n                sprint('>> sent \"{}\" to chat {}'.format(\n                    event.text, get_display_name(chat)\n                ))\n            else:\n                sprint('<< {} @ {} sent \"{}\"'.format(\n                    get_display_name(await event.get_sender()),\n                    get_display_name(chat),\n                    event.text\n                ))\n        else:\n            if event.out:\n                sprint('>> \"{}\" to user {}'.format(\n                    event.text, get_display_name(chat)\n                ))\n            else:\n                sprint('<< {} sent \"{}\"'.format(\n                    get_display_name(chat), event.text\n                ))\n\n\nasync def main():\n    SESSION = os.environ.get('TG_SESSION', 'interactive')\n    API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int)\n    API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')\n    client = InteractiveTelegramClient(SESSION, API_ID, API_HASH)\n    await client.init()\n    await client.run()\n\n\nif __name__ == '__main__':\n    asyncio.run()\n"
  },
  {
    "path": "telethon_examples/payment.py",
    "content": "from telethon import TelegramClient, events, types, functions\n\nimport asyncio\nimport logging\nimport tracemalloc\nimport os\nimport time\nimport sys\n\n\"\"\"\nProvider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token\n\nIf you are using test token, set test=True in generate_invoice function,\nIf you are using real token, set test=False\n\"\"\"\nprovider_token = ''\n\ntracemalloc.start()\nlogging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n                    level=logging.WARNING)\nlogger = logging.getLogger(__name__)\n\n\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\nbot = TelegramClient(\n    os.environ.get('TG_SESSION', 'payment'),\n    get_env('TG_API_ID', 'Enter your API ID: ', int),\n    get_env('TG_API_HASH', 'Enter your API hash: '),\n    proxy=None\n)\n\n\n# That event is handled when customer enters his card/etc, on final pre-checkout\n# If we don't `SetBotPrecheckoutResultsRequest`, money won't be charged from buyer, and nothing will happen next.\n@bot.on(events.Raw(types.UpdateBotPrecheckoutQuery))\nasync def payment_pre_checkout_handler(event: types.UpdateBotPrecheckoutQuery):\n    if event.payload.decode('UTF-8') == 'product A':\n        # so we have to confirm payment\n        await bot(\n            functions.messages.SetBotPrecheckoutResultsRequest(\n                query_id=event.query_id,\n                success=True,\n                error=None\n            )\n        )\n    elif event.payload.decode('UTF-8') == 'product B':\n        # same for another\n        await bot(\n            functions.messages.SetBotPrecheckoutResultsRequest(\n                query_id=event.query_id,\n                success=True,\n                error=None\n            )\n        )\n    else:\n        # for example, something went wrong (whatever reason). We can tell customer about that:\n        await bot(\n            functions.messages.SetBotPrecheckoutResultsRequest(\n                query_id=event.query_id,\n                success=False,\n                error='Something went wrong'\n            )\n        )\n\n    raise events.StopPropagation\n\n\n# That event is handled at the end, when customer payed.\n@bot.on(events.Raw(types.UpdateNewMessage))\nasync def payment_received_handler(event):\n    if isinstance(event.message.action, types.MessageActionPaymentSentMe):\n        payment: types.MessageActionPaymentSentMe = event.message.action\n        # do something after payment was received\n        if payment.payload.decode('UTF-8') == 'product A':\n            await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product A!')\n        elif payment.payload.decode('UTF-8') == 'product B':\n            await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product B!')\n        raise events.StopPropagation\n\n\n# let's put it in one function for more easier way\ndef generate_invoice(price_label: str, price_amount: int, currency: str, title: str,\n                     description: str, payload: str, start_param: str) -> types.InputMediaInvoice:\n    price = types.LabeledPrice(label=price_label, amount=price_amount)  # label - just a text, amount=10000 means 100.00\n    invoice = types.Invoice(\n        currency=currency,  # currency like USD\n        prices=[price],  # there could be a couple of prices.\n        test=True,  # if you're working with test token, else set test=False.\n        # More info at https://core.telegram.org/bots/payments\n\n        # params for requesting specific fields\n        name_requested=False,\n        phone_requested=False,\n        email_requested=False,\n        shipping_address_requested=False,\n\n        # if price changes depending on shipping\n        flexible=False,\n\n        # send data to provider\n        phone_to_provider=False,\n        email_to_provider=False\n    )\n    return types.InputMediaInvoice(\n        title=title,\n        description=description,\n        invoice=invoice,\n        payload=payload.encode('UTF-8'),  # payload, which will be sent to next 2 handlers\n        provider=provider_token,\n\n        provider_data=types.DataJSON('{}'),\n        # data about the invoice, which will be shared with the payment provider. A detailed description of\n        # required fields should be provided by the payment provider.\n\n        start_param=start_param,\n        # Unique deep-linking parameter. May also be used in UpdateBotPrecheckoutQuery\n        # see: https://core.telegram.org/bots#deep-linking\n        # it may be the empty string if not needed\n\n    )\n\n\n@bot.on(events.NewMessage(pattern='/start'))\nasync def start_handler(event: events.NewMessage.Event):\n    await event.respond('/product_a - product A\\n/product_b - product B\\n/product_c - product, shall cause an error')\n\n\n@bot.on(events.NewMessage(pattern='/product_a'))\nasync def start_handler(event: events.NewMessage.Event):\n    await bot.send_message(\n        event.chat_id, 'Sending invoice A',\n        file=generate_invoice(\n            price_label='Pay', price_amount=10000, currency='RUB', title='Title A', description='description A',\n            payload='product A', start_param='abc'\n        )\n    )\n\n\n@bot.on(events.NewMessage(pattern='/product_b'))\nasync def start_handler(event: events.NewMessage.Event):\n    await bot.send_message(\n        event.chat_id, 'Sending invoice B',\n        file=generate_invoice(\n            price_label='Pay', price_amount=20000, currency='RUB', title='Title B', description='description B',\n            payload='product B', start_param='abc'\n        )\n    )\n\n\n@bot.on(events.NewMessage(pattern='/product_c'))\nasync def start_handler(event: events.NewMessage.Event):\n    await bot.send_message(\n        event.chat_id, 'Sending invoice C',\n        file=generate_invoice(\n            price_label='Pay', price_amount=50000, currency='RUB', title='Title C',\n            description='description c - shall cause an error', payload='product C', start_param='abc'\n        )\n    )\n\n\nasync def main():\n    await bot.start()\n    await bot.run_until_disconnected()\n\n\nif __name__ == '__main__':\n    if not provider_token:\n        logger.error(\"No provider token supplied.\")\n        exit(1)\n    asyncio.run(main())\n"
  },
  {
    "path": "telethon_examples/print_messages.py",
    "content": "#!/usr/bin/env python3\n# A simple script to print some messages.\nimport os\nimport sys\nimport time\n\nfrom telethon import TelegramClient, events, utils\n\n\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\nsession = os.environ.get('TG_SESSION', 'printer')\napi_id = get_env('TG_API_ID', 'Enter your API ID: ', int)\napi_hash = get_env('TG_API_HASH', 'Enter your API hash: ')\nproxy = None  # https://github.com/romis2012/python-socks\n\n# Create and start the client so we can make requests (we don't here)\nclient = TelegramClient(session, api_id, api_hash, proxy=proxy).start()\n\n\n# `pattern` is a regex, see https://docs.python.org/3/library/re.html\n# Use https://regexone.com/ if you want a more interactive way of learning.\n#\n# \"(?i)\" makes it case-insensitive, and | separates \"options\".\n@client.on(events.NewMessage(pattern=r'(?i).*\\b(hello|hi)\\b'))\nasync def handler(event):\n    sender = await event.get_sender()\n    name = utils.get_display_name(sender)\n    print(name, 'said', event.text, '!')\n\ntry:\n    print('(Press Ctrl+C to stop this)')\n    client.run_until_disconnected()\nfinally:\n    client.disconnect()\n\n# Note: We used try/finally to show it can be done this way, but using:\n#\n#   with client:\n#       client.run_until_disconnected()\n#\n# is almost always a better idea.\n"
  },
  {
    "path": "telethon_examples/print_updates.py",
    "content": "#!/usr/bin/env python3\n# A simple script to print all updates received.\n# Import modules to access environment, sleep, write to stderr\nimport os\nimport sys\nimport time\n\n# Import the client\nfrom telethon import TelegramClient\n\n\n# This is a helper method to access environment variables or\n# prompt the user to type them in the terminal if missing.\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\n# Define some variables so the code reads easier\nsession = os.environ.get('TG_SESSION', 'printer')\napi_id = get_env('TG_API_ID', 'Enter your API ID: ', int)\napi_hash = get_env('TG_API_HASH', 'Enter your API hash: ')\nproxy = None  # https://github.com/romis2012/python-socks\n\n\n# This is our update handler. It is called when a new update arrives.\nasync def handler(update):\n    print(update)\n\n\n# Use the client in a `with` block. It calls `start/disconnect` automatically.\nwith TelegramClient(session, api_id, api_hash, proxy=proxy) as client:\n    # Register the update handler so that it gets called\n    client.add_event_handler(handler)\n\n    # Run the client until Ctrl+C is pressed, or the client disconnects\n    print('(Press Ctrl+C to stop this)')\n    client.run_until_disconnected()\n"
  },
  {
    "path": "telethon_examples/quart_login.py",
    "content": "import base64\nimport os\n\nfrom quart import Quart, render_template_string, request\n\nfrom telethon import TelegramClient, utils\nfrom telethon.errors import SessionPasswordNeededError\n\n\ndef get_env(name, message):\n    if name in os.environ:\n        return os.environ[name]\n    return input(message)\n\n\nBASE_TEMPLATE = '''\n<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset='UTF-8'>\n        <title>Telethon + Quart</title>\n    </head>\n    <body>{{ content | safe }}</body>\n</html>\n'''\n\nPHONE_FORM = '''\n<form action='/' method='post'>\n    Phone (international format): <input name='phone' type='text' placeholder='+34600000000'>\n    <input type='submit'>\n</form>\n'''\n\nCODE_FORM = '''\n<form action='/' method='post'>\n    Telegram code: <input name='code' type='text' placeholder='70707'>\n    <input type='submit'>\n</form>\n'''\n\nPASSWORD_FORM = '''\n<form action='/' method='post'>\n    Telegram password: <input name='password' type='text' placeholder='your password'>\n    <input type='submit'>\n</form>\n'''\n\n# Session name, API ID and hash to use; loaded from environmental variables\nSESSION = os.environ.get('TG_SESSION', 'quart')\nAPI_ID = int(get_env('TG_API_ID', 'Enter your API ID: '))\nAPI_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')\n\n# Telethon client\nclient = TelegramClient(SESSION, API_ID, API_HASH)\nclient.parse_mode = 'html'  # <- Render things nicely\nphone = None\n\n# Quart app\napp = Quart(__name__)\napp.secret_key = 'CHANGE THIS TO SOMETHING SECRET'\n\n\n# Helper method to format messages nicely\nasync def format_message(message):\n    if message.photo:\n        content = '<img src=\"data:image/png;base64,{}\" alt=\"{}\" />'.format(\n            base64.b64encode(await message.download_media(bytes)).decode(),\n            message.raw_text\n        )\n    else:\n        # client.parse_mode = 'html', so bold etc. will work!\n        content = (message.text or '(action message)').replace('\\n', '<br>')\n\n    return '<p><strong>{}</strong>: {}<sub>{}</sub></p>'.format(\n        utils.get_display_name(message.sender),\n        content,\n        message.date\n    )\n\n\n# Connect the client before we start serving with Quart\n@app.before_serving\nasync def startup():\n    # After connecting, the client will create additional asyncio tasks that run until it's disconnected again.\n    # Be careful to not mix different asyncio loops during a client's lifetime, or things won't work properly!\n    await client.connect()\n\n\n# After we're done serving (near shutdown), clean up the client\n@app.after_serving\nasync def cleanup():\n    await client.disconnect()\n\n\n@app.route('/', methods=['GET', 'POST'])\nasync def root():\n    # We want to update the global phone variable to remember it\n    global phone\n\n    # Check form parameters (phone/code)\n    form = await request.form\n    if 'phone' in form:\n        phone = form['phone']\n        await client.send_code_request(phone)\n\n    if 'code' in form:\n        try:\n            await client.sign_in(code=form['code'])\n        except SessionPasswordNeededError:\n            return await render_template_string(BASE_TEMPLATE, content=PASSWORD_FORM)\n\n    if 'password' in form:\n        await client.sign_in(password=form['password'])\n\n    # If we're logged in, show them some messages from their first dialog\n    if await client.is_user_authorized():\n        # They are logged in, show them some messages from their first dialog\n        dialog = (await client.get_dialogs())[0]\n        result = '<h1>{}</h1>'.format(dialog.title)\n        async for m in client.iter_messages(dialog, 10):\n            result += await(format_message(m))\n\n        return await render_template_string(BASE_TEMPLATE, content=result)\n\n    # Ask for the phone if we don't know it yet\n    if phone is None:\n        return await render_template_string(BASE_TEMPLATE, content=PHONE_FORM)\n\n    # We have the phone, but we're not logged in, so ask for the code\n    return await render_template_string(BASE_TEMPLATE, content=CODE_FORM)\n\n\n# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio\n# event loop. If we had connected the `TelegramClient` before, `telethon` will\n# use `asyncio.get_running_loop()` to create some additional tasks. If these\n# loops are different, it won't work.\n#\n# To keep things simple, be sure to not create multiple asyncio loops!\nif __name__ == '__main__':\n    app.run()\n"
  },
  {
    "path": "telethon_examples/replier.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nA example script to automatically send messages based on certain triggers.\n\nThis script assumes that you have certain files on the working directory,\nsuch as \"xfiles.m4a\" or \"anytime.png\" for some of the automated replies.\n\"\"\"\nimport os\nimport sys\nimport time\nfrom collections import defaultdict\n\nfrom telethon import TelegramClient, events\n\nimport logging\nlogging.basicConfig(level=logging.WARNING)\n\n# \"When did we last react?\" dictionary, 0.0 by default\nrecent_reacts = defaultdict(float)\n\n\ndef get_env(name, message, cast=str):\n    if name in os.environ:\n        return os.environ[name]\n    while True:\n        value = input(message)\n        try:\n            return cast(value)\n        except ValueError as e:\n            print(e, file=sys.stderr)\n            time.sleep(1)\n\n\ndef can_react(chat_id):\n    # Get the time when we last sent a reaction (or 0)\n    last = recent_reacts[chat_id]\n\n    # Get the current time\n    now = time.time()\n\n    # If 10 minutes as seconds have passed, we can react\n    if now - last < 10 * 60:\n        # Make sure we updated the last reaction time\n        recent_reacts[chat_id] = now\n        return True\n    else:\n        return False\n\n\n# Register `events.NewMessage` before defining the client.\n# Once you have a client, `add_event_handler` will use this event.\n@events.register(events.NewMessage)\nasync def handler(event):\n    # There are better ways to do this, but this is simple.\n    # If the message is not outgoing (i.e. someone else sent it)\n    if not event.out:\n        if 'emacs' in event.raw_text:\n            if can_react(event.chat_id):\n                await event.reply('> emacs\\nneeds more vim')\n\n        elif 'vim' in event.raw_text:\n            if can_react(event.chat_id):\n                await event.reply('> vim\\nneeds more emacs')\n\n        elif 'chrome' in event.raw_text:\n            if can_react(event.chat_id):\n                await event.reply('> chrome\\nneeds more firefox')\n\n    # Reply always responds as a reply. We can respond without replying too\n    if 'shrug' in event.raw_text:\n        if can_react(event.chat_id):\n            await event.respond(r'¯\\_(ツ)_/¯')\n\n    # We can also use client methods from here\n    client = event.client\n\n    # If we sent the message, we are replying to someone,\n    # and we said \"save pic\" in the message\n    if event.out and event.is_reply and 'save pic' in event.raw_text:\n        reply_msg = await event.get_reply_message()\n        replied_to_user = await reply_msg.get_input_sender()\n\n        message = await event.reply('Downloading your profile photo...')\n        file = await client.download_profile_photo(replied_to_user)\n        await message.edit('I saved your photo in {}'.format(file))\n\n\nclient = TelegramClient(\n    os.environ.get('TG_SESSION', 'replier'),\n    get_env('TG_API_ID', 'Enter your API ID: ', int),\n    get_env('TG_API_HASH', 'Enter your API hash: '),\n    proxy=None\n)\n\nwith client:\n    # This remembers the events.NewMessage we registered before\n    client.add_event_handler(handler)\n\n    print('(Press Ctrl+C to stop this)')\n    client.run_until_disconnected()\n"
  },
  {
    "path": "telethon_generator/__init__.py",
    "content": "\n"
  },
  {
    "path": "telethon_generator/data/api.tl",
    "content": "boolFalse#bc799737 = Bool;\nboolTrue#997275b5 = Bool;\n\ntrue#3fedd339 = True;\n\nvector#1cb5c415 {t:Type} # [ t ] = Vector t;\n\nerror#c4b9f9bb code:int text:string = Error;\n\nnull#56730bcc = Null;\n\ninputPeerEmpty#7f3b18ea = InputPeer;\ninputPeerSelf#7da07ec9 = InputPeer;\ninputPeerChat#35a95cb9 chat_id:long = InputPeer;\ninputPeerUser#dde8a54c user_id:long access_hash:long = InputPeer;\ninputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer;\ninputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer;\ninputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer;\n\ninputUserEmpty#b98886cf = InputUser;\ninputUserSelf#f7c1b13f = InputUser;\ninputUser#f21158c6 user_id:long access_hash:long = InputUser;\ninputUserFromMessage#1da448e2 peer:InputPeer msg_id:int user_id:long = InputUser;\n\ninputPhoneContact#6a1dc4be flags:# client_id:long phone:string first_name:string last_name:string note:flags.0?TextWithEntities = InputContact;\n\ninputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile;\ninputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile;\ninputFileStoryDocument#62dc8b48 id:InputDocument = InputFile;\n\ninputMediaEmpty#9664f57f = InputMedia;\ninputMediaUploadedPhoto#1e287d04 flags:# spoiler:flags.2?true file:InputFile stickers:flags.0?Vector<InputDocument> ttl_seconds:flags.1?int = InputMedia;\ninputMediaPhoto#b3ba0635 flags:# spoiler:flags.1?true id:InputPhoto ttl_seconds:flags.0?int = InputMedia;\ninputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia;\ninputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia;\ninputMediaUploadedDocument#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<DocumentAttribute> stickers:flags.0?Vector<InputDocument> video_cover:flags.6?InputPhoto video_timestamp:flags.7?int ttl_seconds:flags.1?int = InputMedia;\ninputMediaDocument#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;\ninputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia;\ninputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia;\ninputMediaDocumentExternal#779600f9 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int video_cover:flags.2?InputPhoto video_timestamp:flags.3?int = InputMedia;\ninputMediaGame#d33f43f3 id:InputGame = InputMedia;\ninputMediaInvoice#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;\ninputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia;\ninputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector<bytes> solution:flags.1?string solution_entities:flags.1?Vector<MessageEntity> = InputMedia;\ninputMediaDice#e66fbf7b emoticon:string = InputMedia;\ninputMediaStory#89fdd778 peer:InputPeer id:int = InputMedia;\ninputMediaWebPage#c21b8849 flags:# force_large_media:flags.0?true force_small_media:flags.1?true optional:flags.2?true url:string = InputMedia;\ninputMediaPaidMedia#c4103386 flags:# stars_amount:long extended_media:Vector<InputMedia> payload:flags.0?string = InputMedia;\ninputMediaTodo#9fc55fde todo:TodoList = InputMedia;\ninputMediaStakeDice#f3a9244a game_hash:string ton_amount:long client_seed:bytes = InputMedia;\n\ninputChatPhotoEmpty#1ca48f57 = InputChatPhoto;\ninputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto;\ninputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto;\n\ninputGeoPointEmpty#e4c123d6 = InputGeoPoint;\ninputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint;\n\ninputPhotoEmpty#1cd7bf0d = InputPhoto;\ninputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto;\n\ninputFileLocation#dfdaabe1 volume_id:long local_id:int secret:long file_reference:bytes = InputFileLocation;\ninputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation;\ninputDocumentFileLocation#bad07584 id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation;\ninputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation;\ninputTakeoutFileLocation#29be5899 = InputFileLocation;\ninputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation;\ninputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation;\ninputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation;\ninputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation;\ninputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation;\n\npeerUser#59511722 user_id:long = Peer;\npeerChat#36c6019a chat_id:long = Peer;\npeerChannel#a2a5371e channel_id:long = Peer;\n\nstorage.fileUnknown#aa963b05 = storage.FileType;\nstorage.filePartial#40bc6f52 = storage.FileType;\nstorage.fileJpeg#7efe0e = storage.FileType;\nstorage.fileGif#cae1aadf = storage.FileType;\nstorage.filePng#a4f63c0 = storage.FileType;\nstorage.filePdf#ae1e508d = storage.FileType;\nstorage.fileMp3#528a0677 = storage.FileType;\nstorage.fileMov#4b09ebbc = storage.FileType;\nstorage.fileMp4#b3cea0e4 = storage.FileType;\nstorage.fileWebp#1081464c = storage.FileType;\n\nuserEmpty#d3bc4b7a id:long = User;\nuser#31774388 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 bot_forum_view:flags2.16?true bot_forum_can_manage_topics:flags2.17?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<RestrictionReason> bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector<Username> stories_max_id:flags2.5?RecentStory color:flags2.8?PeerColor profile_color:flags2.9?PeerColor bot_active_users:flags2.12?int bot_verification_icon:flags2.14?long send_paid_messages_stars:flags2.15?long = User;\n\nuserProfilePhotoEmpty#4f11bae1 = UserProfilePhoto;\nuserProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto;\n\nuserStatusEmpty#9d05049 = UserStatus;\nuserStatusOnline#edb93949 expires:int = UserStatus;\nuserStatusOffline#8c703f was_online:int = UserStatus;\nuserStatusRecently#7b197dc8 flags:# by_me:flags.0?true = UserStatus;\nuserStatusLastWeek#541a1d1a flags:# by_me:flags.0?true = UserStatus;\nuserStatusLastMonth#65899777 flags:# by_me:flags.0?true = UserStatus;\n\nchatEmpty#29562865 id:long = Chat;\nchat#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;\nchatForbidden#6592a1a7 id:long title:string = Chat;\nchannel#1c32b11c 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 autotranslation:flags2.15?true broadcast_messages_allowed:flags2.16?true monoforum:flags2.17?true forum_tabs:flags2.19?true id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector<RestrictionReason> 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<Username> stories_max_id:flags2.4?RecentStory 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 send_paid_messages_stars:flags2.14?long linked_monoforum_id:flags2.18?long = Chat;\nchannelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true monoforum:flags.10?true id:long access_hash:long title:string until_date:flags.16?int = Chat;\n\nchatFull#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<BotInfo> 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<long> available_reactions:flags.18?ChatReactions reactions_limit:flags.20?int = ChatFull;\nchannelFull#e4e0b29d 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 paid_messages_available:flags2.20?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<BotInfo> 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<string> groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector<long> 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 send_paid_messages_stars:flags2.21?long main_tab:flags2.22?ProfileTab = ChatFull;\n\nchatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant;\nchatParticipantCreator#e46bcee4 user_id:long = ChatParticipant;\nchatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant;\n\nchatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants;\nchatParticipants#3cbc93f8 chat_id:long participants:Vector<ChatParticipant> version:int = ChatParticipants;\n\nchatPhotoEmpty#37c1011c = ChatPhoto;\nchatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto;\n\nmessageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message;\nmessage#9cb490e9 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 paid_suggested_post_stars:flags2.8?true paid_suggested_post_ton:flags2.9?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<MessageEntity> 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<RestrictionReason> 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 paid_message_stars:flags2.6?long suggested_post:flags2.7?SuggestedPost schedule_repeat_period:flags2.10?int summary_from_language:flags2.11?string = Message;\nmessageService#7a800e0a 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 saved_peer_id:flags.28?Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction reactions:flags.20?MessageReactions ttl_period:flags.25?int = Message;\n\nmessageMediaEmpty#3ded6320 = MessageMedia;\nmessageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia;\nmessageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia;\nmessageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia;\nmessageMediaUnsupported#9f84f49e = MessageMedia;\nmessageMediaDocument#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<Document> video_cover:flags.9?Photo video_timestamp:flags.10?int ttl_seconds:flags.2?int = MessageMedia;\nmessageMediaWebPage#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;\nmessageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia;\nmessageMediaGame#fdb19008 game:Game = MessageMedia;\nmessageMediaInvoice#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;\nmessageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia;\nmessageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia;\nmessageMediaDice#8cbec07 flags:# value:int emoticon:string game_outcome:flags.0?messages.EmojiGameOutcome = MessageMedia;\nmessageMediaStory#68cb6283 flags:# via_mention:flags.1?true peer:Peer id:int story:flags.0?StoryItem = MessageMedia;\nmessageMediaGiveaway#aa073beb flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.2?true channels:Vector<long> countries_iso2:flags.1?Vector<string> prize_description:flags.3?string quantity:int months:flags.4?int stars:flags.5?long until_date:int = MessageMedia;\nmessageMediaGiveawayResults#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<long> months:flags.4?int stars:flags.5?long prize_description:flags.1?string until_date:int = MessageMedia;\nmessageMediaPaidMedia#a8852491 stars_amount:long extended_media:Vector<MessageExtendedMedia> = MessageMedia;\nmessageMediaToDo#8a53b014 flags:# todo:TodoList completions:flags.0?Vector<TodoCompletion> = MessageMedia;\nmessageMediaVideoStream#ca5cab89 flags:# rtmp_stream:flags.0?true call:InputGroupCall = MessageMedia;\n\nmessageActionEmpty#b6aef7b0 = MessageAction;\nmessageActionChatCreate#bd47cbad title:string users:Vector<long> = MessageAction;\nmessageActionChatEditTitle#b5a1ce5a title:string = MessageAction;\nmessageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction;\nmessageActionChatDeletePhoto#95e3fbef = MessageAction;\nmessageActionChatAddUser#15cefd00 users:Vector<long> = MessageAction;\nmessageActionChatDeleteUser#a43f30cc user_id:long = MessageAction;\nmessageActionChatJoinedByLink#31224c3 inviter_id:long = MessageAction;\nmessageActionChannelCreate#95d2ac92 title:string = MessageAction;\nmessageActionChatMigrateTo#e1037f92 channel_id:long = MessageAction;\nmessageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageAction;\nmessageActionPinMessage#94bd38ed = MessageAction;\nmessageActionHistoryClear#9fbab604 = MessageAction;\nmessageActionGameScore#92a72876 game_id:long score:int = MessageAction;\nmessageActionPaymentSentMe#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;\nmessageActionPaymentSent#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;\nmessageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction;\nmessageActionScreenshotTaken#4792929b = MessageAction;\nmessageActionCustomAction#fae69f56 message:string = MessageAction;\nmessageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true from_request:flags.3?true domain:flags.0?string app:flags.2?BotApp = MessageAction;\nmessageActionSecureValuesSentMe#1b287353 values:Vector<SecureValue> credentials:SecureCredentialsEncrypted = MessageAction;\nmessageActionSecureValuesSent#d95c6154 types:Vector<SecureValueType> = MessageAction;\nmessageActionContactSignUp#f3f25f76 = MessageAction;\nmessageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction;\nmessageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction;\nmessageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector<long> = MessageAction;\nmessageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction;\nmessageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction;\nmessageActionSetChatTheme#b91bbd3a theme:ChatTheme = MessageAction;\nmessageActionChatJoinedByRequest#ebbca3cb = MessageAction;\nmessageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction;\nmessageActionWebViewDataSent#b4c38cb5 text:string = MessageAction;\nmessageActionGiftPremium#48e91302 flags:# currency:string amount:long days:int crypto_currency:flags.0?string crypto_amount:flags.0?long message:flags.1?TextWithEntities = MessageAction;\nmessageActionTopicCreate#d999256 flags:# title_missing:flags.1?true title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction;\nmessageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction;\nmessageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction;\nmessageActionRequestedPeer#31518e9b button_id:int peers:Vector<Peer> = MessageAction;\nmessageActionSetChatWallPaper#5060a3f4 flags:# same:flags.0?true for_both:flags.1?true wallpaper:WallPaper = MessageAction;\nmessageActionGiftCode#31c48347 flags:# via_giveaway:flags.0?true unclaimed:flags.5?true boost_peer:flags.1?Peer days: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;\nmessageActionGiveawayLaunch#a80f51e4 flags:# stars:flags.0?long = MessageAction;\nmessageActionGiveawayResults#87e2f155 flags:# stars:flags.0?true winners_count:int unclaimed_count:int = MessageAction;\nmessageActionBoostApply#cc02aa6d boosts:int = MessageAction;\nmessageActionRequestedPeerSentMe#93b31848 button_id:int peers:Vector<RequestedPeer> = MessageAction;\nmessageActionPaymentRefunded#41b3e202 flags:# peer:Peer currency:string total_amount:long payload:flags.0?bytes charge:PaymentCharge = MessageAction;\nmessageActionGiftStars#45d5b021 flags:# currency:string amount:long stars:long crypto_currency:flags.0?string crypto_amount:flags.0?long transaction_id:flags.1?string = MessageAction;\nmessageActionPrizeStars#b00c47a2 flags:# unclaimed:flags.0?true stars:long transaction_id:string boost_peer:Peer giveaway_msg_id:int = MessageAction;\nmessageActionStarGift#ea2c31d3 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 prepaid_upgrade:flags.13?true upgrade_separate:flags.16?true auction_acquired:flags.17?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 prepaid_upgrade_hash:flags.14?string gift_msg_id:flags.15?int to_id:flags.18?Peer gift_num:flags.19?int = MessageAction;\nmessageActionStarGiftUnique#e6c31522 flags:# upgrade:flags.0?true transferred:flags.1?true saved:flags.2?true refunded:flags.5?true prepaid_upgrade:flags.11?true assigned:flags.13?true from_offer:flags.14?true craft:flags.16?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 resale_amount:flags.8?StarsAmount can_transfer_at:flags.9?int can_resell_at:flags.10?int drop_original_details_stars:flags.12?long can_craft_at:flags.15?int = MessageAction;\nmessageActionPaidMessagesRefunded#ac1f1fcd count:int stars:long = MessageAction;\nmessageActionPaidMessagesPrice#84b88578 flags:# broadcast_messages_allowed:flags.0?true stars:long = MessageAction;\nmessageActionConferenceCall#2ffe2f7a flags:# missed:flags.0?true active:flags.1?true video:flags.4?true call_id:long duration:flags.2?int other_participants:flags.3?Vector<Peer> = MessageAction;\nmessageActionTodoCompletions#cc7c5c89 completed:Vector<int> incompleted:Vector<int> = MessageAction;\nmessageActionTodoAppendTasks#c7edbc83 list:Vector<TodoItem> = MessageAction;\nmessageActionSuggestedPostApproval#ee7a1596 flags:# rejected:flags.0?true balance_too_low:flags.1?true reject_comment:flags.2?string schedule_date:flags.3?int price:flags.4?StarsAmount = MessageAction;\nmessageActionSuggestedPostSuccess#95ddcf69 price:StarsAmount = MessageAction;\nmessageActionSuggestedPostRefund#69f916f8 flags:# payer_initiated:flags.0?true = MessageAction;\nmessageActionGiftTon#a8a3c699 flags:# currency:string amount:long crypto_currency:string crypto_amount:long transaction_id:flags.0?string = MessageAction;\nmessageActionSuggestBirthday#2c8f2a25 birthday:Birthday = MessageAction;\nmessageActionStarGiftPurchaseOffer#774278d4 flags:# accepted:flags.0?true declined:flags.1?true gift:StarGift price:StarsAmount expires_at:int = MessageAction;\nmessageActionStarGiftPurchaseOfferDeclined#73ada76b flags:# expired:flags.0?true gift:StarGift price:StarsAmount = MessageAction;\nmessageActionNewCreatorPending#b07ed085 new_creator_id:long = MessageAction;\nmessageActionChangeCreator#e188503b new_creator_id:long = MessageAction;\n\ndialog#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;\ndialogFolder#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;\n\nphotoEmpty#2331b22d id:long = Photo;\nphoto#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector<PhotoSize> video_sizes:flags.1?Vector<VideoSize> dc_id:int = Photo;\n\nphotoSizeEmpty#e17e23c type:string = PhotoSize;\nphotoSize#75c78e60 type:string w:int h:int size:int = PhotoSize;\nphotoCachedSize#21e1ad6 type:string w:int h:int bytes:bytes = PhotoSize;\nphotoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize;\nphotoSizeProgressive#fa3efb95 type:string w:int h:int sizes:Vector<int> = PhotoSize;\nphotoPathSize#d8214d41 type:string bytes:bytes = PhotoSize;\n\ngeoPointEmpty#1117dd5f = GeoPoint;\ngeoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint;\n\nauth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode;\nauth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode;\nauth.sentCodePaymentRequired#e0955a3c store_product:string phone_code_hash:string support_email_address:string support_email_subject:string currency:string amount:long = auth.SentCode;\n\nauth.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;\nauth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization;\n\nauth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization;\n\ninputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer;\ninputNotifyUsers#193b4417 = InputNotifyPeer;\ninputNotifyChats#4a95e84e = InputNotifyPeer;\ninputNotifyBroadcasts#b1db7c7e = InputNotifyPeer;\ninputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer;\n\ninputPeerNotifySettings#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;\n\npeerNotifySettings#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;\n\npeerSettings#f47741f7 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 charge_paid_message_stars:flags.14?long registration_month:flags.15?string phone_country:flags.16?string name_change_date:flags.17?int photo_change_date:flags.18?int = PeerSettings;\n\nwallPaper#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;\nwallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper;\n\ninputReportReasonSpam#58dbcab8 = ReportReason;\ninputReportReasonViolence#1e22c78d = ReportReason;\ninputReportReasonPornography#2e59d922 = ReportReason;\ninputReportReasonChildAbuse#adf44ee3 = ReportReason;\ninputReportReasonOther#c1e4a2b1 = ReportReason;\ninputReportReasonCopyright#9b89f93a = ReportReason;\ninputReportReasonGeoIrrelevant#dbd4feed = ReportReason;\ninputReportReasonFake#f5ddd6e7 = ReportReason;\ninputReportReasonIllegalDrugs#a8eb2be = ReportReason;\ninputReportReasonPersonalDetails#9ec7863d = ReportReason;\n\nuserFull#a02bc13e 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 display_gifts_button:flags2.16?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:flags.15?ChatTheme private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights 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 send_paid_messages_stars:flags2.14?long disallowed_gifts:flags2.15?DisallowedGiftsSettings stars_rating:flags2.17?StarsRating stars_my_pending_rating:flags2.18?StarsRating stars_my_pending_rating_date:flags2.18?int main_tab:flags2.20?ProfileTab saved_music:flags2.21?Document note:flags2.22?TextWithEntities = UserFull;\n\ncontact#145ade0b user_id:long mutual:Bool = Contact;\n\nimportedContact#c13e3c50 user_id:long client_id:long = ImportedContact;\n\ncontactStatus#16d9703b user_id:long status:UserStatus = ContactStatus;\n\ncontacts.contactsNotModified#b74ba9d2 = contacts.Contacts;\ncontacts.contacts#eae87e42 contacts:Vector<Contact> saved_count:int users:Vector<User> = contacts.Contacts;\n\ncontacts.importedContacts#77d01c3b imported:Vector<ImportedContact> popular_invites:Vector<PopularContact> retry_contacts:Vector<long> users:Vector<User> = contacts.ImportedContacts;\n\ncontacts.blocked#ade1591 blocked:Vector<PeerBlocked> chats:Vector<Chat> users:Vector<User> = contacts.Blocked;\ncontacts.blockedSlice#e1664194 count:int blocked:Vector<PeerBlocked> chats:Vector<Chat> users:Vector<User> = contacts.Blocked;\n\nmessages.dialogs#15ba6c40 dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Dialogs;\nmessages.dialogsSlice#71e094f3 count:int dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.Dialogs;\nmessages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs;\n\nmessages.messages#1d73e7ea messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.messagesSlice#5f206716 flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int search_flood:flags.3?SearchPostsFlood messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector<Message> topics:Vector<ForumTopic> chats:Vector<Chat> users:Vector<User> = messages.Messages;\nmessages.messagesNotModified#74535f21 count:int = messages.Messages;\n\nmessages.chats#64ff9fd5 chats:Vector<Chat> = messages.Chats;\nmessages.chatsSlice#9cd81144 count:int chats:Vector<Chat> = messages.Chats;\n\nmessages.chatFull#e5d7d19c full_chat:ChatFull chats:Vector<Chat> users:Vector<User> = messages.ChatFull;\n\nmessages.affectedHistory#b45c69d1 pts:int pts_count:int offset:int = messages.AffectedHistory;\n\ninputMessagesFilterEmpty#57e2f66c = MessagesFilter;\ninputMessagesFilterPhotos#9609a51c = MessagesFilter;\ninputMessagesFilterVideo#9fc00e65 = MessagesFilter;\ninputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter;\ninputMessagesFilterDocument#9eddf188 = MessagesFilter;\ninputMessagesFilterUrl#7ef0dd87 = MessagesFilter;\ninputMessagesFilterGif#ffc86587 = MessagesFilter;\ninputMessagesFilterVoice#50f5c392 = MessagesFilter;\ninputMessagesFilterMusic#3751b49e = MessagesFilter;\ninputMessagesFilterChatPhotos#3a20ecb8 = MessagesFilter;\ninputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFilter;\ninputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter;\ninputMessagesFilterRoundVideo#b549da53 = MessagesFilter;\ninputMessagesFilterMyMentions#c1f8e69a = MessagesFilter;\ninputMessagesFilterGeo#e7026d0d = MessagesFilter;\ninputMessagesFilterContacts#e062db83 = MessagesFilter;\ninputMessagesFilterPinned#1bb00451 = MessagesFilter;\n\nupdateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update;\nupdateMessageID#4e90bfd6 id:int random_id:long = Update;\nupdateDeleteMessages#a20db0e5 messages:Vector<int> pts:int pts_count:int = Update;\nupdateUserTyping#2a17bf5c flags:# user_id:long top_msg_id:flags.0?int action:SendMessageAction = Update;\nupdateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update;\nupdateChatParticipants#7761198 participants:ChatParticipants = Update;\nupdateUserStatus#e5bdf8de user_id:long status:UserStatus = Update;\nupdateUserName#a7848924 user_id:long first_name:string last_name:string usernames:Vector<Username> = Update;\nupdateNewAuthorization#8951abef flags:# unconfirmed:flags.0?true hash:long date:flags.0?int device:flags.0?string location:flags.0?string = Update;\nupdateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update;\nupdateEncryptedChatTyping#1710f156 chat_id:int = Update;\nupdateEncryption#b4a2e88d chat:EncryptedChat date:int = Update;\nupdateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update;\nupdateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date:int version:int = Update;\nupdateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update;\nupdateDcOptions#8e5e9873 dc_options:Vector<DcOption> = Update;\nupdateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update;\nupdateServiceNotification#ebe46819 flags:# popup:flags.0?true invert_media:flags.2?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector<MessageEntity> = Update;\nupdatePrivacy#ee3b272a key:PrivacyKey rules:Vector<PrivacyRule> = Update;\nupdateUserPhone#5492a13 user_id:long phone:string = Update;\nupdateReadHistoryInbox#9e84bc99 flags:# folder_id:flags.0?int peer:Peer top_msg_id:flags.1?int max_id:int still_unread_count:int pts:int pts_count:int = Update;\nupdateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update;\nupdateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update;\nupdateReadMessagesContents#f8227181 flags:# messages:Vector<int> pts:int pts_count:int date:flags.0?int = Update;\nupdateChannelTooLong#108d941f flags:# channel_id:long pts:flags.0?int = Update;\nupdateChannel#635b4c09 channel_id:long = Update;\nupdateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update;\nupdateReadChannelInbox#922e6e10 flags:# folder_id:flags.0?int channel_id:long max_id:int still_unread_count:int pts:int = Update;\nupdateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector<int> pts:int pts_count:int = Update;\nupdateChannelMessageViews#f226ac08 channel_id:long id:int views:int = Update;\nupdateChatParticipantAdmin#d7ca61a2 chat_id:long user_id:long is_admin:Bool version:int = Update;\nupdateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update;\nupdateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true emojis:flags.1?true order:Vector<long> = Update;\nupdateStickerSets#31c24808 flags:# masks:flags.0?true emojis:flags.1?true = Update;\nupdateSavedGifs#9375341e = Update;\nupdateBotInlineQuery#496f379c flags:# query_id:long user_id:long query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update;\nupdateBotInlineSend#12f12a07 flags:# user_id:long query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update;\nupdateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update;\nupdateBotCallbackQuery#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;\nupdateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update;\nupdateInlineBotCallbackQuery#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;\nupdateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update;\nupdateDraftMessage#edfc111e flags:# peer:Peer top_msg_id:flags.0?int saved_peer_id:flags.1?Peer draft:DraftMessage = Update;\nupdateReadFeaturedStickers#571d2742 = Update;\nupdateRecentStickers#9a422c20 = Update;\nupdateConfig#a229dd06 = Update;\nupdatePtsChanged#3354678f = Update;\nupdateChannelWebPage#2f2ba99f channel_id:long webpage:WebPage pts:int pts_count:int = Update;\nupdateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update;\nupdatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector<DialogPeer> = Update;\nupdateBotWebhookJSON#8317c0c3 data:DataJSON = Update;\nupdateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update;\nupdateBotShippingQuery#b5aefd7d query_id:long user_id:long payload:bytes shipping_address:PostAddress = Update;\nupdateBotPrecheckoutQuery#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;\nupdatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update;\nupdateLangPackTooLong#46560264 lang_code:string = Update;\nupdateLangPack#56022f4d difference:LangPackDifference = Update;\nupdateFavedStickers#e511996d = Update;\nupdateChannelReadMessagesContents#25f324f7 flags:# channel_id:long top_msg_id:flags.0?int saved_peer_id:flags.1?Peer messages:Vector<int> = Update;\nupdateContactsReset#7084a7be = Update;\nupdateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update;\nupdateDialogUnreadMark#b658f23e flags:# unread:flags.0?true peer:DialogPeer saved_peer_id:flags.1?Peer = Update;\nupdateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update;\nupdateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update;\nupdateFolderPeers#19360dc0 folder_peers:Vector<FolderPeer> pts:int pts_count:int = Update;\nupdatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update;\nupdatePeerLocated#b4afcfb0 peers:Vector<PeerLocated> = Update;\nupdateNewScheduledMessage#39a51dfb message:Message = Update;\nupdateDeleteScheduledMessages#f2a71983 flags:# peer:Peer messages:Vector<int> sent_messages:flags.0?Vector<int> = Update;\nupdateTheme#8216fba3 theme:Theme = Update;\nupdateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update;\nupdateLoginToken#564fe691 = Update;\nupdateMessagePollVote#24f40e77 poll_id:long peer:Peer options:Vector<bytes> qts:int = Update;\nupdateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update;\nupdateDialogFilterOrder#a5d72105 order:Vector<int> = Update;\nupdateDialogFilters#3504914f = Update;\nupdatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update;\nupdateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update;\nupdateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update;\nupdateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update;\nupdatePeerBlocked#ebe07752 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer = Update;\nupdateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update;\nupdatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector<int> pts:int pts_count:int = Update;\nupdatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector<int> pts:int pts_count:int = Update;\nupdateChat#f89a6a4e chat_id:long = Update;\nupdateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector<GroupCallParticipant> version:int = Update;\nupdateGroupCall#9d2216e0 flags:# live_story:flags.2?true peer:flags.1?Peer call:GroupCall = Update;\nupdatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update;\nupdateChatParticipant#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;\nupdateChannelParticipant#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;\nupdateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update;\nupdateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update;\nupdateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector<BotCommand> = Update;\nupdatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector<long> = Update;\nupdateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update;\nupdateMessageReactions#1e297bfa flags:# peer:Peer msg_id:int top_msg_id:flags.0?int saved_peer_id:flags.1?Peer reactions:MessageReactions = Update;\nupdateAttachMenuBots#17b7a20b = Update;\nupdateWebViewResultSent#1592b79d query_id:long = Update;\nupdateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update;\nupdateSavedRingtones#74d8be99 = Update;\nupdateTranscribedAudio#84cd5a flags:# pending:flags.0?true peer:Peer msg_id:int transcription_id:long text:string = Update;\nupdateReadFeaturedEmojiStickers#fb4c496c = Update;\nupdateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update;\nupdateRecentEmojiStatuses#30f443db = Update;\nupdateRecentReactions#6f7863f4 = Update;\nupdateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update;\nupdateMessageExtendedMedia#d5a41724 peer:Peer msg_id:int extended_media:Vector<MessageExtendedMedia> = Update;\nupdateUser#20529438 user_id:long = Update;\nupdateAutoSaveSettings#ec05b097 = Update;\nupdateStory#75b3b798 peer:Peer story:StoryItem = Update;\nupdateReadStories#f74e932b peer:Peer max_id:int = Update;\nupdateStoryID#1bf335b9 id:int random_id:long = Update;\nupdateStoriesStealthMode#2c084dc1 stealth_mode:StoriesStealthMode = Update;\nupdateSentStoryReaction#7d627683 peer:Peer story_id:int reaction:Reaction = Update;\nupdateBotChatBoost#904dd49c peer:Peer boost:Boost qts:int = Update;\nupdateChannelViewForumAsMessages#7b68920 channel_id:long enabled:Bool = Update;\nupdatePeerWallpaper#ae3f101d flags:# wallpaper_overridden:flags.1?true peer:Peer wallpaper:flags.0?WallPaper = Update;\nupdateBotMessageReaction#ac21d3ce peer:Peer msg_id:int date:int actor:Peer old_reactions:Vector<Reaction> new_reactions:Vector<Reaction> qts:int = Update;\nupdateBotMessageReactions#9cb7759 peer:Peer msg_id:int date:int reactions:Vector<ReactionCount> qts:int = Update;\nupdateSavedDialogPinned#aeaf9e74 flags:# pinned:flags.0?true peer:DialogPeer = Update;\nupdatePinnedSavedDialogs#686c85a6 flags:# order:flags.0?Vector<DialogPeer> = Update;\nupdateSavedReactionTags#39c67432 = Update;\nupdateSmsJob#f16269d4 job_id:string = Update;\nupdateQuickReplies#f9470ab2 quick_replies:Vector<QuickReply> = Update;\nupdateNewQuickReply#f53da717 quick_reply:QuickReply = Update;\nupdateDeleteQuickReply#53e6f1ec shortcut_id:int = Update;\nupdateQuickReplyMessage#3e050d0f message:Message = Update;\nupdateDeleteQuickReplyMessages#566fe7cd shortcut_id:int messages:Vector<int> = Update;\nupdateBotBusinessConnect#8ae5c97a connection:BotBusinessConnection qts:int = Update;\nupdateBotNewBusinessMessage#9ddb347c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;\nupdateBotEditBusinessMessage#7df587c flags:# connection_id:string message:Message reply_to_message:flags.0?Message qts:int = Update;\nupdateBotDeleteBusinessMessage#a02a982e connection_id:string peer:Peer messages:Vector<int> qts:int = Update;\nupdateNewStoryReaction#1824e40b story_id:int peer:Peer reaction:Reaction = Update;\nupdateStarsBalance#4e80a379 balance:StarsAmount = Update;\nupdateBusinessBotCallbackQuery#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;\nupdateStarsRevenueStatus#a584b019 peer:Peer status:StarsRevenueStatus = Update;\nupdateBotPurchasedPaidMedia#283bd312 user_id:long payload:string qts:int = Update;\nupdatePaidReactionPrivacy#8b725fce private:PaidReactionPrivacy = Update;\nupdateSentPhoneCode#504aa18f sent_code:auth.SentCode = Update;\nupdateGroupCallChainBlocks#a477288f call:InputGroupCall sub_chain_id:int blocks:Vector<bytes> next_offset:int = Update;\nupdateReadMonoForumInbox#77b0e372 channel_id:long saved_peer_id:Peer read_max_id:int = Update;\nupdateReadMonoForumOutbox#a4a79376 channel_id:long saved_peer_id:Peer read_max_id:int = Update;\nupdateMonoForumNoPaidException#9f812b08 flags:# exception:flags.0?true channel_id:long saved_peer_id:Peer = Update;\nupdateGroupCallMessage#d8326f0d call:InputGroupCall message:GroupCallMessage = Update;\nupdateGroupCallEncryptedMessage#c957a766 call:InputGroupCall from_id:Peer encrypted_message:bytes = Update;\nupdatePinnedForumTopic#683b2c52 flags:# pinned:flags.0?true peer:Peer topic_id:int = Update;\nupdatePinnedForumTopics#def143d0 flags:# peer:Peer order:flags.0?Vector<int> = Update;\nupdateDeleteGroupCallMessages#3e85e92c call:InputGroupCall messages:Vector<int> = Update;\nupdateStarGiftAuctionState#48e246c2 gift_id:long state:StarGiftAuctionState = Update;\nupdateStarGiftAuctionUserState#dc58f31e gift_id:long user_state:StarGiftAuctionUserState = Update;\nupdateEmojiGameInfo#fb9c547a info:messages.EmojiGameInfo = Update;\nupdateStarGiftCraftFail#ac072444 = Update;\n\nupdates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State;\n\nupdates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference;\nupdates.difference#f49ca0 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> state:updates.State = updates.Difference;\nupdates.differenceSlice#a8fb1981 new_messages:Vector<Message> new_encrypted_messages:Vector<EncryptedMessage> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> intermediate_state:updates.State = updates.Difference;\nupdates.differenceTooLong#4afe8f6d pts:int = updates.Difference;\n\nupdatesTooLong#e317af7e = Updates;\nupdateShortMessage#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<MessageEntity> ttl_period:flags.25?int = Updates;\nupdateShortChatMessage#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<MessageEntity> ttl_period:flags.25?int = Updates;\nupdateShort#78d4dec1 update:Update date:int = Updates;\nupdatesCombined#725b04c3 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq_start:int seq:int = Updates;\nupdates#74ae4240 updates:Vector<Update> users:Vector<User> chats:Vector<Chat> date:int seq:int = Updates;\nupdateShortSentMessage#9015e101 flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector<MessageEntity> ttl_period:flags.25?int = Updates;\n\nphotos.photos#8dca6aa5 photos:Vector<Photo> users:Vector<User> = photos.Photos;\nphotos.photosSlice#15051f54 count:int photos:Vector<Photo> users:Vector<User> = photos.Photos;\n\nphotos.photo#20212ca8 photo:Photo users:Vector<User> = photos.Photo;\n\nupload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File;\nupload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector<FileHash> = upload.File;\n\ndcOption#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;\n\nconfig#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<DcOption> 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;\n\nnearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc;\n\nhelp.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector<MessageEntity> document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate;\nhelp.noAppUpdate#c45a6536 = help.AppUpdate;\n\nhelp.inviteText#18cb9f78 message:string = help.InviteText;\n\nencryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat;\nencryptedChatWaiting#66b25953 id:int access_hash:long date:int admin_id:long participant_id:long = EncryptedChat;\nencryptedChatRequested#48f1d94c flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:long participant_id:long g_a:bytes = EncryptedChat;\nencryptedChat#61f0d4c7 id:int access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long = EncryptedChat;\nencryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat;\n\ninputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat;\n\nencryptedFileEmpty#c21f497e = EncryptedFile;\nencryptedFile#a8008cd8 id:long access_hash:long size:long dc_id:int key_fingerprint:int = EncryptedFile;\n\ninputEncryptedFileEmpty#1837c364 = InputEncryptedFile;\ninputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile;\ninputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile;\ninputEncryptedFileBigUploaded#2dc173c8 id:long parts:int key_fingerprint:int = InputEncryptedFile;\n\nencryptedMessage#ed18c118 random_id:long chat_id:int date:int bytes:bytes file:EncryptedFile = EncryptedMessage;\nencryptedMessageService#23734b06 random_id:long chat_id:int date:int bytes:bytes = EncryptedMessage;\n\nmessages.dhConfigNotModified#c0e24635 random:bytes = messages.DhConfig;\nmessages.dhConfig#2c221edd g:int p:bytes version:int random:bytes = messages.DhConfig;\n\nmessages.sentEncryptedMessage#560f8935 date:int = messages.SentEncryptedMessage;\nmessages.sentEncryptedFile#9493ff32 date:int file:EncryptedFile = messages.SentEncryptedMessage;\n\ninputDocumentEmpty#72f0eaae = InputDocument;\ninputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument;\n\ndocumentEmpty#36f8c871 id:long = Document;\ndocument#8fd4c4d8 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:long thumbs:flags.0?Vector<PhotoSize> video_thumbs:flags.1?Vector<VideoSize> dc_id:int attributes:Vector<DocumentAttribute> = Document;\n\nhelp.support#17c6b5f6 phone_number:string user:User = help.Support;\n\nnotifyPeer#9fd40bd8 peer:Peer = NotifyPeer;\nnotifyUsers#b4c83b4c = NotifyPeer;\nnotifyChats#c007cec3 = NotifyPeer;\nnotifyBroadcasts#d612e8ef = NotifyPeer;\nnotifyForumTopic#226e6308 peer:Peer top_msg_id:int = NotifyPeer;\n\nsendMessageTypingAction#16bf744e = SendMessageAction;\nsendMessageCancelAction#fd5ec8f5 = SendMessageAction;\nsendMessageRecordVideoAction#a187d66f = SendMessageAction;\nsendMessageUploadVideoAction#e9763aec progress:int = SendMessageAction;\nsendMessageRecordAudioAction#d52f73f7 = SendMessageAction;\nsendMessageUploadAudioAction#f351d7ab progress:int = SendMessageAction;\nsendMessageUploadPhotoAction#d1d34a26 progress:int = SendMessageAction;\nsendMessageUploadDocumentAction#aa0cd9e4 progress:int = SendMessageAction;\nsendMessageGeoLocationAction#176f8ba1 = SendMessageAction;\nsendMessageChooseContactAction#628cbc6f = SendMessageAction;\nsendMessageGamePlayAction#dd6a8f48 = SendMessageAction;\nsendMessageRecordRoundAction#88f27fbc = SendMessageAction;\nsendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction;\nspeakingInGroupCallAction#d92c2285 = SendMessageAction;\nsendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction;\nsendMessageChooseStickerAction#b05ac6b1 = SendMessageAction;\nsendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction;\nsendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction;\nsendMessageTextDraftAction#376d975c random_id:long text:TextWithEntities = SendMessageAction;\n\ncontacts.found#b3134d9d my_results:Vector<Peer> results:Vector<Peer> chats:Vector<Chat> users:Vector<User> = contacts.Found;\n\ninputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey;\ninputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey;\ninputPrivacyKeyPhoneCall#fabadc5f = InputPrivacyKey;\ninputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey;\ninputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey;\ninputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey;\ninputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey;\ninputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey;\ninputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey;\ninputPrivacyKeyAbout#3823cc40 = InputPrivacyKey;\ninputPrivacyKeyBirthday#d65a11cc = InputPrivacyKey;\ninputPrivacyKeyStarGiftsAutoSave#e1732341 = InputPrivacyKey;\ninputPrivacyKeyNoPaidMessages#bdc597b4 = InputPrivacyKey;\ninputPrivacyKeySavedMusic#4dbe9226 = InputPrivacyKey;\n\nprivacyKeyStatusTimestamp#bc2eab30 = PrivacyKey;\nprivacyKeyChatInvite#500e6dfa = PrivacyKey;\nprivacyKeyPhoneCall#3d662b7b = PrivacyKey;\nprivacyKeyPhoneP2P#39491cc8 = PrivacyKey;\nprivacyKeyForwards#69ec56a3 = PrivacyKey;\nprivacyKeyProfilePhoto#96151fed = PrivacyKey;\nprivacyKeyPhoneNumber#d19ae46d = PrivacyKey;\nprivacyKeyAddedByPhone#42ffd42b = PrivacyKey;\nprivacyKeyVoiceMessages#697f414 = PrivacyKey;\nprivacyKeyAbout#a486b761 = PrivacyKey;\nprivacyKeyBirthday#2000a518 = PrivacyKey;\nprivacyKeyStarGiftsAutoSave#2ca4fdf8 = PrivacyKey;\nprivacyKeyNoPaidMessages#17d348d2 = PrivacyKey;\nprivacyKeySavedMusic#ff7a571b = PrivacyKey;\n\ninputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule;\ninputPrivacyValueAllowAll#184b35ce = InputPrivacyRule;\ninputPrivacyValueAllowUsers#131cc67f users:Vector<InputUser> = InputPrivacyRule;\ninputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule;\ninputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule;\ninputPrivacyValueDisallowUsers#90110467 users:Vector<InputUser> = InputPrivacyRule;\ninputPrivacyValueAllowChatParticipants#840649cf chats:Vector<long> = InputPrivacyRule;\ninputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector<long> = InputPrivacyRule;\ninputPrivacyValueAllowCloseFriends#2f453e49 = InputPrivacyRule;\ninputPrivacyValueAllowPremium#77cdc9f1 = InputPrivacyRule;\ninputPrivacyValueAllowBots#5a4fcce5 = InputPrivacyRule;\ninputPrivacyValueDisallowBots#c4e57915 = InputPrivacyRule;\n\nprivacyValueAllowContacts#fffe1bac = PrivacyRule;\nprivacyValueAllowAll#65427b82 = PrivacyRule;\nprivacyValueAllowUsers#b8905fb2 users:Vector<long> = PrivacyRule;\nprivacyValueDisallowContacts#f888fa1a = PrivacyRule;\nprivacyValueDisallowAll#8b73e763 = PrivacyRule;\nprivacyValueDisallowUsers#e4621141 users:Vector<long> = PrivacyRule;\nprivacyValueAllowChatParticipants#6b134e8e chats:Vector<long> = PrivacyRule;\nprivacyValueDisallowChatParticipants#41c87565 chats:Vector<long> = PrivacyRule;\nprivacyValueAllowCloseFriends#f7e8d89b = PrivacyRule;\nprivacyValueAllowPremium#ece9814b = PrivacyRule;\nprivacyValueAllowBots#21461b5d = PrivacyRule;\nprivacyValueDisallowBots#f6a5f82f = PrivacyRule;\n\naccount.privacyRules#50a04e45 rules:Vector<PrivacyRule> chats:Vector<Chat> users:Vector<User> = account.PrivacyRules;\n\naccountDaysTTL#b8d0afdf days:int = AccountDaysTTL;\n\ndocumentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute;\ndocumentAttributeAnimated#11b58939 = DocumentAttribute;\ndocumentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute;\ndocumentAttributeVideo#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;\ndocumentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute;\ndocumentAttributeFilename#15590068 file_name:string = DocumentAttribute;\ndocumentAttributeHasStickers#9801d2f7 = DocumentAttribute;\ndocumentAttributeCustomEmoji#fd149899 flags:# free:flags.0?true text_color:flags.1?true alt:string stickerset:InputStickerSet = DocumentAttribute;\n\nmessages.stickersNotModified#f1749a22 = messages.Stickers;\nmessages.stickers#30a6ec7e hash:long stickers:Vector<Document> = messages.Stickers;\n\nstickerPack#12b299d4 emoticon:string documents:Vector<long> = StickerPack;\n\nmessages.allStickersNotModified#e86602c3 = messages.AllStickers;\nmessages.allStickers#cdbbcebb hash:long sets:Vector<StickerSet> = messages.AllStickers;\n\nmessages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages;\n\nwebPageEmpty#211a1788 flags:# id:long url:flags.0?string = WebPage;\nwebPagePending#b0d13e47 flags:# id:long url:flags.0?string date:int = WebPage;\nwebPage#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<WebPageAttribute> = WebPage;\nwebPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage;\n\nauthorization#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;\n\naccount.authorizations#4bff8ea0 authorization_ttl_days:int authorizations:Vector<Authorization> = account.Authorizations;\n\naccount.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;\n\naccount.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings;\n\naccount.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;\n\nauth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery;\n\nreceivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage;\n\nchatInviteExported#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;\nchatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite;\n\nchatInviteAlready#5a686d7c chat:Chat = ChatInvite;\nchatInvite#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<User> color:int subscription_pricing:flags.10?StarsSubscriptionPricing subscription_form_id:flags.12?long bot_verification:flags.13?BotVerification = ChatInvite;\nchatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite;\n\ninputStickerSetEmpty#ffb62b95 = InputStickerSet;\ninputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet;\ninputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet;\ninputStickerSetAnimatedEmoji#28703c8 = InputStickerSet;\ninputStickerSetDice#e67f520e emoticon:string = InputStickerSet;\ninputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet;\ninputStickerSetPremiumGifts#c88b3b02 = InputStickerSet;\ninputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet;\ninputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet;\ninputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet;\ninputStickerSetEmojiChannelDefaultStatuses#49748553 = InputStickerSet;\ninputStickerSetTonGifts#1cf671a0 = InputStickerSet;\n\nstickerSet#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<PhotoSize> thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet;\n\nmessages.stickerSet#6e153f16 set:StickerSet packs:Vector<StickerPack> keywords:Vector<StickerKeyword> documents:Vector<Document> = messages.StickerSet;\nmessages.stickerSetNotModified#d3f924eb = messages.StickerSet;\n\nbotCommand#c27ac8c7 command:string description:string = BotCommand;\n\nbotInfo#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<BotCommand> menu_button:flags.3?BotMenuButton privacy_policy_url:flags.7?string app_settings:flags.8?BotAppSettings verifier_settings:flags.9?BotVerifierSettings = BotInfo;\n\nkeyboardButton#7d170cff flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonUrl#d80c25ec flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonCallback#e62bc960 flags:# requires_password:flags.0?true style:flags.10?KeyboardButtonStyle text:string data:bytes = KeyboardButton;\nkeyboardButtonRequestPhone#417efd8f flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonRequestGeoLocation#aa40f94d flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonSwitchInline#991399fc flags:# same_peer:flags.0?true style:flags.10?KeyboardButtonStyle text:string query:string peer_types:flags.1?Vector<InlineQueryPeerType> = KeyboardButton;\nkeyboardButtonGame#89c590f9 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonBuy#3fa53905 flags:# style:flags.10?KeyboardButtonStyle text:string = KeyboardButton;\nkeyboardButtonUrlAuth#f51006f9 flags:# style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton;\ninputKeyboardButtonUrlAuth#68013e72 flags:# request_write_access:flags.0?true style:flags.10?KeyboardButtonStyle text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton;\nkeyboardButtonRequestPoll#7a11d782 flags:# style:flags.10?KeyboardButtonStyle quiz:flags.0?Bool text:string = KeyboardButton;\ninputKeyboardButtonUserProfile#7d5e07c7 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:InputUser = KeyboardButton;\nkeyboardButtonUserProfile#c0fd5d09 flags:# style:flags.10?KeyboardButtonStyle text:string user_id:long = KeyboardButton;\nkeyboardButtonWebView#e846b1a0 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonSimpleWebView#e15c4370 flags:# style:flags.10?KeyboardButtonStyle text:string url:string = KeyboardButton;\nkeyboardButtonRequestPeer#5b0f15f5 flags:# style:flags.10?KeyboardButtonStyle text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton;\ninputKeyboardButtonRequestPeer#2b78156 flags:# name_requested:flags.0?true username_requested:flags.1?true photo_requested:flags.2?true style:flags.10?KeyboardButtonStyle text:string button_id:int peer_type:RequestPeerType max_quantity:int = KeyboardButton;\nkeyboardButtonCopy#bcc4af10 flags:# style:flags.10?KeyboardButtonStyle text:string copy_text:string = KeyboardButton;\n\nkeyboardButtonRow#77608b83 buttons:Vector<KeyboardButton> = KeyboardButtonRow;\n\nreplyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup;\nreplyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup;\nreplyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true rows:Vector<KeyboardButtonRow> placeholder:flags.3?string = ReplyMarkup;\nreplyInlineMarkup#48a30254 rows:Vector<KeyboardButtonRow> = ReplyMarkup;\n\nmessageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity;\nmessageEntityMention#fa04579d offset:int length:int = MessageEntity;\nmessageEntityHashtag#6f635b0d offset:int length:int = MessageEntity;\nmessageEntityBotCommand#6cef8ac7 offset:int length:int = MessageEntity;\nmessageEntityUrl#6ed02538 offset:int length:int = MessageEntity;\nmessageEntityEmail#64e475c2 offset:int length:int = MessageEntity;\nmessageEntityBold#bd610bc9 offset:int length:int = MessageEntity;\nmessageEntityItalic#826f8b60 offset:int length:int = MessageEntity;\nmessageEntityCode#28a20571 offset:int length:int = MessageEntity;\nmessageEntityPre#73924be0 offset:int length:int language:string = MessageEntity;\nmessageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity;\nmessageEntityMentionName#dc7b1140 offset:int length:int user_id:long = MessageEntity;\ninputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity;\nmessageEntityPhone#9b69e34b offset:int length:int = MessageEntity;\nmessageEntityCashtag#4c4e743f offset:int length:int = MessageEntity;\nmessageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity;\nmessageEntityStrike#bf0693d4 offset:int length:int = MessageEntity;\nmessageEntityBankCard#761e6af4 offset:int length:int = MessageEntity;\nmessageEntitySpoiler#32ca960f offset:int length:int = MessageEntity;\nmessageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity;\nmessageEntityBlockquote#f1ccaaac flags:# collapsed:flags.0?true offset:int length:int = MessageEntity;\n\ninputChannelEmpty#ee8c1e86 = InputChannel;\ninputChannel#f35aec28 channel_id:long access_hash:long = InputChannel;\ninputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel;\n\ncontacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector<Chat> users:Vector<User> = contacts.ResolvedPeer;\n\nmessageRange#ae30253 min_id:int max_id:int = MessageRange;\n\nupdates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference;\nupdates.channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int dialog:Dialog messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = updates.ChannelDifference;\nupdates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int new_messages:Vector<Message> other_updates:Vector<Update> chats:Vector<Chat> users:Vector<User> = updates.ChannelDifference;\n\nchannelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter;\nchannelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector<MessageRange> = ChannelMessagesFilter;\n\nchannelParticipant#cb397619 flags:# user_id:long date:int subscription_until_date:flags.0?int = ChannelParticipant;\nchannelParticipantSelf#4f607bef flags:# via_request:flags.0?true user_id:long inviter_id:long date:int subscription_until_date:flags.1?int = ChannelParticipant;\nchannelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant;\nchannelParticipantAdmin#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;\nchannelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant;\nchannelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant;\n\nchannelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter;\nchannelParticipantsAdmins#b4608969 = ChannelParticipantsFilter;\nchannelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter;\nchannelParticipantsBots#b0d1865b = ChannelParticipantsFilter;\nchannelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter;\nchannelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter;\nchannelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter;\nchannelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter;\n\nchannels.channelParticipants#9ab0feaf count:int participants:Vector<ChannelParticipant> chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipants;\nchannels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants;\n\nchannels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector<Chat> users:Vector<User> = channels.ChannelParticipant;\n\nhelp.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector<MessageEntity> min_age_confirm:flags.1?int = help.TermsOfService;\n\nmessages.savedGifsNotModified#e8025ca2 = messages.SavedGifs;\nmessages.savedGifs#84a02a0d hash:long gifs:Vector<Document> = messages.SavedGifs;\n\ninputBotInlineMessageMediaAuto#3380c786 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaGeo#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;\ninputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\ninputBotInlineMessageMediaInvoice#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;\ninputBotInlineMessageMediaWebPage#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<MessageEntity> url:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage;\n\ninputBotInlineResult#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;\ninputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult;\ninputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult;\ninputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult;\n\nbotInlineMessageMediaAuto#764cf810 flags:# invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true invert_media:flags.3?true message:string entities:flags.1?Vector<MessageEntity> reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaGeo#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\nbotInlineMessageMediaInvoice#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;\nbotInlineMessageMediaWebPage#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<MessageEntity> url:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage;\n\nbotInlineResult#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;\nbotInlineMediaResult#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;\n\nmessages.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<BotInlineResult> cache_time:int users:Vector<User> = messages.BotResults;\n\nexportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink;\n\nmessageFwdHeader#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;\n\nauth.codeTypeSms#72a3158c = auth.CodeType;\nauth.codeTypeCall#741cd3e3 = auth.CodeType;\nauth.codeTypeFlashCall#226ccefb = auth.CodeType;\nauth.codeTypeMissedCall#d61ad6ee = auth.CodeType;\nauth.codeTypeFragmentSms#6ed998c = auth.CodeType;\n\nauth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType;\nauth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType;\nauth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType;\nauth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType;\nauth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType;\nauth.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;\nauth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType;\nauth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType;\nauth.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;\nauth.sentCodeTypeSmsWord#a416ac81 flags:# beginning:flags.0?string = auth.SentCodeType;\nauth.sentCodeTypeSmsPhrase#b37794af flags:# beginning:flags.0?string = auth.SentCodeType;\n\nmessages.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;\n\nmessages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;\n\ninputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;\ninputBotInlineMessageID64#b6d915d7 dc_id:int owner_id:long id:int access_hash:long = InputBotInlineMessageID;\n\ninlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM;\n\nmessages.peerDialogs#3371c354 dialogs:Vector<Dialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> state:updates.State = messages.PeerDialogs;\n\ntopPeer#edcdc05b peer:Peer rating:double = TopPeer;\n\ntopPeerCategoryBotsPM#ab661b5b = TopPeerCategory;\ntopPeerCategoryBotsInline#148677e2 = TopPeerCategory;\ntopPeerCategoryCorrespondents#637b7ed = TopPeerCategory;\ntopPeerCategoryGroups#bd17a14a = TopPeerCategory;\ntopPeerCategoryChannels#161d9628 = TopPeerCategory;\ntopPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory;\ntopPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory;\ntopPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory;\ntopPeerCategoryBotsApp#fd9e7bec = TopPeerCategory;\n\ntopPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector<TopPeer> = TopPeerCategoryPeers;\n\ncontacts.topPeersNotModified#de266ef5 = contacts.TopPeers;\ncontacts.topPeers#70b772a8 categories:Vector<TopPeerCategoryPeers> chats:Vector<Chat> users:Vector<User> = contacts.TopPeers;\ncontacts.topPeersDisabled#b52c939d = contacts.TopPeers;\n\ndraftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage;\ndraftMessage#96eaa5eb flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia date:int effect:flags.7?long suggested_post:flags.8?SuggestedPost = DraftMessage;\n\nmessages.featuredStickersNotModified#c6dc0c66 count:int = messages.FeaturedStickers;\nmessages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector<StickerSetCovered> unread:Vector<long> = messages.FeaturedStickers;\n\nmessages.recentStickersNotModified#b17f890 = messages.RecentStickers;\nmessages.recentStickers#88d37c56 hash:long packs:Vector<StickerPack> stickers:Vector<Document> dates:Vector<int> = messages.RecentStickers;\n\nmessages.archivedStickers#4fcba9c8 count:int sets:Vector<StickerSetCovered> = messages.ArchivedStickers;\n\nmessages.stickerSetInstallResultSuccess#38641628 = messages.StickerSetInstallResult;\nmessages.stickerSetInstallResultArchive#35e410a8 sets:Vector<StickerSetCovered> = messages.StickerSetInstallResult;\n\nstickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered;\nstickerSetMultiCovered#3407e51b set:StickerSet covers:Vector<Document> = StickerSetCovered;\nstickerSetFullCovered#40d13c0e set:StickerSet packs:Vector<StickerPack> keywords:Vector<StickerKeyword> documents:Vector<Document> = StickerSetCovered;\nstickerSetNoCovered#77b15d1c set:StickerSet = StickerSetCovered;\n\nmaskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;\n\ninputStickeredMediaPhoto#4a992157 id:InputPhoto = InputStickeredMedia;\ninputStickeredMediaDocument#438865b id:InputDocument = InputStickeredMedia;\n\ngame#bdf9653b flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document = Game;\n\ninputGameID#32c3e77 id:long access_hash:long = InputGame;\ninputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame;\n\nhighScore#73a379eb pos:int user_id:long score:int = HighScore;\n\nmessages.highScores#9a3bfd99 scores:Vector<HighScore> users:Vector<User> = messages.HighScores;\n\ntextEmpty#dc3d824f = RichText;\ntextPlain#744694e0 text:string = RichText;\ntextBold#6724abc4 text:RichText = RichText;\ntextItalic#d912a59c text:RichText = RichText;\ntextUnderline#c12622c4 text:RichText = RichText;\ntextStrike#9bf8bb95 text:RichText = RichText;\ntextFixed#6c3f19b9 text:RichText = RichText;\ntextUrl#3c2884c1 text:RichText url:string webpage_id:long = RichText;\ntextEmail#de5a0dd6 text:RichText email:string = RichText;\ntextConcat#7e6260d7 texts:Vector<RichText> = RichText;\ntextSubscript#ed6a8504 text:RichText = RichText;\ntextSuperscript#c7fb5e01 text:RichText = RichText;\ntextMarked#34b8621 text:RichText = RichText;\ntextPhone#1ccb966a text:RichText phone:string = RichText;\ntextImage#81ccf4f document_id:long w:int h:int = RichText;\ntextAnchor#35553762 text:RichText name:string = RichText;\n\npageBlockUnsupported#13567e8a = PageBlock;\npageBlockTitle#70abc3fd text:RichText = PageBlock;\npageBlockSubtitle#8ffa9a1f text:RichText = PageBlock;\npageBlockAuthorDate#baafe5e0 author:RichText published_date:int = PageBlock;\npageBlockHeader#bfd064ec text:RichText = PageBlock;\npageBlockSubheader#f12bb6e1 text:RichText = PageBlock;\npageBlockParagraph#467a0766 text:RichText = PageBlock;\npageBlockPreformatted#c070d93e text:RichText language:string = PageBlock;\npageBlockFooter#48870999 text:RichText = PageBlock;\npageBlockDivider#db20b188 = PageBlock;\npageBlockAnchor#ce0d37b0 name:string = PageBlock;\npageBlockList#e4e88011 items:Vector<PageListItem> = PageBlock;\npageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock;\npageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock;\npageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock;\npageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock;\npageBlockCover#39f23300 cover:PageBlock = PageBlock;\npageBlockEmbed#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;\npageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockCollage#65a0fa4d items:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockSlideshow#31f9590 items:Vector<PageBlock> caption:PageCaption = PageBlock;\npageBlockChannel#ef1751b5 channel:Chat = PageBlock;\npageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock;\npageBlockKicker#1e148390 text:RichText = PageBlock;\npageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector<PageTableRow> = PageBlock;\npageBlockOrderedList#9a8ae1e1 items:Vector<PageListOrderedItem> = PageBlock;\npageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector<PageBlock> title:RichText = PageBlock;\npageBlockRelatedArticles#16115a96 title:RichText articles:Vector<PageRelatedArticle> = PageBlock;\npageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock;\n\nphoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason;\nphoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason;\nphoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason;\nphoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason;\nphoneCallDiscardReasonMigrateConferenceCall#9fbbf1f7 slug:string = PhoneCallDiscardReason;\n\ndataJSON#7d748d04 data:string = DataJSON;\n\nlabeledPrice#cb296bf8 label:string amount:long = LabeledPrice;\n\ninvoice#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<LabeledPrice> max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector<long> terms_url:flags.10?string subscription_period:flags.11?int = Invoice;\n\npaymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge;\n\npostAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress;\n\npaymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo;\n\npaymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials;\n\nwebDocument#1c570ed1 url:string access_hash:long size:int mime_type:string attributes:Vector<DocumentAttribute> = WebDocument;\nwebDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector<DocumentAttribute> = WebDocument;\n\ninputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector<DocumentAttribute> = InputWebDocument;\n\ninputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation;\ninputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w:int h:int zoom:int scale:int = InputWebFileLocation;\ninputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document:flags.0?InputDocument title:flags.1?string performer:flags.1?string = InputWebFileLocation;\n\nupload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile;\n\npayments.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<PaymentFormMethod> saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector<PaymentSavedCredentials> users:Vector<User> = payments.PaymentForm;\npayments.paymentFormStars#7bf6b15c flags:# form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice users:Vector<User> = payments.PaymentForm;\npayments.paymentFormStarGift#b425cfe1 form_id:long invoice:Invoice = payments.PaymentForm;\n\npayments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = payments.ValidatedRequestedInfo;\n\npayments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult;\npayments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult;\n\npayments.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<User> = payments.PaymentReceipt;\npayments.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<User> = payments.PaymentReceipt;\n\npayments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo;\n\ninputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials;\ninputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials;\ninputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials;\ninputPaymentCredentialsGooglePay#8ac32801 payment_token:DataJSON = InputPaymentCredentials;\n\naccount.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword;\n\nshippingOption#b6213cdf id:string title:string prices:Vector<LabeledPrice> = ShippingOption;\n\ninputStickerSetItem#32da9e9c flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords keywords:flags.1?string = InputStickerSetItem;\n\ninputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall;\n\nphoneCallEmpty#5366c915 id:long = PhoneCall;\nphoneCallWaiting#c5226f17 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 = PhoneCall;\nphoneCallRequested#14b0ed0c flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall;\nphoneCallAccepted#3660c311 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol = PhoneCall;\nphoneCall#30535af5 flags:# p2p_allowed:flags.5?true video:flags.6?true conference_supported:flags.8?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<PhoneConnection> start_date:int custom_parameters:flags.7?DataJSON = PhoneCall;\nphoneCallDiscarded#50ca4de1 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 = PhoneCall;\n\nphoneConnection#9cc123c7 flags:# tcp:flags.0?true id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection;\nphoneConnectionWebrtc#635fe375 flags:# turn:flags.0?true stun:flags.1?true id:long ip:string ipv6:string port:int username:string password:string = PhoneConnection;\n\nphoneCallProtocol#fc878fc8 flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int library_versions:Vector<string> = PhoneCallProtocol;\n\nphone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector<User> = phone.PhoneCall;\n\nupload.cdnFileReuploadNeeded#eea8e46e request_token:bytes = upload.CdnFile;\nupload.cdnFile#a99fca4f bytes:bytes = upload.CdnFile;\n\ncdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey;\n\ncdnConfig#5725e40a public_keys:Vector<CdnPublicKey> = CdnConfig;\n\nlangPackString#cad181f6 key:string value:string = LangPackString;\nlangPackStringPluralized#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;\nlangPackStringDeleted#2979eeb2 key:string = LangPackString;\n\nlangPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector<LangPackString> = LangPackDifference;\n\nlangPackLanguage#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;\n\nchannelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangePhoto#434bd2af prev_photo:Photo new_photo:Photo = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction;\nchannelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction;\nchannelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeLinkedChat#50c7ac8 prev_value:long new_value:long = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction;\nchannelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDiscardGroupCall#db9f9140 call:InputGroupCall = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantMute#f92424d2 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantUnmute#e64429c0 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleGroupCallSetting#56d6a247 join_muted:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoinByInvite#fe9fc158 flags:# via_chatlist:flags.0?true invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteDelete#5a50fca4 invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector<string> new_value:Vector<string> = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangePeerColor#5796e780 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeProfilePeerColor#5e477b25 prev_value:PeerColor new_value:PeerColor = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeWallpaper#31bb5d52 prev_value:WallPaper new_value:WallPaper = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeEmojiStatus#3ea9feb1 prev_value:EmojiStatus new_value:EmojiStatus = ChannelAdminLogEventAction;\nchannelAdminLogEventActionChangeEmojiStickerSet#46d840ab prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleSignatureProfiles#60a79c79 new_value:Bool = ChannelAdminLogEventAction;\nchannelAdminLogEventActionParticipantSubExtend#64642db3 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction;\nchannelAdminLogEventActionToggleAutotranslation#c517f77e new_value:Bool = ChannelAdminLogEventAction;\n\nchannelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent;\n\nchannels.adminLogResults#ed8af74d events:Vector<ChannelAdminLogEvent> chats:Vector<Chat> users:Vector<User> = channels.AdminLogResults;\n\nchannelAdminLogEventsFilter#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;\n\npopularContact#5ce14175 client_id:long importers:int = PopularContact;\n\nmessages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers;\nmessages.favedStickers#2cb51097 hash:long packs:Vector<StickerPack> stickers:Vector<Document> = messages.FavedStickers;\n\nrecentMeUrlUnknown#46e1d13d url:string = RecentMeUrl;\nrecentMeUrlUser#b92c09e2 url:string user_id:long = RecentMeUrl;\nrecentMeUrlChat#b2da71d2 url:string chat_id:long = RecentMeUrl;\nrecentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl;\nrecentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl;\n\nhelp.recentMeUrls#e0310d7 urls:Vector<RecentMeUrl> chats:Vector<Chat> users:Vector<User> = help.RecentMeUrls;\n\ninputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector<MessageEntity> = InputSingleMedia;\n\nwebAuthorization#a6f8f452 hash:long bot_id:long domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization;\n\naccount.webAuthorizations#ed56c9fc authorizations:Vector<WebAuthorization> users:Vector<User> = account.WebAuthorizations;\n\ninputMessageID#a676a322 id:int = InputMessage;\ninputMessageReplyTo#bad88395 id:int = InputMessage;\ninputMessagePinned#86872538 = InputMessage;\ninputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage;\n\ninputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer;\ninputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer;\n\ndialogPeer#e56dbf05 peer:Peer = DialogPeer;\ndialogPeerFolder#514519e2 folder_id:int = DialogPeer;\n\nmessages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets;\nmessages.foundStickerSets#8af09dd2 hash:long sets:Vector<StickerSetCovered> = messages.FoundStickerSets;\n\nfileHash#f39b035c offset:long limit:int hash:bytes = FileHash;\n\ninputClientProxy#75588b3f address:string port:int = InputClientProxy;\n\nhelp.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate;\nhelp.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate;\n\ninputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile;\ninputSecureFile#5367e5be id:long access_hash:long = InputSecureFile;\n\nsecureFileEmpty#64199744 = SecureFile;\nsecureFile#7d09c27e id:long access_hash:long size:long dc_id:int date:int file_hash:bytes secret:bytes = SecureFile;\n\nsecureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData;\n\nsecurePlainPhone#7d6099dd phone:string = SecurePlainData;\nsecurePlainEmail#21ec5a5f email:string = SecurePlainData;\n\nsecureValueTypePersonalDetails#9d2a81e3 = SecureValueType;\nsecureValueTypePassport#3dac6a00 = SecureValueType;\nsecureValueTypeDriverLicense#6e425c4 = SecureValueType;\nsecureValueTypeIdentityCard#a0d0744b = SecureValueType;\nsecureValueTypeInternalPassport#99a48f23 = SecureValueType;\nsecureValueTypeAddress#cbe31e26 = SecureValueType;\nsecureValueTypeUtilityBill#fc36954e = SecureValueType;\nsecureValueTypeBankStatement#89137c0d = SecureValueType;\nsecureValueTypeRentalAgreement#8b883488 = SecureValueType;\nsecureValueTypePassportRegistration#99e3806a = SecureValueType;\nsecureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType;\nsecureValueTypePhone#b320aadb = SecureValueType;\nsecureValueTypeEmail#8e3ca7ee = SecureValueType;\n\nsecureValue#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<SecureFile> files:flags.4?Vector<SecureFile> plain_data:flags.5?SecurePlainData hash:bytes = SecureValue;\n\ninputSecureValue#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<InputSecureFile> files:flags.4?Vector<InputSecureFile> plain_data:flags.5?SecurePlainData = InputSecureValue;\n\nsecureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash;\n\nsecureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError;\nsecureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;\nsecureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError;\nsecureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError;\nsecureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector<bytes> text:string = SecureValueError;\n\nsecureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted;\n\naccount.authorizationForm#ad2e1cd8 flags:# required_types:Vector<SecureRequiredType> values:Vector<SecureValue> errors:Vector<SecureValueError> users:Vector<User> privacy_policy_url:flags.0?string = account.AuthorizationForm;\n\naccount.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode;\n\nhelp.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo;\nhelp.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector<MessageEntity> = help.DeepLinkInfo;\n\nsavedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact;\n\naccount.takeout#4dba4501 id:long = account.Takeout;\n\npasswordKdfAlgoUnknown#d45ab096 = PasswordKdfAlgo;\npasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow#3a912d4a salt1:bytes salt2:bytes g:int p:bytes = PasswordKdfAlgo;\n\nsecurePasswordKdfAlgoUnknown#4a8537 = SecurePasswordKdfAlgo;\nsecurePasswordKdfAlgoPBKDF2HMACSHA512iter100000#bbf2dda0 salt:bytes = SecurePasswordKdfAlgo;\nsecurePasswordKdfAlgoSHA512#86471d92 salt:bytes = SecurePasswordKdfAlgo;\n\nsecureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:bytes secure_secret_id:long = SecureSecretSettings;\n\ninputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP;\ninputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP;\n\nsecureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType;\nsecureRequiredTypeOneOf#27477b4 types:Vector<SecureRequiredType> = SecureRequiredType;\n\nhelp.passportConfigNotModified#bfb9f457 = help.PassportConfig;\nhelp.passportConfig#a098d6af hash:int countries_langs:DataJSON = help.PassportConfig;\n\ninputAppEvent#1d1b1245 time:double type:string peer:long data:JSONValue = InputAppEvent;\n\njsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue;\n\njsonNull#3f6d7b68 = JSONValue;\njsonBool#c7345e6a value:Bool = JSONValue;\njsonNumber#2be0dfa4 value:double = JSONValue;\njsonString#b71e767a value:string = JSONValue;\njsonArray#f7444763 value:Vector<JSONValue> = JSONValue;\njsonObject#99c1d49d value:Vector<JSONObjectValue> = JSONValue;\n\npageTableCell#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;\n\npageTableRow#e0c0c5e5 cells:Vector<PageTableCell> = PageTableRow;\n\npageCaption#6f747657 text:RichText credit:RichText = PageCaption;\n\npageListItemText#b92fb6cd text:RichText = PageListItem;\npageListItemBlocks#25e073fc blocks:Vector<PageBlock> = PageListItem;\n\npageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem;\npageListOrderedItemBlocks#98dd8936 num:string blocks:Vector<PageBlock> = PageListOrderedItem;\n\npageRelatedArticle#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;\n\npage#98657f0d flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector<PageBlock> photos:Vector<Photo> documents:Vector<Document> views:flags.3?int = Page;\n\nhelp.supportName#8c05f1c9 name:string = help.SupportName;\n\nhelp.userInfoEmpty#f3ae2eed = help.UserInfo;\nhelp.userInfo#1eb3758 message:string entities:Vector<MessageEntity> author:string date:int = help.UserInfo;\n\npollAnswer#ff16e2ca text:TextWithEntities option:bytes = PollAnswer;\n\npoll#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<PollAnswer> close_period:flags.4?int close_date:flags.5?int = Poll;\n\npollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters;\n\npollResults#7adf2420 flags:# min:flags.0?true results:flags.1?Vector<PollAnswerVoters> total_voters:flags.2?int recent_voters:flags.3?Vector<Peer> solution:flags.4?string solution_entities:flags.4?Vector<MessageEntity> = PollResults;\n\nchatOnlines#f041e250 onlines:int = ChatOnlines;\n\nstatsURL#47a971e0 url:string = StatsURL;\n\nchatAdminRights#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 manage_direct_messages:flags.17?true = ChatAdminRights;\n\nchatBannedRights#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;\n\ninputWallPaper#e630b979 id:long access_hash:long = InputWallPaper;\ninputWallPaperSlug#72091c80 slug:string = InputWallPaper;\ninputWallPaperNoFile#967a462e id:long = InputWallPaper;\n\naccount.wallPapersNotModified#1c199183 = account.WallPapers;\naccount.wallPapers#cdc3858c hash:long wallpapers:Vector<WallPaper> = account.WallPapers;\n\ncodeSettings#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<bytes> token:flags.8?string app_sandbox:flags.8?Bool = CodeSettings;\n\nwallPaperSettings#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;\n\nautoDownloadSettings#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;\n\naccount.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings;\n\nemojiKeyword#d5b3b9f9 keyword:string emoticons:Vector<string> = EmojiKeyword;\nemojiKeywordDeleted#236df622 keyword:string emoticons:Vector<string> = EmojiKeyword;\n\nemojiKeywordsDifference#5cc761bd lang_code:string from_version:int version:int keywords:Vector<EmojiKeyword> = EmojiKeywordsDifference;\n\nemojiURL#a575739d url:string = EmojiURL;\n\nemojiLanguage#b3fb5361 lang_code:string = EmojiLanguage;\n\nfolder#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;\n\ninputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer;\n\nfolderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer;\n\nmessages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter;\n\nurlAuthResultRequest#32fabf1a flags:# request_write_access:flags.0?true request_phone_number:flags.1?true bot:User domain:string browser:flags.2?string platform:flags.2?string ip:flags.2?string region:flags.2?string = UrlAuthResult;\nurlAuthResultAccepted#623a8fa0 flags:# url:flags.0?string = UrlAuthResult;\nurlAuthResultDefault#a9d6db1f = UrlAuthResult;\n\nchannelLocationEmpty#bfb5ad8b = ChannelLocation;\nchannelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation;\n\npeerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated;\npeerSelfLocated#f8ec284b expires:int = PeerLocated;\n\nrestrictionReason#d072acb4 platform:string reason:string text:string = RestrictionReason;\n\ninputTheme#3c5693e9 id:long access_hash:long = InputTheme;\ninputThemeSlug#f5890df1 slug:string = InputTheme;\n\ntheme#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<ThemeSettings> emoticon:flags.6?string installs_count:flags.4?int = Theme;\n\naccount.themesNotModified#f41eb622 = account.Themes;\naccount.themes#9a3d8c6d hash:long themes:Vector<Theme> = account.Themes;\n\nauth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken;\nauth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken;\nauth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken;\n\naccount.contentSettings#57e28221 flags:# sensitive_enabled:flags.0?true sensitive_can_change:flags.1?true = account.ContentSettings;\n\nmessages.inactiveChats#a927fec5 dates:Vector<int> chats:Vector<Chat> users:Vector<User> = messages.InactiveChats;\n\nbaseThemeClassic#c3a12462 = BaseTheme;\nbaseThemeDay#fbd81688 = BaseTheme;\nbaseThemeNight#b7b31ea8 = BaseTheme;\nbaseThemeTinted#6d5f77ee = BaseTheme;\nbaseThemeArctic#5b11125a = BaseTheme;\n\ninputThemeSettings#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<int> wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings;\n\nthemeSettings#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<int> wallpaper:flags.1?WallPaper = ThemeSettings;\n\nwebPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector<Document> settings:flags.1?ThemeSettings = WebPageAttribute;\nwebPageAttributeStory#2e94c3e7 flags:# peer:Peer id:int story:flags.0?StoryItem = WebPageAttribute;\nwebPageAttributeStickerSet#50cc03d3 flags:# emojis:flags.0?true text_color:flags.1?true stickers:Vector<Document> = WebPageAttribute;\nwebPageAttributeUniqueStarGift#cf6f6db8 gift:StarGift = WebPageAttribute;\nwebPageAttributeStarGiftCollection#31cad303 icons:Vector<Document> = WebPageAttribute;\nwebPageAttributeStarGiftAuction#1c641c2 gift:StarGift end_date:int = WebPageAttribute;\n\nmessages.votesList#4899484e flags:# count:int votes:Vector<MessagePeerVote> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = messages.VotesList;\n\nbankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl;\n\npayments.bankCardData#3e24e573 title:string open_urls:Vector<BankCardOpenUrl> = payments.BankCardData;\n\ndialogFilter#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<InputPeer> include_peers:Vector<InputPeer> exclude_peers:Vector<InputPeer> = DialogFilter;\ndialogFilterDefault#363293ae = DialogFilter;\ndialogFilterChatlist#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<InputPeer> include_peers:Vector<InputPeer> = DialogFilter;\n\ndialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested;\n\nstatsDateRangeDays#b637edaf min_date:int max_date:int = StatsDateRangeDays;\n\nstatsAbsValueAndPrev#cb43acde current:double previous:double = StatsAbsValueAndPrev;\n\nstatsPercentValue#cbce2fe0 part:double total:double = StatsPercentValue;\n\nstatsGraphAsync#4a27eb2d token:string = StatsGraph;\nstatsGraphError#bedc9822 error:string = StatsGraph;\nstatsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph;\n\nstats.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<PostInteractionCounters> = stats.BroadcastStats;\n\nhelp.promoDataEmpty#98f6ac75 expires:int = help.PromoData;\nhelp.promoData#8a4d87a flags:# proxy:flags.0?true expires:int peer:flags.3?Peer psa_type:flags.1?string psa_message:flags.2?string pending_suggestions:Vector<string> dismissed_suggestions:Vector<string> custom_pending_suggestion:flags.4?PendingSuggestion chats:Vector<Chat> users:Vector<User> = help.PromoData;\n\nvideoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize;\nvideoSizeEmojiMarkup#f85c413c emoji_id:long background_colors:Vector<int> = VideoSize;\nvideoSizeStickerMarkup#da082fe stickerset:InputStickerSet sticker_id:long background_colors:Vector<int> = VideoSize;\n\nstatsGroupTopPoster#9d04af9b user_id:long messages:int avg_chars:int = StatsGroupTopPoster;\n\nstatsGroupTopAdmin#d7584c87 user_id:long deleted:int kicked:int banned:int = StatsGroupTopAdmin;\n\nstatsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInviter;\n\nstats.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<StatsGroupTopPoster> top_admins:Vector<StatsGroupTopAdmin> top_inviters:Vector<StatsGroupTopInviter> users:Vector<User> = stats.MegagroupStats;\n\nglobalPrivacySettings#fe41b34f 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 display_gifts_button:flags.7?true noncontact_peers_paid_stars:flags.5?long disallowed_gifts:flags.6?DisallowedGiftsSettings = GlobalPrivacySettings;\n\nhelp.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector<string> patterns:flags.1?Vector<string> = help.CountryCode;\n\nhelp.country#c3878e23 flags:# hidden:flags.0?true iso2:string default_name:string name:flags.1?string country_codes:Vector<help.CountryCode> = help.Country;\n\nhelp.countriesListNotModified#93cc1f32 = help.CountriesList;\nhelp.countriesList#87d0759e countries:Vector<help.Country> hash:int = help.CountriesList;\n\nmessageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:flags.2?MessageReplies = MessageViews;\n\nmessages.messageViews#b6c4f543 views:Vector<MessageViews> chats:Vector<Chat> users:Vector<User> = messages.MessageViews;\n\nmessages.discussionMessage#a6341782 flags:# messages:Vector<Message> max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector<Chat> users:Vector<User> = messages.DiscussionMessage;\n\nmessageReplyHeader#6917560b 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<MessageEntity> quote_offset:flags.10?int todo_item_id:flags.11?int = MessageReplyHeader;\nmessageReplyStoryHeader#e5af939 peer:Peer story_id:int = MessageReplyHeader;\n\nmessageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector<Peer> channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies;\n\npeerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked;\n\nstats.messageStats#7fe91c14 views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.MessageStats;\n\ngroupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall;\ngroupCall#efb2b617 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 conference:flags.14?true creator:flags.15?true messages_enabled:flags.17?true can_change_messages_enabled:flags.18?true min:flags.19?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 invite_link:flags.16?string send_paid_messages_stars:flags.20?long default_send_as:flags.21?Peer = GroupCall;\n\ninputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall;\ninputGroupCallSlug#fe06823f slug:string = InputGroupCall;\ninputGroupCallInviteMessage#8c10603f msg_id:int = InputGroupCall;\n\ngroupCallParticipant#2a3dc7ac 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 paid_stars_total:flags.16?long = GroupCallParticipant;\n\nphone.groupCall#9e727aad call:GroupCall participants:Vector<GroupCallParticipant> participants_next_offset:string chats:Vector<Chat> users:Vector<User> = phone.GroupCall;\n\nphone.groupParticipants#f47751b6 count:int participants:Vector<GroupCallParticipant> next_offset:string chats:Vector<Chat> users:Vector<User> version:int = phone.GroupParticipants;\n\ninlineQueryPeerTypeSameBotPM#3081ed9d = InlineQueryPeerType;\ninlineQueryPeerTypePM#833c0fac = InlineQueryPeerType;\ninlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType;\ninlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType;\ninlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType;\ninlineQueryPeerTypeBotPM#e3b2d0c = InlineQueryPeerType;\n\nmessages.historyImport#1662af0b id:long = messages.HistoryImport;\n\nmessages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true title:flags.2?string = messages.HistoryImportParsed;\n\nmessages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector<int> = messages.AffectedFoundMessages;\n\nchatInviteImporter#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;\n\nmessages.exportedChatInvites#bdc62dcc count:int invites:Vector<ExportedChatInvite> users:Vector<User> = messages.ExportedChatInvites;\n\nmessages.exportedChatInvite#1871be50 invite:ExportedChatInvite users:Vector<User> = messages.ExportedChatInvite;\nmessages.exportedChatInviteReplaced#222600ef invite:ExportedChatInvite new_invite:ExportedChatInvite users:Vector<User> = messages.ExportedChatInvite;\n\nmessages.chatInviteImporters#81b6b00a count:int importers:Vector<ChatInviteImporter> users:Vector<User> = messages.ChatInviteImporters;\n\nchatAdminWithInvites#f2ecef23 admin_id:long invites_count:int revoked_invites_count:int = ChatAdminWithInvites;\n\nmessages.chatAdminsWithInvites#b69b72d7 admins:Vector<ChatAdminWithInvites> users:Vector<User> = messages.ChatAdminsWithInvites;\n\nmessages.checkedHistoryImportPeer#a24de717 confirm_text:string = messages.CheckedHistoryImportPeer;\n\nphone.joinAsPeers#afe5623f peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = phone.JoinAsPeers;\n\nphone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvite;\n\ngroupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector<int> = GroupCallParticipantVideoSourceGroup;\n\ngroupCallParticipantVideo#67753ac8 flags:# paused:flags.0?true endpoint:string source_groups:Vector<GroupCallParticipantVideoSourceGroup> audio_source:flags.1?int = GroupCallParticipantVideo;\n\nstickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName;\n\nbotCommandScopeDefault#2f6cb2ab = BotCommandScope;\nbotCommandScopeUsers#3c4f04d8 = BotCommandScope;\nbotCommandScopeChats#6fe1a881 = BotCommandScope;\nbotCommandScopeChatAdmins#b9aa606a = BotCommandScope;\nbotCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope;\nbotCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope;\nbotCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope;\n\naccount.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult;\naccount.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult;\naccount.resetPasswordOk#e926d63e = account.ResetPasswordResult;\n\nchatTheme#c3dffc04 emoticon:string = ChatTheme;\nchatThemeUniqueGift#3458f9c8 gift:StarGift theme_settings:Vector<ThemeSettings> = ChatTheme;\n\naccount.chatThemesNotModified#e011e1c4 = account.ChatThemes;\naccount.chatThemes#be098173 flags:# hash:long themes:Vector<ChatTheme> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = account.ChatThemes;\n\nsponsoredMessage#7dbf8673 flags:# recommended:flags.5?true can_report:flags.12?true random_id:bytes url:string title:string message:string entities:flags.1?Vector<MessageEntity> 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 min_display_duration:flags.15?int max_display_duration:flags.15?int = SponsoredMessage;\n\nmessages.sponsoredMessages#ffda656d flags:# posts_between:flags.0?int start_delay:flags.1?int between_delay:flags.2?int messages:Vector<SponsoredMessage> chats:Vector<Chat> users:Vector<User> = messages.SponsoredMessages;\nmessages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages;\n\nsearchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod;\n\nmessages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector<SearchResultsCalendarPeriod> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SearchResultsCalendar;\n\nsearchResultPosition#7f648b67 msg_id:int date:int offset:int = SearchResultsPosition;\n\nmessages.searchResultsPositions#53b22baf count:int positions:Vector<SearchResultsPosition> = messages.SearchResultsPositions;\n\nchannels.sendAsPeers#f496b0c6 peers:Vector<SendAsPeer> chats:Vector<Chat> users:Vector<User> = channels.SendAsPeers;\n\nusers.userFull#3b6d152e full_user:UserFull chats:Vector<Chat> users:Vector<User> = users.UserFull;\n\nmessages.peerSettings#6880b94d settings:PeerSettings chats:Vector<Chat> users:Vector<User> = messages.PeerSettings;\n\nauth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut;\n\nreactionCount#a3d1cb80 flags:# chosen_order:flags.0?int reaction:Reaction count:int = ReactionCount;\n\nmessageReactions#a339f0b flags:# min:flags.0?true can_see_list:flags.2?true reactions_as_tags:flags.3?true results:Vector<ReactionCount> recent_reactions:flags.1?Vector<MessagePeerReaction> top_reactors:flags.4?Vector<MessageReactor> = MessageReactions;\n\nmessages.messageReactionsList#31bd492d flags:# count:int reactions:Vector<MessagePeerReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = messages.MessageReactionsList;\n\navailableReaction#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;\n\nmessages.availableReactionsNotModified#9f071957 = messages.AvailableReactions;\nmessages.availableReactions#768e3aad hash:int reactions:Vector<AvailableReaction> = messages.AvailableReactions;\n\nmessagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true my:flags.2?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction;\n\ngroupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel;\n\nphone.groupCallStreamChannels#d0e482b2 channels:Vector<GroupCallStreamChannel> = phone.GroupCallStreamChannels;\n\nphone.groupCallStreamRtmpUrl#2dbf3432 url:string key:string = phone.GroupCallStreamRtmpUrl;\n\nattachMenuBotIconColor#4576f3f0 name:string color:int = AttachMenuBotIconColor;\n\nattachMenuBotIcon#b2a7386b flags:# name:string icon:Document colors:flags.0?Vector<AttachMenuBotIconColor> = AttachMenuBotIcon;\n\nattachMenuBot#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<AttachMenuPeerType> icons:Vector<AttachMenuBotIcon> = AttachMenuBot;\n\nattachMenuBotsNotModified#f1d88a5c = AttachMenuBots;\nattachMenuBots#3c4301c0 hash:long bots:Vector<AttachMenuBot> users:Vector<User> = AttachMenuBots;\n\nattachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector<User> = AttachMenuBotsBot;\n\nwebViewResultUrl#4d22ff98 flags:# fullsize:flags.1?true fullscreen:flags.2?true query_id:flags.0?long url:string = WebViewResult;\n\nwebViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent;\n\nbotMenuButtonDefault#7533a588 = BotMenuButton;\nbotMenuButtonCommands#4258c205 = BotMenuButton;\nbotMenuButton#c7b57ce6 text:string url:string = BotMenuButton;\n\naccount.savedRingtonesNotModified#fbf6e8b1 = account.SavedRingtones;\naccount.savedRingtones#c1e92cc5 hash:long ringtones:Vector<Document> = account.SavedRingtones;\n\nnotificationSoundDefault#97e8bebe = NotificationSound;\nnotificationSoundNone#6f0c34df = NotificationSound;\nnotificationSoundLocal#830b9ae4 title:string data:string = NotificationSound;\nnotificationSoundRingtone#ff6c8049 id:long = NotificationSound;\n\naccount.savedRingtone#b7263f6d = account.SavedRingtone;\naccount.savedRingtoneConverted#1f307eb7 document:Document = account.SavedRingtone;\n\nattachMenuPeerTypeSameBotPM#7d6be90e = AttachMenuPeerType;\nattachMenuPeerTypeBotPM#c32bfa1a = AttachMenuPeerType;\nattachMenuPeerTypePM#f146d31f = AttachMenuPeerType;\nattachMenuPeerTypeChat#509113f = AttachMenuPeerType;\nattachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType;\n\ninputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice;\ninputInvoiceSlug#c326caef slug:string = InputInvoice;\ninputInvoicePremiumGiftCode#98986c0d purpose:InputStorePaymentPurpose option:PremiumGiftCodeOption = InputInvoice;\ninputInvoiceStars#65f00ce3 purpose:InputStorePaymentPurpose = InputInvoice;\ninputInvoiceChatInviteSubscription#34e793f1 hash:string = InputInvoice;\ninputInvoiceStarGift#e8625e92 flags:# hide_name:flags.0?true include_upgrade:flags.2?true peer:InputPeer gift_id:long message:flags.1?TextWithEntities = InputInvoice;\ninputInvoiceStarGiftUpgrade#4d818d5d flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = InputInvoice;\ninputInvoiceStarGiftTransfer#4a5f5bd9 stargift:InputSavedStarGift to_id:InputPeer = InputInvoice;\ninputInvoicePremiumGiftStars#dabab2ef flags:# user_id:InputUser months:int message:flags.0?TextWithEntities = InputInvoice;\ninputInvoiceBusinessBotTransferStars#f4997e42 bot:InputUser stars:long = InputInvoice;\ninputInvoiceStarGiftResale#c39f5324 flags:# ton:flags.0?true slug:string to_id:InputPeer = InputInvoice;\ninputInvoiceStarGiftPrepaidUpgrade#9a0b48b8 peer:InputPeer hash:string = InputInvoice;\ninputInvoicePremiumAuthCode#3e77f614 purpose:InputStorePaymentPurpose = InputInvoice;\ninputInvoiceStarGiftDropOriginalDetails#923d8d1 stargift:InputSavedStarGift = InputInvoice;\ninputInvoiceStarGiftAuctionBid#1ecafa10 flags:# hide_name:flags.0?true update_bid:flags.2?true peer:flags.3?InputPeer gift_id:long bid_amount:long message:flags.1?TextWithEntities = InputInvoice;\n\npayments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice;\n\nmessages.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;\n\nhelp.premiumPromo#5334759c status_text:string status_entities:Vector<MessageEntity> video_sections:Vector<string> videos:Vector<Document> period_options:Vector<PremiumSubscriptionOption> users:Vector<User> = help.PremiumPromo;\n\ninputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgrade:flags.1?true = InputStorePaymentPurpose;\ninputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentPremiumGiftCode#fb790393 flags:# users:Vector<InputUser> boost_peer:flags.0?InputPeer currency:string amount:long message:flags.1?TextWithEntities = InputStorePaymentPurpose;\ninputStorePaymentPremiumGiveaway#160544ca flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true boost_peer:InputPeer additional_peers:flags.1?Vector<InputPeer> countries_iso2:flags.2?Vector<string> prize_description:flags.4?string random_id:long until_date:int currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentStarsTopup#f9a2a6cb flags:# stars:long currency:string amount:long spend_purpose_peer:flags.0?InputPeer = InputStorePaymentPurpose;\ninputStorePaymentStarsGift#1d741ef7 user_id:InputUser stars:long currency:string amount:long = InputStorePaymentPurpose;\ninputStorePaymentStarsGiveaway#751f08fa flags:# only_new_subscribers:flags.0?true winners_are_visible:flags.3?true stars:long boost_peer:InputPeer additional_peers:flags.1?Vector<InputPeer> countries_iso2:flags.2?Vector<string> prize_description:flags.4?string random_id:long until_date:int currency:string amount:long users:int = InputStorePaymentPurpose;\ninputStorePaymentAuthCode#9bb2636d flags:# restore:flags.0?true phone_number:string phone_code_hash:string currency:string amount:long = InputStorePaymentPurpose;\n\npaymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod;\n\nemojiStatusEmpty#2de11aae = EmojiStatus;\nemojiStatus#e7ff068a flags:# document_id:long until:flags.0?int = EmojiStatus;\nemojiStatusCollectible#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;\ninputEmojiStatusCollectible#7141dbf flags:# collectible_id:long until:flags.0?int = EmojiStatus;\n\naccount.emojiStatusesNotModified#d08ce645 = account.EmojiStatuses;\naccount.emojiStatuses#90c467d1 hash:long statuses:Vector<EmojiStatus> = account.EmojiStatuses;\n\nreactionEmpty#79f5d419 = Reaction;\nreactionEmoji#1b2286b8 emoticon:string = Reaction;\nreactionCustomEmoji#8935fc73 document_id:long = Reaction;\nreactionPaid#523da4eb = Reaction;\n\nchatReactionsNone#eafc32bc = ChatReactions;\nchatReactionsAll#52928bca flags:# allow_custom:flags.0?true = ChatReactions;\nchatReactionsSome#661d4037 reactions:Vector<Reaction> = ChatReactions;\n\nmessages.reactionsNotModified#b06fdbdf = messages.Reactions;\nmessages.reactions#eafdf716 hash:long reactions:Vector<Reaction> = messages.Reactions;\n\nemailVerifyPurposeLoginSetup#4345be73 phone_number:string phone_code_hash:string = EmailVerifyPurpose;\nemailVerifyPurposeLoginChange#527d22eb = EmailVerifyPurpose;\nemailVerifyPurposePassport#bbf51685 = EmailVerifyPurpose;\n\nemailVerificationCode#922e55a9 code:string = EmailVerification;\nemailVerificationGoogle#db909ec2 token:string = EmailVerification;\nemailVerificationApple#96d074fd token:string = EmailVerification;\n\naccount.emailVerified#2b96cd1b email:string = account.EmailVerified;\naccount.emailVerifiedLogin#e1bb0d61 email:string sent_code:auth.SentCode = account.EmailVerified;\n\npremiumSubscriptionOption#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;\n\nsendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer;\n\nmessageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia;\nmessageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia;\n\nstickerKeyword#fcfeb29c document_id:long keyword:Vector<string> = StickerKeyword;\n\nusername#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username;\n\nforumTopicDeleted#23f109b id:int = ForumTopic;\nforumTopic#cdff0eca flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true title_missing:flags.7?true id:int date:int peer:Peer 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;\n\nmessages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector<ForumTopic> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> pts:int = messages.ForumTopics;\n\ndefaultHistoryTTL#43b46b20 period:int = DefaultHistoryTTL;\n\nexportedContactToken#41bf109b url:string expires:int = ExportedContactToken;\n\nrequestPeerTypeUser#5f3b8a00 flags:# bot:flags.0?Bool premium:flags.1?Bool = RequestPeerType;\nrequestPeerTypeChat#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;\nrequestPeerTypeBroadcast#339bef6c flags:# creator:flags.0?true has_username:flags.3?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType;\n\nemojiListNotModified#481eadfa = EmojiList;\nemojiList#7a1e11d1 hash:long document_id:Vector<long> = EmojiList;\n\nemojiGroup#7a9abda9 title:string icon_emoji_id:long emoticons:Vector<string> = EmojiGroup;\nemojiGroupGreeting#80d26cc7 title:string icon_emoji_id:long emoticons:Vector<string> = EmojiGroup;\nemojiGroupPremium#93bcf34 title:string icon_emoji_id:long = EmojiGroup;\n\nmessages.emojiGroupsNotModified#6fb4ad87 = messages.EmojiGroups;\nmessages.emojiGroups#881fb94b hash:int groups:Vector<EmojiGroup> = messages.EmojiGroups;\n\ntextWithEntities#751f3146 text:string entities:Vector<MessageEntity> = TextWithEntities;\n\nmessages.translateResult#33db32f8 result:Vector<TextWithEntities> = messages.TranslatedText;\n\nautoSaveSettings#c84834ce flags:# photos:flags.0?true videos:flags.1?true video_max_size:flags.2?long = AutoSaveSettings;\n\nautoSaveException#81602d47 peer:Peer settings:AutoSaveSettings = AutoSaveException;\n\naccount.autoSaveSettings#4c3e069d users_settings:AutoSaveSettings chats_settings:AutoSaveSettings broadcasts_settings:AutoSaveSettings exceptions:Vector<AutoSaveException> chats:Vector<Chat> users:Vector<User> = account.AutoSaveSettings;\n\nhelp.appConfigNotModified#7cde641d = help.AppConfig;\nhelp.appConfig#dd18782e hash:int config:JSONValue = help.AppConfig;\n\ninputBotAppID#a920bd7a id:long access_hash:long = InputBotApp;\ninputBotAppShortName#908c0407 bot_id:InputUser short_name:string = InputBotApp;\n\nbotAppNotModified#5da674b7 = BotApp;\nbotApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document hash:long = BotApp;\n\nmessages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true has_settings:flags.2?true app:BotApp = messages.BotApp;\n\ninlineBotWebView#b57295d5 text:string url:string = InlineBotWebView;\n\nreadParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate;\n\ninputChatlistDialogFilter#f3e0da33 filter_id:int = InputChatlist;\n\nexportedChatlistInvite#c5181ac flags:# title:string url:string peers:Vector<Peer> = ExportedChatlistInvite;\n\nchatlists.exportedChatlistInvite#10e6e3a6 filter:DialogFilter invite:ExportedChatlistInvite = chatlists.ExportedChatlistInvite;\n\nchatlists.exportedInvites#10ab6dc7 invites:Vector<ExportedChatlistInvite> chats:Vector<Chat> users:Vector<User> = chatlists.ExportedInvites;\n\nchatlists.chatlistInviteAlready#fa87f659 filter_id:int missing_peers:Vector<Peer> already_peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistInvite;\nchatlists.chatlistInvite#f10ece2f flags:# title_noanimate:flags.1?true title:TextWithEntities emoticon:flags.0?string peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistInvite;\n\nchatlists.chatlistUpdates#93bd878d missing_peers:Vector<Peer> chats:Vector<Chat> users:Vector<User> = chatlists.ChatlistUpdates;\n\nbots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo;\n\nmessagePeerVote#b6cc2d5c peer:Peer option:bytes date:int = MessagePeerVote;\nmessagePeerVoteInputOption#74cda504 peer:Peer date:int = MessagePeerVote;\nmessagePeerVoteMultiple#4628f6e6 peer:Peer options:Vector<bytes> date:int = MessagePeerVote;\n\nstoryViews#8d595cd6 flags:# has_viewers:flags.1?true views_count:int forwards_count:flags.2?int reactions:flags.3?Vector<ReactionCount> reactions_count:flags.4?int recent_viewers:flags.0?Vector<long> = StoryViews;\n\nstoryItemDeleted#51e6ee4f id:int = StoryItem;\nstoryItemSkipped#ffadc913 flags:# close_friends:flags.8?true live:flags.9?true id:int date:int expire_date:int = StoryItem;\nstoryItem#edf164f1 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<MessageEntity> media:MessageMedia media_areas:flags.14?Vector<MediaArea> privacy:flags.2?Vector<PrivacyRule> views:flags.3?StoryViews sent_reaction:flags.15?Reaction albums:flags.19?Vector<int> = StoryItem;\n\nstories.allStoriesNotModified#1158fe3e flags:# state:string stealth_mode:StoriesStealthMode = stories.AllStories;\nstories.allStories#6efc5e81 flags:# has_more:flags.0?true count:int state:string peer_stories:Vector<PeerStories> chats:Vector<Chat> users:Vector<User> stealth_mode:StoriesStealthMode = stories.AllStories;\n\nstories.stories#63c3dd0a flags:# count:int stories:Vector<StoryItem> pinned_to_top:flags.0?Vector<int> chats:Vector<Chat> users:Vector<User> = stories.Stories;\n\nstoryView#b0bdeac5 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true user_id:long date:int reaction:flags.2?Reaction = StoryView;\nstoryViewPublicForward#9083670b flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true message:Message = StoryView;\nstoryViewPublicRepost#bd74cf49 flags:# blocked:flags.0?true blocked_my_stories_from:flags.1?true peer_id:Peer story:StoryItem = StoryView;\n\nstories.storyViewsList#59d78fc5 flags:# count:int views_count:int forwards_count:int reactions_count:int views:Vector<StoryView> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryViewsList;\n\nstories.storyViews#de9eed1d views:Vector<StoryViews> users:Vector<User> = stories.StoryViews;\n\ninputReplyToMessage#869fbe10 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<MessageEntity> quote_offset:flags.4?int monoforum_peer_id:flags.5?InputPeer todo_item_id:flags.6?int = InputReplyTo;\ninputReplyToStory#5881323a peer:InputPeer story_id:int = InputReplyTo;\ninputReplyToMonoForum#69d66c45 monoforum_peer_id:InputPeer = InputReplyTo;\n\nexportedStoryLink#3fc9053b link:string = ExportedStoryLink;\n\nstoriesStealthMode#712e27fd flags:# active_until_date:flags.0?int cooldown_until_date:flags.1?int = StoriesStealthMode;\n\nmediaAreaCoordinates#cfc9e002 flags:# x:double y:double w:double h:double rotation:double radius:flags.0?double = MediaAreaCoordinates;\n\nmediaAreaVenue#be82db9c coordinates:MediaAreaCoordinates geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MediaArea;\ninputMediaAreaVenue#b282217f coordinates:MediaAreaCoordinates query_id:long result_id:string = MediaArea;\nmediaAreaGeoPoint#cad5452d flags:# coordinates:MediaAreaCoordinates geo:GeoPoint address:flags.0?GeoPointAddress = MediaArea;\nmediaAreaSuggestedReaction#14455871 flags:# dark:flags.0?true flipped:flags.1?true coordinates:MediaAreaCoordinates reaction:Reaction = MediaArea;\nmediaAreaChannelPost#770416af coordinates:MediaAreaCoordinates channel_id:long msg_id:int = MediaArea;\ninputMediaAreaChannelPost#2271f2bf coordinates:MediaAreaCoordinates channel:InputChannel msg_id:int = MediaArea;\nmediaAreaUrl#37381085 coordinates:MediaAreaCoordinates url:string = MediaArea;\nmediaAreaWeather#49a6549c coordinates:MediaAreaCoordinates emoji:string temperature_c:double color:int = MediaArea;\nmediaAreaStarGift#5787686d coordinates:MediaAreaCoordinates slug:string = MediaArea;\n\npeerStories#9a35e999 flags:# peer:Peer max_read_id:flags.0?int stories:Vector<StoryItem> = PeerStories;\n\nstories.peerStories#cae68768 stories:PeerStories chats:Vector<Chat> users:Vector<User> = stories.PeerStories;\n\nmessages.webPage#fd5e12bd webpage:WebPage chats:Vector<Chat> users:Vector<User> = messages.WebPage;\n\npremiumGiftCodeOption#257e962b flags:# users:int months:int store_product:flags.0?string store_quantity:flags.1?int currency:string amount:long = PremiumGiftCodeOption;\n\npayments.checkedGiftCode#eb983f8f flags:# via_giveaway:flags.2?true from_id:flags.4?Peer giveaway_msg_id:flags.3?int to_id:flags.0?long date:int days:int used_date:flags.1?int chats:Vector<Chat> users:Vector<User> = payments.CheckedGiftCode;\n\npayments.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;\npayments.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;\n\nprepaidGiveaway#b2539d54 id:long months:int quantity:int date:int = PrepaidGiveaway;\nprepaidStarsGiveaway#9a9d77e0 id:long stars:long quantity:int boosts:int date:int = PrepaidGiveaway;\n\nboost#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;\n\npremium.boostsList#86f8613c flags:# count:int boosts:Vector<Boost> next_offset:flags.0?string users:Vector<User> = premium.BoostsList;\n\nmyBoost#c448415c flags:# slot:int peer:flags.0?Peer date:int expires:int cooldown_until_date:flags.1?int = MyBoost;\n\npremium.myBoosts#9ae228e2 my_boosts:Vector<MyBoost> chats:Vector<Chat> users:Vector<User> = premium.MyBoosts;\n\npremium.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<PrepaidGiveaway> my_boost_slots:flags.2?Vector<int> = premium.BoostsStatus;\n\nstoryFwdHeader#b826e150 flags:# modified:flags.3?true from:flags.0?Peer from_name:flags.1?string story_id:flags.2?int = StoryFwdHeader;\n\npostInteractionCountersMessage#e7058e7f msg_id:int views:int forwards:int reactions:int = PostInteractionCounters;\npostInteractionCountersStory#8a480e27 story_id:int views:int forwards:int reactions:int = PostInteractionCounters;\n\nstats.storyStats#50cd067c views_graph:StatsGraph reactions_by_emotion_graph:StatsGraph = stats.StoryStats;\n\npublicForwardMessage#1f2bf4a message:Message = PublicForward;\npublicForwardStory#edf3add0 peer:Peer story:StoryItem = PublicForward;\n\nstats.publicForwards#93037e20 flags:# count:int forwards:Vector<PublicForward> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stats.PublicForwards;\n\npeerColor#b54b5acf flags:# color:flags.0?int background_emoji_id:flags.1?long = PeerColor;\npeerColorCollectible#b9c0639a flags:# collectible_id:long gift_emoji_id:long background_emoji_id:long accent_color:int colors:Vector<int> dark_accent_color:flags.0?int dark_colors:flags.1?Vector<int> = PeerColor;\ninputPeerColorCollectible#b8ea86a9 collectible_id:long = PeerColor;\n\nhelp.peerColorSet#26219a58 colors:Vector<int> = help.PeerColorSet;\nhelp.peerColorProfileSet#767d61eb palette_colors:Vector<int> bg_colors:Vector<int> story_colors:Vector<int> = help.PeerColorSet;\n\nhelp.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;\n\nhelp.peerColorsNotModified#2ba1f5ce = help.PeerColors;\nhelp.peerColors#f8ed08 hash:int colors:Vector<help.PeerColorOption> = help.PeerColors;\n\nstoryReaction#6090d6d5 peer_id:Peer date:int reaction:Reaction = StoryReaction;\nstoryReactionPublicForward#bbab2643 message:Message = StoryReaction;\nstoryReactionPublicRepost#cfcd0f13 peer_id:Peer story:StoryItem = StoryReaction;\n\nstories.storyReactionsList#aa5f789c flags:# count:int reactions:Vector<StoryReaction> chats:Vector<Chat> users:Vector<User> next_offset:flags.0?string = stories.StoryReactionsList;\n\nsavedDialog#bd87cb6c flags:# pinned:flags.2?true peer:Peer top_message:int = SavedDialog;\nmonoForumDialog#64407ea7 flags:# unread_mark:flags.3?true nopaid_messages_exception:flags.4?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_reactions_count:int draft:flags.1?DraftMessage = SavedDialog;\n\nmessages.savedDialogs#f83ae221 dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;\nmessages.savedDialogsSlice#44ba9dd9 count:int dialogs:Vector<SavedDialog> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.SavedDialogs;\nmessages.savedDialogsNotModified#c01f6fe8 count:int = messages.SavedDialogs;\n\nsavedReactionTag#cb6ff828 flags:# reaction:Reaction title:flags.0?string count:int = SavedReactionTag;\n\nmessages.savedReactionTagsNotModified#889b59ef = messages.SavedReactionTags;\nmessages.savedReactionTags#3259950a tags:Vector<SavedReactionTag> hash:long = messages.SavedReactionTags;\n\noutboxReadDate#3bb842ac date:int = OutboxReadDate;\n\nsmsjobs.eligibleToJoin#dc8b44cf terms_url:string monthly_sent_sms:int = smsjobs.EligibilityToJoin;\n\nsmsjobs.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;\n\nsmsJob#e6a1eeb8 job_id:string phone_number:string text:string = SmsJob;\n\nbusinessWeeklyOpen#120b1ab9 start_minute:int end_minute:int = BusinessWeeklyOpen;\n\nbusinessWorkHours#8c92b098 flags:# open_now:flags.0?true timezone_id:string weekly_open:Vector<BusinessWeeklyOpen> = BusinessWorkHours;\n\nbusinessLocation#ac5c1af7 flags:# geo_point:flags.0?GeoPoint address:string = BusinessLocation;\n\ninputBusinessRecipients#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<InputUser> = InputBusinessRecipients;\n\nbusinessRecipients#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<long> = BusinessRecipients;\n\nbusinessAwayMessageScheduleAlways#c9b9e2b9 = BusinessAwayMessageSchedule;\nbusinessAwayMessageScheduleOutsideWorkHours#c3f2f501 = BusinessAwayMessageSchedule;\nbusinessAwayMessageScheduleCustom#cc4d9ecc start_date:int end_date:int = BusinessAwayMessageSchedule;\n\ninputBusinessGreetingMessage#194cb3b shortcut_id:int recipients:InputBusinessRecipients no_activity_days:int = InputBusinessGreetingMessage;\n\nbusinessGreetingMessage#e519abab shortcut_id:int recipients:BusinessRecipients no_activity_days:int = BusinessGreetingMessage;\n\ninputBusinessAwayMessage#832175e0 flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:InputBusinessRecipients = InputBusinessAwayMessage;\n\nbusinessAwayMessage#ef156a5c flags:# offline_only:flags.0?true shortcut_id:int schedule:BusinessAwayMessageSchedule recipients:BusinessRecipients = BusinessAwayMessage;\n\ntimezone#ff9289f5 id:string name:string utc_offset:int = Timezone;\n\nhelp.timezonesListNotModified#970708cc = help.TimezonesList;\nhelp.timezonesList#7b74ed71 timezones:Vector<Timezone> hash:int = help.TimezonesList;\n\nquickReply#697102b shortcut_id:int shortcut:string top_message:int count:int = QuickReply;\n\ninputQuickReplyShortcut#24596d41 shortcut:string = InputQuickReplyShortcut;\ninputQuickReplyShortcutId#1190cf1 shortcut_id:int = InputQuickReplyShortcut;\n\nmessages.quickReplies#c68d6695 quick_replies:Vector<QuickReply> messages:Vector<Message> chats:Vector<Chat> users:Vector<User> = messages.QuickReplies;\nmessages.quickRepliesNotModified#5f91eb5b = messages.QuickReplies;\n\nconnectedBot#cd64636c flags:# bot_id:long recipients:BusinessBotRecipients rights:BusinessBotRights = ConnectedBot;\n\naccount.connectedBots#17d7f87b connected_bots:Vector<ConnectedBot> users:Vector<User> = account.ConnectedBots;\n\nmessages.dialogFilters#2ad93719 flags:# tags_enabled:flags.0?true filters:Vector<DialogFilter> = messages.DialogFilters;\n\nbirthday#6c8e1e06 flags:# day:int month:int year:flags.0?int = Birthday;\n\nbotBusinessConnection#8f34b2f5 flags:# disabled:flags.1?true connection_id:string user_id:long dc_id:int date:int rights:flags.2?BusinessBotRights = BotBusinessConnection;\n\ninputBusinessIntro#9c469cd flags:# title:string description:string sticker:flags.0?InputDocument = InputBusinessIntro;\n\nbusinessIntro#5a0a066d flags:# title:string description:string sticker:flags.0?Document = BusinessIntro;\n\nmessages.myStickers#faff629d count:int sets:Vector<StickerSetCovered> = messages.MyStickers;\n\ninputCollectibleUsername#e39460a9 username:string = InputCollectible;\ninputCollectiblePhone#a2e214a4 phone:string = InputCollectible;\n\nfragment.collectibleInfo#6ebdff91 purchase_date:int currency:string amount:long crypto_currency:string crypto_amount:long url:string = fragment.CollectibleInfo;\n\ninputBusinessBotRecipients#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<InputUser> exclude_users:flags.6?Vector<InputUser> = InputBusinessBotRecipients;\n\nbusinessBotRecipients#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<long> exclude_users:flags.6?Vector<long> = BusinessBotRecipients;\n\ncontactBirthday#1d998733 contact_id:long birthday:Birthday = ContactBirthday;\n\ncontacts.contactBirthdays#114ff30d contacts:Vector<ContactBirthday> users:Vector<User> = contacts.ContactBirthdays;\n\nmissingInvitee#628c9224 flags:# premium_would_allow_invite:flags.0?true premium_required_for_pm:flags.1?true user_id:long = MissingInvitee;\n\nmessages.invitedUsers#7f5defa6 updates:Updates missing_invitees:Vector<MissingInvitee> = messages.InvitedUsers;\n\ninputBusinessChatLink#11679fa7 flags:# message:string entities:flags.0?Vector<MessageEntity> title:flags.1?string = InputBusinessChatLink;\n\nbusinessChatLink#b4ae666f flags:# link:string message:string entities:flags.0?Vector<MessageEntity> title:flags.1?string views:int = BusinessChatLink;\n\naccount.businessChatLinks#ec43a2d1 links:Vector<BusinessChatLink> chats:Vector<Chat> users:Vector<User> = account.BusinessChatLinks;\n\naccount.resolvedBusinessChatLinks#9a23af21 flags:# peer:Peer message:string entities:flags.0?Vector<MessageEntity> chats:Vector<Chat> users:Vector<User> = account.ResolvedBusinessChatLinks;\n\nrequestedPeerUser#d62ff46a flags:# user_id:long first_name:flags.0?string last_name:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer;\nrequestedPeerChat#7307544f flags:# chat_id:long title:flags.0?string photo:flags.2?Photo = RequestedPeer;\nrequestedPeerChannel#8ba403e4 flags:# channel_id:long title:flags.0?string username:flags.1?string photo:flags.2?Photo = RequestedPeer;\n\nsponsoredMessageReportOption#430d3150 text:string option:bytes = SponsoredMessageReportOption;\n\nchannels.sponsoredMessageReportResultChooseOption#846f9e42 title:string options:Vector<SponsoredMessageReportOption> = channels.SponsoredMessageReportResult;\nchannels.sponsoredMessageReportResultAdsHidden#3e3bcf2f = channels.SponsoredMessageReportResult;\nchannels.sponsoredMessageReportResultReported#ad798849 = channels.SponsoredMessageReportResult;\n\nreactionNotificationsFromContacts#bac3a61a = ReactionNotificationsFrom;\nreactionNotificationsFromAll#4b9e22a0 = ReactionNotificationsFrom;\n\nreactionsNotifySettings#56e34970 flags:# messages_notify_from:flags.0?ReactionNotificationsFrom stories_notify_from:flags.1?ReactionNotificationsFrom sound:NotificationSound show_previews:Bool = ReactionsNotifySettings;\n\navailableEffect#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;\n\nmessages.availableEffectsNotModified#d1ed9a5b = messages.AvailableEffects;\nmessages.availableEffects#bddb616e hash:int effects:Vector<AvailableEffect> documents:Vector<Document> = messages.AvailableEffects;\n\nfactCheck#b89bfccf flags:# need_check:flags.0?true country:flags.1?string text:flags.1?TextWithEntities hash:long = FactCheck;\n\nstarsTransactionPeerUnsupported#95f2bfe4 = StarsTransactionPeer;\nstarsTransactionPeerAppStore#b457b375 = StarsTransactionPeer;\nstarsTransactionPeerPlayMarket#7b560a0b = StarsTransactionPeer;\nstarsTransactionPeerPremiumBot#250dbaf8 = StarsTransactionPeer;\nstarsTransactionPeerFragment#e92fd902 = StarsTransactionPeer;\nstarsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer;\nstarsTransactionPeerAds#60682812 = StarsTransactionPeer;\nstarsTransactionPeerAPI#f9677aad = StarsTransactionPeer;\n\nstarsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption;\n\nstarsTransaction#13659eb0 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 business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true stargift_prepaid_upgrade:flags.25?true stargift_drop_original_details:flags.26?true phonegroup_message:flags.27?true stargift_auction_bid:flags.28?true offer:flags.29?true id:string amount: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<MessageMedia> 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 paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction;\n\npayments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;\n\nfoundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory;\n\nstories.foundStories#e2de7737 flags:# count:int stories:Vector<FoundStory> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stories.FoundStories;\n\ngeoPointAddress#de4c5d93 flags:# country_iso2:string state:flags.0?string city:flags.1?string street:flags.2?string = GeoPointAddress;\n\nstarsRevenueStatus#febe5491 flags:# withdrawal_enabled:flags.0?true current_balance:StarsAmount available_balance:StarsAmount overall_revenue:StarsAmount next_withdrawal_at:flags.1?int = StarsRevenueStatus;\n\npayments.starsRevenueStats#6c207376 flags:# top_hours_graph:flags.0?StatsGraph revenue_graph:StatsGraph status:StarsRevenueStatus usd_rate:double = payments.StarsRevenueStats;\n\npayments.starsRevenueWithdrawalUrl#1dab80b7 url:string = payments.StarsRevenueWithdrawalUrl;\n\npayments.starsRevenueAdsAccountUrl#394e7f21 url:string = payments.StarsRevenueAdsAccountUrl;\n\ninputStarsTransaction#206ae6d1 flags:# refund:flags.0?true id:string = InputStarsTransaction;\n\nstarsGiftOption#5e0589f1 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsGiftOption;\n\nbots.popularAppBots#1991b13b flags:# next_offset:flags.0?string users:Vector<User> = bots.PopularAppBots;\n\nbotPreviewMedia#23e91ba3 date:int media:MessageMedia = BotPreviewMedia;\n\nbots.previewInfo#ca71d64 media:Vector<BotPreviewMedia> lang_codes:Vector<string> = bots.PreviewInfo;\n\nstarsSubscriptionPricing#5416d58 period:int amount:long = StarsSubscriptionPricing;\n\nstarsSubscription#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;\n\nmessageReactor#4ba3a95a flags:# top:flags.0?true my:flags.1?true anonymous:flags.2?true peer_id:flags.3?Peer count:int = MessageReactor;\n\nstarsGiveawayOption#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<StarsGiveawayWinnersOption> = StarsGiveawayOption;\n\nstarsGiveawayWinnersOption#54236209 flags:# default:flags.0?true users:int per_user_stars:long = StarsGiveawayWinnersOption;\n\nstarGift#313a9547 flags:# limited:flags.0?true sold_out:flags.1?true birthday:flags.2?true require_premium:flags.7?true limited_per_user:flags.8?true peer_color_available:flags.10?true auction:flags.11?true id:long sticker:Document stars:long availability_remains:flags.0?int availability_total:flags.0?int availability_resale:flags.4?long convert_stars:long first_sale_date:flags.1?int last_sale_date:flags.1?int upgrade_stars:flags.3?long resell_min_stars:flags.4?long title:flags.5?string released_by:flags.6?Peer per_user_total:flags.8?int per_user_remains:flags.8?int locked_until_date:flags.9?int auction_slug:flags.11?string gifts_per_round:flags.11?int auction_start_date:flags.11?int upgrade_variants:flags.12?int background:flags.13?StarGiftBackground = StarGift;\nstarGiftUnique#85f0a9cd flags:# require_premium:flags.6?true resale_ton_only:flags.7?true theme_available:flags.9?true burned:flags.14?true crafted:flags.15?true id:long gift_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<StarGiftAttribute> availability_issued:int availability_total:int gift_address:flags.3?string resell_amount:flags.4?Vector<StarsAmount> released_by:flags.5?Peer value_amount:flags.8?long value_currency:flags.8?string value_usd_amount:flags.8?long theme_peer:flags.10?Peer peer_color:flags.11?PeerColor host_id:flags.12?Peer offer_min_stars:flags.13?int craft_chance_permille:flags.16?int = StarGift;\n\npayments.starGiftsNotModified#a388a368 = payments.StarGifts;\npayments.starGifts#2ed82995 hash:int gifts:Vector<StarGift> chats:Vector<Chat> users:Vector<User> = payments.StarGifts;\n\nmessageReportOption#7903e3d9 text:string option:bytes = MessageReportOption;\n\nreportResultChooseOption#f0e4e0b6 title:string options:Vector<MessageReportOption> = ReportResult;\nreportResultAddComment#6f09ac31 flags:# optional:flags.0?true option:bytes = ReportResult;\nreportResultReported#8db33c4b = ReportResult;\n\nmessages.botPreparedInlineMessage#8ecf0511 id:string expire_date:int = messages.BotPreparedInlineMessage;\n\nmessages.preparedInlineMessage#ff57708d query_id:long result:BotInlineResult peer_types:Vector<InlineQueryPeerType> cache_time:int users:Vector<User> = messages.PreparedInlineMessage;\n\nbotAppSettings#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;\n\nstarRefProgram#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;\n\nconnectedBotStarRef#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;\n\npayments.connectedStarRefBots#98d5ea1d count:int connected_bots:Vector<ConnectedBotStarRef> users:Vector<User> = payments.ConnectedStarRefBots;\n\npayments.suggestedStarRefBots#b4d5d859 flags:# count:int suggested_bots:Vector<StarRefProgram> users:Vector<User> next_offset:flags.0?string = payments.SuggestedStarRefBots;\n\nstarsAmount#bbb6b4a3 amount:long nanos:int = StarsAmount;\nstarsTonAmount#74aee3e0 amount:long = StarsAmount;\n\nmessages.foundStickersNotModified#6010c534 flags:# next_offset:flags.0?int = messages.FoundStickers;\nmessages.foundStickers#82c9e290 flags:# next_offset:flags.0?int hash:long stickers:Vector<Document> = messages.FoundStickers;\n\nbotVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true icon:long company:string custom_description:flags.0?string = BotVerifierSettings;\n\nbotVerification#f93cd45c bot_id:long icon:long description:string = BotVerification;\n\nstarGiftAttributeModel#565251e2 flags:# crafted:flags.0?true name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributePattern#4e7085ea name:string document:Document rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributeBackdrop#9f2504e4 name:string backdrop_id:int center_color:int edge_color:int pattern_color:int text_color:int rarity:StarGiftAttributeRarity = StarGiftAttribute;\nstarGiftAttributeOriginalDetails#e0bff26c flags:# sender_id:flags.0?Peer recipient_id:Peer date:int message:flags.1?TextWithEntities = StarGiftAttribute;\n\npayments.starGiftUpgradePreview#3de1dfed sample_attributes:Vector<StarGiftAttribute> prices:Vector<StarGiftUpgradePrice> next_prices:Vector<StarGiftUpgradePrice> = payments.StarGiftUpgradePreview;\n\nusers.users#62d706b8 users:Vector<User> = users.Users;\nusers.usersSlice#315a4974 count:int users:Vector<User> = users.Users;\n\npayments.uniqueStarGift#416c56e8 gift:StarGift chats:Vector<Chat> users:Vector<User> = payments.UniqueStarGift;\n\nmessages.webPagePreview#8c9a88ac media:MessageMedia chats:Vector<Chat> users:Vector<User> = messages.WebPagePreview;\n\nsavedStarGift#41df43fc flags:# name_hidden:flags.0?true unsaved:flags.5?true refunded:flags.9?true can_upgrade:flags.10?true pinned_to_top:flags.12?true upgrade_separate:flags.17?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 can_transfer_at:flags.13?int can_resell_at:flags.14?int collection_id:flags.15?Vector<int> prepaid_upgrade_hash:flags.16?string drop_original_details_stars:flags.18?long gift_num:flags.19?int can_craft_at:flags.20?int = SavedStarGift;\n\npayments.savedStarGifts#95f389b1 flags:# count:int chat_notifications_enabled:flags.1?Bool gifts:Vector<SavedStarGift> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.SavedStarGifts;\n\ninputSavedStarGiftUser#69279795 msg_id:int = InputSavedStarGift;\ninputSavedStarGiftChat#f101aa7f peer:InputPeer saved_id:long = InputSavedStarGift;\ninputSavedStarGiftSlug#2085c238 slug:string = InputSavedStarGift;\n\npayments.starGiftWithdrawalUrl#84aa3a9c url:string = payments.StarGiftWithdrawalUrl;\n\npaidReactionPrivacyDefault#206ad49e = PaidReactionPrivacy;\npaidReactionPrivacyAnonymous#1f0c1ad9 = PaidReactionPrivacy;\npaidReactionPrivacyPeer#dc6cfcf0 peer:InputPeer = PaidReactionPrivacy;\n\naccount.paidMessagesRevenue#1e109708 stars_amount:long = account.PaidMessagesRevenue;\n\nrequirementToContactEmpty#50a9839 = RequirementToContact;\nrequirementToContactPremium#e581e4e9 = RequirementToContact;\nrequirementToContactPaidMessages#b4f67e93 stars_amount:long = RequirementToContact;\n\nbusinessBotRights#a0624cf7 flags:# reply:flags.0?true read_messages:flags.1?true delete_sent_messages:flags.2?true delete_received_messages:flags.3?true edit_name:flags.4?true edit_bio:flags.5?true edit_profile_photo:flags.6?true edit_username:flags.7?true view_gifts:flags.8?true sell_gifts:flags.9?true change_gift_settings:flags.10?true transfer_and_upgrade_gifts:flags.11?true transfer_stars:flags.12?true manage_stories:flags.13?true = BusinessBotRights;\n\ndisallowedGiftsSettings#71f276c4 flags:# disallow_unlimited_stargifts:flags.0?true disallow_limited_stargifts:flags.1?true disallow_unique_stargifts:flags.2?true disallow_premium_gifts:flags.3?true disallow_stargifts_from_channels:flags.4?true = DisallowedGiftsSettings;\n\nsponsoredPeer#c69708d3 flags:# random_id:bytes peer:Peer sponsor_info:flags.0?string additional_info:flags.1?string = SponsoredPeer;\n\ncontacts.sponsoredPeersEmpty#ea32b4b1 = contacts.SponsoredPeers;\ncontacts.sponsoredPeers#eb032884 peers:Vector<SponsoredPeer> chats:Vector<Chat> users:Vector<User> = contacts.SponsoredPeers;\n\nstarGiftAttributeIdModel#48aaae3c document_id:long = StarGiftAttributeId;\nstarGiftAttributeIdPattern#4a162433 document_id:long = StarGiftAttributeId;\nstarGiftAttributeIdBackdrop#1f01c757 backdrop_id:int = StarGiftAttributeId;\n\nstarGiftAttributeCounter#2eb1b658 attribute:StarGiftAttributeId count:int = StarGiftAttributeCounter;\n\npayments.resaleStarGifts#947a12df flags:# count:int gifts:Vector<StarGift> next_offset:flags.0?string attributes:flags.1?Vector<StarGiftAttribute> attributes_hash:flags.1?long chats:Vector<Chat> counters:flags.2?Vector<StarGiftAttributeCounter> users:Vector<User> = payments.ResaleStarGifts;\n\nstories.canSendStoryCount#c387c04e count_remains:int = stories.CanSendStoryCount;\n\npendingSuggestion#e7e82e12 suggestion:string title:TextWithEntities description:TextWithEntities url:string = PendingSuggestion;\n\ntodoItem#cba9a52f id:int title:TextWithEntities = TodoItem;\n\ntodoList#49b92a26 flags:# others_can_append:flags.0?true others_can_complete:flags.1?true title:TextWithEntities list:Vector<TodoItem> = TodoList;\n\ntodoCompletion#221bb5e4 id:int completed_by:Peer date:int = TodoCompletion;\n\nsuggestedPost#e8e37e5 flags:# accepted:flags.1?true rejected:flags.2?true price:flags.3?StarsAmount schedule_date:flags.0?int = SuggestedPost;\n\nstarsRating#1b0e4f07 flags:# level:int current_level_stars:long stars:long next_level_stars:flags.0?long = StarsRating;\n\nstarGiftCollection#9d6b13b0 flags:# collection_id:int title:string icon:flags.0?Document gifts_count:int hash:long = StarGiftCollection;\n\npayments.starGiftCollectionsNotModified#a0ba4f17 = payments.StarGiftCollections;\npayments.starGiftCollections#8a2932f3 collections:Vector<StarGiftCollection> = payments.StarGiftCollections;\n\nstoryAlbum#9325705a flags:# album_id:int title:string icon_photo:flags.0?Photo icon_video:flags.1?Document = StoryAlbum;\n\nstories.albumsNotModified#564edaeb = stories.Albums;\nstories.albums#c3987a3a hash:long albums:Vector<StoryAlbum> = stories.Albums;\n\nsearchPostsFlood#3e0b5b6a flags:# query_is_free:flags.0?true total_daily:int remains:int wait_till:flags.1?int stars_amount:long = SearchPostsFlood;\n\npayments.uniqueStarGiftValueInfo#512fe446 flags:# last_sale_on_fragment:flags.1?true value_is_average:flags.6?true currency:string value:long initial_sale_date:int initial_sale_stars:long initial_sale_price:long last_sale_date:flags.0?int last_sale_price:flags.0?long floor_price:flags.2?long average_price:flags.3?long listed_count:flags.4?int fragment_listed_count:flags.5?int fragment_listed_url:flags.5?string = payments.UniqueStarGiftValueInfo;\n\nprofileTabPosts#b98cd696 = ProfileTab;\nprofileTabGifts#4d4bd46a = ProfileTab;\nprofileTabMedia#72c64955 = ProfileTab;\nprofileTabFiles#ab339c00 = ProfileTab;\nprofileTabMusic#9f27d26e = ProfileTab;\nprofileTabVoice#e477092e = ProfileTab;\nprofileTabLinks#d3656499 = ProfileTab;\nprofileTabGifs#a2c0f695 = ProfileTab;\n\nusers.savedMusicNotModified#e3878aa4 count:int = users.SavedMusic;\nusers.savedMusic#34a2f297 count:int documents:Vector<Document> = users.SavedMusic;\n\naccount.savedMusicIdsNotModified#4fc81d6e = account.SavedMusicIds;\naccount.savedMusicIds#998d6636 ids:Vector<long> = account.SavedMusicIds;\n\npayments.checkCanSendGiftResultOk#374fa7ad = payments.CheckCanSendGiftResult;\npayments.checkCanSendGiftResultFail#d5e58274 reason:TextWithEntities = payments.CheckCanSendGiftResult;\n\ninputChatThemeEmpty#83268483 = InputChatTheme;\ninputChatTheme#c93de95c emoticon:string = InputChatTheme;\ninputChatThemeUniqueGift#87e5dfe4 slug:string = InputChatTheme;\n\nstarGiftUpgradePrice#99ea331d date:int upgrade_stars:long = StarGiftUpgradePrice;\n\ngroupCallMessage#1a8afc7e flags:# from_admin:flags.1?true id:int from_id:Peer date:int message:TextWithEntities paid_message_stars:flags.0?long = GroupCallMessage;\n\ngroupCallDonor#ee430c85 flags:# top:flags.0?true my:flags.1?true peer_id:flags.3?Peer stars:long = GroupCallDonor;\n\nphone.groupCallStars#9d1dbd26 total_stars:long top_donors:Vector<GroupCallDonor> chats:Vector<Chat> users:Vector<User> = phone.GroupCallStars;\n\nrecentStory#711d692d flags:# live:flags.0?true max_id:flags.1?int = RecentStory;\n\nauctionBidLevel#310240cc pos:int amount:long date:int = AuctionBidLevel;\n\nstarGiftAuctionStateNotModified#fe333952 = StarGiftAuctionState;\nstarGiftAuctionState#771a4e66 version:int start_date:int end_date:int min_bid_amount:long bid_levels:Vector<AuctionBidLevel> top_bidders:Vector<long> next_round_at:int last_gift_num:int gifts_left:int current_round:int total_rounds:int rounds:Vector<StarGiftAuctionRound> = StarGiftAuctionState;\nstarGiftAuctionStateFinished#972dabbf flags:# start_date:int end_date:int average_price:long listed_count:flags.0?int fragment_listed_count:flags.1?int fragment_listed_url:flags.1?string = StarGiftAuctionState;\n\nstarGiftAuctionUserState#2eeed1c4 flags:# returned:flags.1?true bid_amount:flags.0?long bid_date:flags.0?int min_bid_amount:flags.0?long bid_peer:flags.0?Peer acquired_count:int = StarGiftAuctionUserState;\n\npayments.starGiftAuctionState#6b39f4ec gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState timeout:int users:Vector<User> chats:Vector<Chat> = payments.StarGiftAuctionState;\n\nstarGiftAuctionAcquiredGift#42b00348 flags:# name_hidden:flags.0?true peer:Peer date:int bid_amount:long round:int pos:int message:flags.1?TextWithEntities gift_num:flags.2?int = StarGiftAuctionAcquiredGift;\n\npayments.starGiftAuctionAcquiredGifts#7d5bd1f0 gifts:Vector<StarGiftAuctionAcquiredGift> users:Vector<User> chats:Vector<Chat> = payments.StarGiftAuctionAcquiredGifts;\n\nstarGiftActiveAuctionState#d31bc45d gift:StarGift state:StarGiftAuctionState user_state:StarGiftAuctionUserState = StarGiftActiveAuctionState;\n\npayments.starGiftActiveAuctionsNotModified#db33dad0 = payments.StarGiftActiveAuctions;\npayments.starGiftActiveAuctions#aef6abbc auctions:Vector<StarGiftActiveAuctionState> users:Vector<User> chats:Vector<Chat> = payments.StarGiftActiveAuctions;\n\ninputStarGiftAuction#2e16c98 gift_id:long = InputStarGiftAuction;\ninputStarGiftAuctionSlug#7ab58308 slug:string = InputStarGiftAuction;\n\npasskey#98613ebf flags:# id:string name:string date:int software_emoji_id:flags.0?long last_usage_date:flags.1?int = Passkey;\n\naccount.passkeys#f8e0aa1c passkeys:Vector<Passkey> = account.Passkeys;\n\naccount.passkeyRegistrationOptions#e16b5ce1 options:DataJSON = account.PasskeyRegistrationOptions;\n\nauth.passkeyLoginOptions#e2037789 options:DataJSON = auth.PasskeyLoginOptions;\n\ninputPasskeyResponseRegister#3e63935c client_data:DataJSON attestation_data:bytes = InputPasskeyResponse;\ninputPasskeyResponseLogin#c31fc14a client_data:DataJSON authenticator_data:bytes signature:bytes user_handle:string = InputPasskeyResponse;\n\ninputPasskeyCredentialPublicKey#3c27b78f id:string raw_id:string response:InputPasskeyResponse = InputPasskeyCredential;\ninputPasskeyCredentialFirebasePNV#5b1ccb28 pnv_token:string = InputPasskeyCredential;\n\nstarGiftBackground#aff56398 center_color:int edge_color:int text_color:int = StarGiftBackground;\n\nstarGiftAuctionRound#3aae0528 num:int duration:int = StarGiftAuctionRound;\nstarGiftAuctionRoundExtendable#aa021e5 num:int duration:int extend_top:int extend_window:int = StarGiftAuctionRound;\n\npayments.starGiftUpgradeAttributes#46c6e36f attributes:Vector<StarGiftAttribute> = payments.StarGiftUpgradeAttributes;\n\nmessages.emojiGameOutcome#da2ad647 seed:bytes stake_ton_amount:long ton_amount:long = messages.EmojiGameOutcome;\n\nmessages.emojiGameUnavailable#59e65335 = messages.EmojiGameInfo;\nmessages.emojiGameDiceInfo#44e56023 flags:# game_hash:string prev_stake:long current_streak:int params:Vector<int> plays_left:flags.0?int = messages.EmojiGameInfo;\n\nstarGiftAttributeRarity#36437737 permille:int = StarGiftAttributeRarity;\nstarGiftAttributeRarityUncommon#dbce6389 = StarGiftAttributeRarity;\nstarGiftAttributeRarityRare#f08d516b = StarGiftAttributeRarity;\nstarGiftAttributeRarityEpic#78fbf3a8 = StarGiftAttributeRarity;\nstarGiftAttributeRarityLegendary#cef7e7a8 = StarGiftAttributeRarity;\n\nkeyboardButtonStyle#4fdd3430 flags:# bg_primary:flags.0?true bg_danger:flags.1?true bg_success:flags.2?true icon:flags.3?long = KeyboardButtonStyle;\n\n---functions---\n\ninvokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X;\ninvokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector<long> query:!X = X;\ninitConnection#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;\ninvokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;\ninvokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X;\ninvokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X;\ninvokeWithBusinessConnection#dd289f8e {X:Type} connection_id:string query:!X = X;\ninvokeWithGooglePlayIntegrity#1df92984 {X:Type} nonce:string token:string query:!X = X;\ninvokeWithApnsSecret#0dae54f8 {X:Type} nonce:string secret:string query:!X = X;\ninvokeWithReCaptcha#adbb0f94 {X:Type} token:string query:!X = X;\n\nauth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode;\nauth.signUp#aac7b717 flags:# no_joined_notifications:flags.0?true phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization;\nauth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization;\nauth.logOut#3e72ba19 = auth.LoggedOut;\nauth.resetAuthorizations#9fab0d1a = Bool;\nauth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization;\nauth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization;\nauth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool;\nauth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization;\nauth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization;\nauth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery;\nauth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization;\nauth.resendCode#cae47523 flags:# phone_number:string phone_code_hash:string reason:flags.0?string = auth.SentCode;\nauth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool;\nauth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector<long> = Bool;\nauth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector<long> = auth.LoginToken;\nauth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken;\nauth.acceptLoginToken#e894ad4d token:bytes = Authorization;\nauth.checkRecoveryPassword#d36bf79 code:string = Bool;\nauth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization;\nauth.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;\nauth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode;\nauth.reportMissingCode#cb9deff6 phone_number:string phone_code_hash:string mnc:string = Bool;\nauth.checkPaidAuth#56e59f9c phone_number:string phone_code_hash:string form_id:long = auth.SentCode;\nauth.initPasskeyLogin#518ad0b7 api_id:int api_hash:string = auth.PasskeyLoginOptions;\nauth.finishPasskeyLogin#9857ad07 flags:# credential:InputPasskeyCredential from_dc_id:flags.0?int from_auth_key_id:flags.0?long = auth.Authorization;\n\naccount.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector<long> = Bool;\naccount.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector<long> = Bool;\naccount.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool;\naccount.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings;\naccount.resetNotifySettings#db7e1747 = Bool;\naccount.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User;\naccount.updateStatus#6628562c offline:Bool = Bool;\naccount.getWallPapers#7967d36 hash:long = account.WallPapers;\naccount.reportPeer#c5ba3d86 peer:InputPeer reason:ReportReason message:string = Bool;\naccount.checkUsername#2714d86c username:string = Bool;\naccount.updateUsername#3e0bdd7c username:string = User;\naccount.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules;\naccount.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector<InputPrivacyRule> = account.PrivacyRules;\naccount.deleteAccount#a2c0cf74 flags:# reason:string password:flags.0?InputCheckPasswordSRP = Bool;\naccount.getAccountTTL#8fc711d = AccountDaysTTL;\naccount.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool;\naccount.sendChangePhoneCode#82574ae5 phone_number:string settings:CodeSettings = auth.SentCode;\naccount.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User;\naccount.updateDeviceLocked#38df3532 period:int = Bool;\naccount.getAuthorizations#e320c158 = account.Authorizations;\naccount.resetAuthorization#df77f3bc hash:long = Bool;\naccount.getPassword#548a30f5 = account.Password;\naccount.getPasswordSettings#9cd4eaf9 password:InputCheckPasswordSRP = account.PasswordSettings;\naccount.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_settings:account.PasswordInputSettings = Bool;\naccount.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode;\naccount.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool;\naccount.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword;\naccount.getWebAuthorizations#182e6d6f = account.WebAuthorizations;\naccount.resetWebAuthorization#2d01b9ef hash:long = Bool;\naccount.resetWebAuthorizations#682d2594 = Bool;\naccount.getAllSecureValues#b288bc7d = Vector<SecureValue>;\naccount.getSecureValue#73665bc2 types:Vector<SecureValueType> = Vector<SecureValue>;\naccount.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue;\naccount.deleteSecureValue#b880bc4b types:Vector<SecureValueType> = Bool;\naccount.getAuthorizationForm#a929597a bot_id:long scope:string public_key:string = account.AuthorizationForm;\naccount.acceptAuthorization#f3ed4c73 bot_id:long scope:string public_key:string value_hashes:Vector<SecureValueHash> credentials:SecureCredentialsEncrypted = Bool;\naccount.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode;\naccount.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool;\naccount.sendVerifyEmailCode#98e037bb purpose:EmailVerifyPurpose email:string = account.SentEmailCode;\naccount.verifyEmail#32da4cf purpose:EmailVerifyPurpose verification:EmailVerification = account.EmailVerified;\naccount.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;\naccount.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool;\naccount.confirmPasswordEmail#8fdf1920 code:string = Bool;\naccount.resendPasswordEmail#7a7f2a15 = Bool;\naccount.cancelPasswordEmail#c1cbd5b6 = Bool;\naccount.getContactSignUpNotification#9f07c728 = Bool;\naccount.setContactSignUpNotification#cff43f61 silent:Bool = Bool;\naccount.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true compare_stories:flags.2?true peer:flags.0?InputNotifyPeer = Updates;\naccount.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper;\naccount.uploadWallPaper#e39a8f03 flags:# for_chat:flags.0?true file:InputFile mime_type:string settings:WallPaperSettings = WallPaper;\naccount.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool;\naccount.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSettings = Bool;\naccount.resetWallPapers#bb3b9804 = Bool;\naccount.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings;\naccount.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool;\naccount.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document;\naccount.createTheme#652e4400 flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?Vector<InputThemeSettings> = Theme;\naccount.updateTheme#2bf40ccc flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?Vector<InputThemeSettings> = Theme;\naccount.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool;\naccount.installTheme#c727bb3b flags:# dark:flags.0?true theme:flags.1?InputTheme format:flags.2?string base_theme:flags.3?BaseTheme = Bool;\naccount.getTheme#3a5869ec format:string theme:InputTheme = Theme;\naccount.getThemes#7206e458 format:string hash:long = account.Themes;\naccount.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool;\naccount.getContentSettings#8b9b4dae = account.ContentSettings;\naccount.getMultiWallPapers#65ad71dc wallpapers:Vector<InputWallPaper> = Vector<WallPaper>;\naccount.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings;\naccount.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings;\naccount.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool;\naccount.resetPassword#9308ce1b = account.ResetPasswordResult;\naccount.declinePasswordReset#4c9409f6 = Bool;\naccount.getChatThemes#d638de89 hash:long = account.Themes;\naccount.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool;\naccount.changeAuthorizationSettings#40f48462 flags:# confirmed:flags.3?true hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool;\naccount.getSavedRingtones#e1902288 hash:long = account.SavedRingtones;\naccount.saveRingtone#3dea5b03 id:InputDocument unsave:Bool = account.SavedRingtone;\naccount.uploadRingtone#831a83a2 file:InputFile file_name:string mime_type:string = Document;\naccount.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;\naccount.getDefaultEmojiStatuses#d6753386 hash:long = account.EmojiStatuses;\naccount.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;\naccount.clearRecentEmojiStatuses#18201aae = Bool;\naccount.reorderUsernames#ef500eab order:Vector<string> = Bool;\naccount.toggleUsername#58d6b376 username:string active:Bool = Bool;\naccount.getDefaultProfilePhotoEmojis#e2750328 hash:long = EmojiList;\naccount.getDefaultGroupPhotoEmojis#915860ae hash:long = EmojiList;\naccount.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings;\naccount.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool;\naccount.deleteAutoSaveExceptions#53bc0020 = Bool;\naccount.invalidateSignInCodes#ca8ae8ba codes:Vector<string> = Bool;\naccount.updateColor#684d214e flags:# for_profile:flags.1?true color:flags.2?PeerColor = Bool;\naccount.getDefaultBackgroundEmojis#a60ab9ce hash:long = EmojiList;\naccount.getChannelDefaultEmojiStatuses#7727a7d5 hash:long = account.EmojiStatuses;\naccount.getChannelRestrictedStatusEmojis#35a9e0d5 hash:long = EmojiList;\naccount.updateBusinessWorkHours#4b00e066 flags:# business_work_hours:flags.0?BusinessWorkHours = Bool;\naccount.updateBusinessLocation#9e6b131a flags:# geo_point:flags.1?InputGeoPoint address:flags.0?string = Bool;\naccount.updateBusinessGreetingMessage#66cdafc4 flags:# message:flags.0?InputBusinessGreetingMessage = Bool;\naccount.updateBusinessAwayMessage#a26a7fa5 flags:# message:flags.0?InputBusinessAwayMessage = Bool;\naccount.updateConnectedBot#66a08c7e flags:# deleted:flags.1?true rights:flags.0?BusinessBotRights bot:InputUser recipients:InputBusinessBotRecipients = Updates;\naccount.getConnectedBots#4ea4c80f = account.ConnectedBots;\naccount.getBotBusinessConnection#76a86270 connection_id:string = Updates;\naccount.updateBusinessIntro#a614d034 flags:# intro:flags.0?InputBusinessIntro = Bool;\naccount.toggleConnectedBotPaused#646e1097 peer:InputPeer paused:Bool = Bool;\naccount.disablePeerConnectedBot#5e437ed9 peer:InputPeer = Bool;\naccount.updateBirthday#cc6e0c11 flags:# birthday:flags.0?Birthday = Bool;\naccount.createBusinessChatLink#8851e68e link:InputBusinessChatLink = BusinessChatLink;\naccount.editBusinessChatLink#8c3410af slug:string link:InputBusinessChatLink = BusinessChatLink;\naccount.deleteBusinessChatLink#60073674 slug:string = Bool;\naccount.getBusinessChatLinks#6f70dde1 = account.BusinessChatLinks;\naccount.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;\naccount.updatePersonalChannel#d94305e0 channel:InputChannel = Bool;\naccount.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;\naccount.getReactionsNotifySettings#6dd654c = ReactionsNotifySettings;\naccount.setReactionsNotifySettings#316ce548 settings:ReactionsNotifySettings = ReactionsNotifySettings;\naccount.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;\naccount.getPaidMessagesRevenue#19ba4a67 flags:# parent_peer:flags.0?InputPeer user_id:InputUser = account.PaidMessagesRevenue;\naccount.toggleNoPaidMessagesException#fe2eda76 flags:# refund_charged:flags.0?true require_payment:flags.2?true parent_peer:flags.1?InputPeer user_id:InputUser = Bool;\naccount.setMainProfileTab#5dee78b0 tab:ProfileTab = Bool;\naccount.saveMusic#b26732a9 flags:# unsave:flags.0?true id:InputDocument after_id:flags.1?InputDocument = Bool;\naccount.getSavedMusicIds#e09d5faf hash:long = account.SavedMusicIds;\naccount.getUniqueGiftChatThemes#e42ce9c9 offset:string limit:int hash:long = account.ChatThemes;\naccount.initPasskeyRegistration#429547e8 = account.PasskeyRegistrationOptions;\naccount.registerPasskey#55b41fd6 credential:InputPasskeyCredential = Passkey;\naccount.getPasskeys#ea1f0c52 = account.Passkeys;\naccount.deletePasskey#f5b5563f id:string = Bool;\n\nusers.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;\nusers.getFullUser#b60f5918 id:InputUser = users.UserFull;\nusers.setSecureValueErrors#90c894b5 id:InputUser errors:Vector<SecureValueError> = Bool;\nusers.getRequirementsToContact#d89a83a3 id:Vector<InputUser> = Vector<RequirementToContact>;\nusers.getSavedMusic#788d7fe3 id:InputUser offset:int limit:int hash:long = users.SavedMusic;\nusers.getSavedMusicByID#7573a4e9 id:InputUser documents:Vector<InputDocument> = users.SavedMusic;\nusers.suggestBirthday#fc533372 id:InputUser birthday:Birthday = Updates;\n\ncontacts.getContactIDs#7adc669d hash:long = Vector<int>;\ncontacts.getStatuses#c4a353ee = Vector<ContactStatus>;\ncontacts.getContacts#5dd69e12 hash:long = contacts.Contacts;\ncontacts.importContacts#2c800be5 contacts:Vector<InputContact> = contacts.ImportedContacts;\ncontacts.deleteContacts#96a0e00 id:Vector<InputUser> = Updates;\ncontacts.deleteByPhones#1013fd9e phones:Vector<string> = Bool;\ncontacts.block#2e2e8734 flags:# my_stories_from:flags.0?true id:InputPeer = Bool;\ncontacts.unblock#b550d328 flags:# my_stories_from:flags.0?true id:InputPeer = Bool;\ncontacts.getBlocked#9a868f80 flags:# my_stories_from:flags.0?true offset:int limit:int = contacts.Blocked;\ncontacts.search#11f812d8 q:string limit:int = contacts.Found;\ncontacts.resolveUsername#725afbbc flags:# username:string referer:flags.0?string = contacts.ResolvedPeer;\ncontacts.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;\ncontacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool;\ncontacts.resetSaved#879537f1 = Bool;\ncontacts.getSaved#82f1e39f = Vector<SavedContact>;\ncontacts.toggleTopPeers#8514bdda enabled:Bool = Bool;\ncontacts.addContact#d9ba2e54 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string note:flags.1?TextWithEntities = Updates;\ncontacts.acceptContact#f831a20f id:InputUser = Updates;\ncontacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates;\ncontacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates;\ncontacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer;\ncontacts.exportContactToken#f8654027 = ExportedContactToken;\ncontacts.importContactToken#13005788 token:string = User;\ncontacts.editCloseFriends#ba6705f0 id:Vector<long> = Bool;\ncontacts.setBlocked#94c65c76 flags:# my_stories_from:flags.0?true id:Vector<InputPeer> limit:int = Bool;\ncontacts.getBirthdays#daeda864 = contacts.ContactBirthdays;\ncontacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers;\ncontacts.updateContactNote#139f63fb id:InputUser note:TextWithEntities = Bool;\n\nmessages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;\nmessages.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;\nmessages.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;\nmessages.search#29ee847a flags:# peer:InputPeer q:string from_id:flags.0?InputPeer saved_peer_id:flags.2?InputPeer saved_reaction:flags.3?Vector<Reaction> 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;\nmessages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages;\nmessages.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;\nmessages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector<int> = messages.AffectedMessages;\nmessages.receivedMessages#5a954c0 max_id:int = Vector<ReceivedNotifyMessage>;\nmessages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool;\nmessages.sendMessage#545cd15a 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<MessageEntity> schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;\nmessages.sendMedia#330e77f 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<MessageEntity> schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long suggested_post:flags.22?SuggestedPost = Updates;\nmessages.forwardMessages#13704a7c 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<int> random_id:Vector<long> to_peer:InputPeer top_msg_id:flags.9?int reply_to:flags.22?InputReplyTo schedule_date:flags.10?int schedule_repeat_period:flags.24?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long video_timestamp:flags.20?int allow_paid_stars:flags.21?long suggested_post:flags.23?SuggestedPost = Updates;\nmessages.reportSpam#cf1592db peer:InputPeer = Bool;\nmessages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings;\nmessages.report#fc78af9b peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;\nmessages.getChats#49e9528f id:Vector<long> = messages.Chats;\nmessages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull;\nmessages.editChatTitle#73783ffd chat_id:long title:string = Updates;\nmessages.editChatPhoto#35ddd674 chat_id:long photo:InputChatPhoto = Updates;\nmessages.addChatUser#cbc6d107 chat_id:long user_id:InputUser fwd_limit:int = messages.InvitedUsers;\nmessages.deleteChatUser#a2185cab flags:# revoke_history:flags.0?true chat_id:long user_id:InputUser = Updates;\nmessages.createChat#92ceddd4 flags:# users:Vector<InputUser> title:string ttl_period:flags.0?int = messages.InvitedUsers;\nmessages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;\nmessages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat;\nmessages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat;\nmessages.discardEncryption#f393aea0 flags:# delete_history:flags.0?true chat_id:int = Bool;\nmessages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool;\nmessages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool;\nmessages.sendEncrypted#44fa7a15 flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage;\nmessages.sendEncryptedFile#5559481d flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes file:InputEncryptedFile = messages.SentEncryptedMessage;\nmessages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage;\nmessages.receivedQueue#55a5bb66 max_qts:int = Vector<long>;\nmessages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool;\nmessages.readMessageContents#36a73f77 id:Vector<int> = messages.AffectedMessages;\nmessages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers;\nmessages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers;\nmessages.getWebPagePreview#570d6f6f flags:# message:string entities:flags.3?Vector<MessageEntity> = messages.WebPagePreview;\nmessages.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;\nmessages.checkChatInvite#3eadb1bb hash:string = ChatInvite;\nmessages.importChatInvite#6c50051c hash:string = Updates;\nmessages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet;\nmessages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult;\nmessages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool;\nmessages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates;\nmessages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector<int> increment:Bool = messages.MessageViews;\nmessages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool;\nmessages.migrateChat#a2875319 chat_id:long = Updates;\nmessages.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;\nmessages.reorderStickerSets#78337739 flags:# masks:flags.0?true emojis:flags.1?true order:Vector<long> = Bool;\nmessages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document;\nmessages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs;\nmessages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool;\nmessages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults;\nmessages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector<InputBotInlineResult> cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool;\nmessages.sendInlineBotResult#c0cf7646 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 allow_paid_stars:flags.21?long = Updates;\nmessages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData;\nmessages.editMessage#51e842e1 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<MessageEntity> schedule_date:flags.15?int schedule_repeat_period:flags.18?int quick_reply_shortcut_id:flags.17?int = Updates;\nmessages.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<MessageEntity> = Bool;\nmessages.getBotCallbackAnswer#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer;\nmessages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool;\nmessages.getPeerDialogs#e470bcfd peers:Vector<InputDialogPeer> = messages.PeerDialogs;\nmessages.saveDraft#54ae308e flags:# no_webpage:flags.1?true invert_media:flags.6?true reply_to:flags.4?InputReplyTo peer:InputPeer message:string entities:flags.3?Vector<MessageEntity> media:flags.5?InputMedia effect:flags.7?long suggested_post:flags.8?SuggestedPost = Bool;\nmessages.getAllDrafts#6a3f8d65 = Updates;\nmessages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers;\nmessages.readFeaturedStickers#5b118126 id:Vector<long> = Bool;\nmessages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers;\nmessages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool;\nmessages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool;\nmessages.getArchivedStickers#57f17692 flags:# masks:flags.0?true emojis:flags.1?true offset_id:long limit:int = messages.ArchivedStickers;\nmessages.getMaskStickers#640f82b8 hash:long = messages.AllStickers;\nmessages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector<StickerSetCovered>;\nmessages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates;\nmessages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool;\nmessages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores;\nmessages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores;\nmessages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats;\nmessages.getWebPage#8d9692a3 url:string hash:int = messages.WebPage;\nmessages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;\nmessages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector<InputDialogPeer> = Bool;\nmessages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs;\nmessages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector<ShippingOption> = Bool;\nmessages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool;\nmessages.uploadMedia#14967978 flags:# business_connection_id:flags.0?string peer:InputPeer media:InputMedia = MessageMedia;\nmessages.sendScreenshotNotification#a1405817 peer:InputPeer reply_to:InputReplyTo random_id:long = Updates;\nmessages.getFavedStickers#4f1aaa9 hash:long = messages.FavedStickers;\nmessages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool;\nmessages.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;\nmessages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory;\nmessages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages;\nmessages.sendMultiMedia#1bf89d74 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<InputSingleMedia> schedule_date:flags.10?int send_as:flags.13?InputPeer quick_reply_shortcut:flags.17?InputQuickReplyShortcut effect:flags.18?long allow_paid_stars:flags.21?long = Updates;\nmessages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile;\nmessages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;\nmessages.getSplitRanges#1cff7e08 = Vector<MessageRange>;\nmessages.markDialogUnread#8c5006f8 flags:# unread:flags.0?true parent_peer:flags.1?InputPeer peer:InputDialogPeer = Bool;\nmessages.getDialogUnreadMarks#21202222 flags:# parent_peer:flags.0?InputPeer = Vector<DialogPeer>;\nmessages.clearAllDrafts#7e58ee9c = Bool;\nmessages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates;\nmessages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector<bytes> = Updates;\nmessages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates;\nmessages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines;\nmessages.editChatAbout#def60797 peer:InputPeer about:string = Bool;\nmessages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates;\nmessages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference;\nmessages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference;\nmessages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector<string> = Vector<EmojiLanguage>;\nmessages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL;\nmessages.getSearchCounters#1bbcf300 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer top_msg_id:flags.0?int filters:Vector<MessagesFilter> = Vector<messages.SearchCounter>;\nmessages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult;\nmessages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true share_phone_number:flags.3?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult;\nmessages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool;\nmessages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages;\nmessages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector<int> = messages.Messages;\nmessages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector<int> = Updates;\nmessages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector<int> = Updates;\nmessages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList;\nmessages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector<InputStickerSet> = Bool;\nmessages.getDialogFilters#efd48c89 = messages.DialogFilters;\nmessages.getSuggestedDialogFilters#a29cd42c = Vector<DialogFilterSuggested>;\nmessages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool;\nmessages.updateDialogFiltersOrder#c563c1e4 order:Vector<int> = Bool;\nmessages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers;\nmessages.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;\nmessages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage;\nmessages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool;\nmessages.unpinAllMessages#62dd747 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory;\nmessages.deleteChat#5bd0ee50 chat_id:long = Bool;\nmessages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages;\nmessages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed;\nmessages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport;\nmessages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:string media:InputMedia = MessageMedia;\nmessages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool;\nmessages.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;\nmessages.getExportedChatInvite#73746f5c peer:InputPeer link:string = messages.ExportedChatInvite;\nmessages.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;\nmessages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool;\nmessages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool;\nmessages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites;\nmessages.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;\nmessages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates;\nmessages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer;\nmessages.setChatTheme#81202c9 peer:InputPeer theme:InputChatTheme = Updates;\nmessages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector<ReadParticipantDate>;\nmessages.getSearchResultsCalendar#6aa3f6bd flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar;\nmessages.getSearchResultsPositions#9c7f2f10 flags:# peer:InputPeer saved_peer_id:flags.2?InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions;\nmessages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates;\nmessages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates;\nmessages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;\nmessages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;\nmessages.sendReaction#d30d78d4 flags:# big:flags.1?true add_to_recent:flags.2?true peer:InputPeer msg_id:int reaction:flags.0?Vector<Reaction> = Updates;\nmessages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector<int> = Updates;\nmessages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = messages.MessageReactionsList;\nmessages.setChatAvailableReactions#864b2581 flags:# peer:InputPeer available_reactions:ChatReactions reactions_limit:flags.0?int paid_enabled:flags.1?Bool = Updates;\nmessages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions;\nmessages.setDefaultReaction#4f47a016 reaction:Reaction = Bool;\nmessages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector<int> text:flags.1?Vector<TextWithEntities> to_lang:string = messages.TranslatedText;\nmessages.getUnreadReactions#bd7f90ac flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages;\nmessages.readReactions#9ec44f93 flags:# peer:InputPeer top_msg_id:flags.0?int saved_peer_id:flags.1?InputPeer = messages.AffectedHistory;\nmessages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages;\nmessages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots;\nmessages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot;\nmessages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool;\nmessages.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;\nmessages.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;\nmessages.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;\nmessages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent;\nmessages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates;\nmessages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio;\nmessages.rateTranscribedAudio#7f1d072f peer:InputPeer msg_id:int transcription_id:long good:Bool = Bool;\nmessages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;\nmessages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;\nmessages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;\nmessages.reportReaction#3f64c076 peer:InputPeer id:int reaction_peer:InputPeer = Bool;\nmessages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;\nmessages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;\nmessages.clearRecentReactions#9dfeefb4 = Bool;\nmessages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;\nmessages.setDefaultHistoryTTL#9eb51445 period:int = Bool;\nmessages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL;\nmessages.sendBotRequestedPeer#91b2d060 peer:InputPeer msg_id:int button_id:int requested_peers:Vector<InputPeer> = Updates;\nmessages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups;\nmessages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups;\nmessages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups;\nmessages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList;\nmessages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool;\nmessages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp;\nmessages.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;\nmessages.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;\nmessages.searchEmojiStickerSets#92b4494c flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets;\nmessages.getSavedDialogs#1e91fc99 flags:# exclude_pinned:flags.0?true parent_peer:flags.1?InputPeer offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.SavedDialogs;\nmessages.getSavedHistory#998ab009 flags:# parent_peer:flags.0?InputPeer peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;\nmessages.deleteSavedHistory#4dc5085f flags:# parent_peer:flags.0?InputPeer peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory;\nmessages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs;\nmessages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool;\nmessages.reorderPinnedSavedDialogs#8b716587 flags:# force:flags.0?true order:Vector<InputDialogPeer> = Bool;\nmessages.getSavedReactionTags#3637e05b flags:# peer:flags.0?InputPeer hash:long = messages.SavedReactionTags;\nmessages.updateSavedReactionTag#60297dec flags:# reaction:Reaction title:flags.0?string = Bool;\nmessages.getDefaultTagReactions#bdf93428 hash:long = messages.Reactions;\nmessages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate;\nmessages.getQuickReplies#d483f2a8 hash:long = messages.QuickReplies;\nmessages.reorderQuickReplies#60331907 order:Vector<int> = Bool;\nmessages.checkQuickReplyShortcut#f1d0fbd3 shortcut:string = Bool;\nmessages.editQuickReplyShortcut#5c003cef shortcut_id:int shortcut:string = Bool;\nmessages.deleteQuickReplyShortcut#3cc04740 shortcut_id:int = Bool;\nmessages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vector<int> hash:long = messages.Messages;\nmessages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector<int> random_id:Vector<long> = Updates;\nmessages.deleteQuickReplyMessages#e105e910 shortcut_id:int id:Vector<int> = Updates;\nmessages.toggleDialogFilterTags#fd2dda49 enabled:Bool = Bool;\nmessages.getMyStickers#d0b5e1fc offset_id:long limit:int = messages.MyStickers;\nmessages.getEmojiStickerGroups#1dd840f5 hash:int = messages.EmojiGroups;\nmessages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;\nmessages.editFactCheck#589ee75 peer:InputPeer msg_id:int text:TextWithEntities = Updates;\nmessages.deleteFactCheck#d1da940c peer:InputPeer msg_id:int = Updates;\nmessages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;\nmessages.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;\nmessages.sendPaidReaction#58bbcb50 flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?PaidReactionPrivacy = Updates;\nmessages.togglePaidReactionPrivacy#435885b5 peer:InputPeer msg_id:int private:PaidReactionPrivacy = Bool;\nmessages.getPaidReactionPrivacy#472455aa = Updates;\nmessages.viewSponsoredMessage#269e3643 random_id:bytes = Bool;\nmessages.clickSponsoredMessage#8235057e flags:# media:flags.0?true fullscreen:flags.1?true random_id:bytes = Bool;\nmessages.reportSponsoredMessage#12cbf0c4 random_id:bytes option:bytes = channels.SponsoredMessageReportResult;\nmessages.getSponsoredMessages#3d6ce850 flags:# peer:InputPeer msg_id:flags.0?int = messages.SponsoredMessages;\nmessages.savePreparedInlineMessage#f21f7f2f flags:# result:InputBotInlineResult user_id:InputUser peer_types:flags.0?Vector<InlineQueryPeerType> = messages.BotPreparedInlineMessage;\nmessages.getPreparedInlineMessage#857ebdb8 bot:InputUser id:string = messages.PreparedInlineMessage;\nmessages.searchStickers#29b1c66a flags:# emojis:flags.0?true q:string emoticon:string lang_code:Vector<string> offset:int limit:int hash:long = messages.FoundStickers;\nmessages.reportMessagesDelivery#5a6d7395 flags:# push:flags.0?true peer:InputPeer id:Vector<int> = Bool;\nmessages.getSavedDialogsByID#6f6f9c96 flags:# parent_peer:flags.1?InputPeer ids:Vector<InputPeer> = messages.SavedDialogs;\nmessages.readSavedHistory#ba4a3b5b parent_peer:InputPeer peer:InputPeer max_id:int = Bool;\nmessages.toggleTodoCompleted#d3e03124 peer:InputPeer msg_id:int completed:Vector<int> incompleted:Vector<int> = Updates;\nmessages.appendTodoList#21a61057 peer:InputPeer msg_id:int list:Vector<TodoItem> = Updates;\nmessages.toggleSuggestedPostApproval#8107455c flags:# reject:flags.1?true peer:InputPeer msg_id:int schedule_date:flags.0?int reject_comment:flags.2?string = Updates;\nmessages.getForumTopics#3ba47bff flags:# peer:InputPeer q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics;\nmessages.getForumTopicsByID#af0a4a08 peer:InputPeer topics:Vector<int> = messages.ForumTopics;\nmessages.editForumTopic#cecc1134 flags:# peer:InputPeer topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates;\nmessages.updatePinnedForumTopic#175df251 peer:InputPeer topic_id:int pinned:Bool = Updates;\nmessages.reorderPinnedForumTopics#e7841f0 flags:# force:flags.0?true peer:InputPeer order:Vector<int> = Updates;\nmessages.createForumTopic#2f98c3d5 flags:# title_missing:flags.4?true peer:InputPeer title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates;\nmessages.deleteTopicHistory#d2816f10 peer:InputPeer top_msg_id:int = messages.AffectedHistory;\nmessages.getEmojiGameInfo#fb7e8ca7 = messages.EmojiGameInfo;\nmessages.summarizeText#9d4104e2 flags:# peer:InputPeer id:int to_lang:flags.0?string = TextWithEntities;\n\nupdates.getState#edd4882a = updates.State;\nupdates.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;\nupdates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;\n\nphotos.updateProfilePhoto#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo;\nphotos.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;\nphotos.deletePhotos#87cf7f2f id:Vector<InputPhoto> = Vector<long>;\nphotos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos;\nphotos.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;\n\nupload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool;\nupload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File;\nupload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool;\nupload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile;\nupload.getCdnFile#395f69da file_token:bytes offset:long limit:int = upload.CdnFile;\nupload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector<FileHash>;\nupload.getCdnFileHashes#91dc3f31 file_token:bytes offset:long = Vector<FileHash>;\nupload.getFileHashes#9156982a location:InputFileLocation offset:long = Vector<FileHash>;\n\nhelp.getConfig#c4f9186b = Config;\nhelp.getNearestDc#1fb33026 = NearestDc;\nhelp.getAppUpdate#522d5a7d source:string = help.AppUpdate;\nhelp.getInviteText#4d392343 = help.InviteText;\nhelp.getSupport#9cdf08cd = help.Support;\nhelp.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool;\nhelp.getCdnConfig#52029342 = CdnConfig;\nhelp.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls;\nhelp.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate;\nhelp.acceptTermsOfService#ee72f79a id:DataJSON = Bool;\nhelp.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo;\nhelp.getAppConfig#61e3f854 hash:int = help.AppConfig;\nhelp.saveAppLog#6f02f748 events:Vector<InputAppEvent> = Bool;\nhelp.getPassportConfig#c661ad08 hash:int = help.PassportConfig;\nhelp.getSupportName#d360e72c = help.SupportName;\nhelp.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo;\nhelp.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector<MessageEntity> = help.UserInfo;\nhelp.getPromoData#c0977421 = help.PromoData;\nhelp.hidePromoData#1e251c95 peer:InputPeer = Bool;\nhelp.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool;\nhelp.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList;\nhelp.getPremiumPromo#b81b93d4 = help.PremiumPromo;\nhelp.getPeerColors#da80f42f hash:int = help.PeerColors;\nhelp.getPeerProfileColors#abcfa9fd hash:int = help.PeerColors;\nhelp.getTimezonesList#49b30240 hash:int = help.TimezonesList;\n\nchannels.readHistory#cc104937 channel:InputChannel max_id:int = Bool;\nchannels.deleteMessages#84c1fd4e channel:InputChannel id:Vector<int> = messages.AffectedMessages;\nchannels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector<int> = Bool;\nchannels.getMessages#ad8c9a23 channel:InputChannel id:Vector<InputMessage> = messages.Messages;\nchannels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants;\nchannels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant;\nchannels.getChannels#a7f6bbb id:Vector<InputChannel> = messages.Chats;\nchannels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull;\nchannels.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;\nchannels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates;\nchannels.editTitle#566decd0 channel:InputChannel title:string = Updates;\nchannels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates;\nchannels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool;\nchannels.updateUsername#3514b3de channel:InputChannel username:string = Bool;\nchannels.joinChannel#24b524c5 channel:InputChannel = Updates;\nchannels.leaveChannel#f836aa95 channel:InputChannel = Updates;\nchannels.inviteToChannel#c9e33d54 channel:InputChannel users:Vector<InputUser> = messages.InvitedUsers;\nchannels.deleteChannel#c0111fe3 channel:InputChannel = Updates;\nchannels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink;\nchannels.toggleSignatures#418d549c flags:# signatures_enabled:flags.0?true profiles_enabled:flags.1?true channel:InputChannel = Updates;\nchannels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true for_personal:flags.2?true = messages.Chats;\nchannels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates;\nchannels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector<InputUser> max_id:long min_id:long limit:int = channels.AdminLogResults;\nchannels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool;\nchannels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool;\nchannels.deleteHistory#9baa9647 flags:# for_everyone:flags.0?true channel:InputChannel max_id:int = Updates;\nchannels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;\nchannels.getLeftChannels#8341ecc0 offset:int = messages.Chats;\nchannels.getGroupsForDiscussion#f5dad378 = messages.Chats;\nchannels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;\nchannels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates;\nchannels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool;\nchannels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates;\nchannels.getInactiveChannels#11e831ee = messages.InactiveChats;\nchannels.convertToGigagroup#b290c69 channel:InputChannel = Updates;\nchannels.getSendAs#e785a43f flags:# for_paid_reactions:flags.0?true for_live_stories:flags.1?true peer:InputPeer = channels.SendAsPeers;\nchannels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory;\nchannels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates;\nchannels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates;\nchannels.reorderUsernames#b45ced1d channel:InputChannel order:Vector<string> = Bool;\nchannels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool;\nchannels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool;\nchannels.toggleForum#3ff75734 channel:InputChannel enabled:Bool tabs:Bool = Updates;\nchannels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates;\nchannels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool;\nchannels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates;\nchannels.updateColor#d8aa3671 flags:# for_profile:flags.1?true channel:InputChannel color:flags.2?int background_emoji_id:flags.0?long = Updates;\nchannels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;\nchannels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;\nchannels.updateEmojiStatus#f0d3e6a8 channel:InputChannel emoji_status:EmojiStatus = Updates;\nchannels.setBoostsToUnblockRestrictions#ad399cee channel:InputChannel boosts:int = Updates;\nchannels.setEmojiStickers#3cd930b7 channel:InputChannel stickerset:InputStickerSet = Bool;\nchannels.restrictSponsoredMessages#9ae91519 channel:InputChannel restricted:Bool = Updates;\nchannels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?string offset_rate:int offset_peer:InputPeer offset_id:int limit:int allow_paid_stars:flags.2?long = messages.Messages;\nchannels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates;\nchannels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates;\nchannels.getMessageAuthor#ece2a0e6 channel:InputChannel id:int = User;\nchannels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood;\nchannels.setMainProfileTab#3583fcb1 channel:InputChannel tab:ProfileTab = Bool;\nchannels.getFutureCreatorAfterLeave#a00918af channel:InputChannel = User;\n\nbots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON;\nbots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool;\nbots.setBotCommands#517165a scope:BotCommandScope lang_code:string commands:Vector<BotCommand> = Bool;\nbots.resetBotCommands#3d8de0f9 scope:BotCommandScope lang_code:string = Bool;\nbots.getBotCommands#e34c0dd6 scope:BotCommandScope lang_code:string = Vector<BotCommand>;\nbots.setBotMenuButton#4504d54f user_id:InputUser button:BotMenuButton = Bool;\nbots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton;\nbots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool;\nbots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool;\nbots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;\nbots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo;\nbots.reorderUsernames#9709b1c2 bot:InputUser order:Vector<string> = Bool;\nbots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool;\nbots.canSendMessage#1359f4e6 bot:InputUser = Bool;\nbots.allowSendMessage#f132e3ef bot:InputUser = Updates;\nbots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;\nbots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;\nbots.addPreviewMedia#17aeb75a bot:InputUser lang_code:string media:InputMedia = BotPreviewMedia;\nbots.editPreviewMedia#8525606f bot:InputUser lang_code:string media:InputMedia new_media:InputMedia = BotPreviewMedia;\nbots.deletePreviewMedia#2d0135b3 bot:InputUser lang_code:string media:Vector<InputMedia> = Bool;\nbots.reorderPreviewMedias#b627f3aa bot:InputUser lang_code:string order:Vector<InputMedia> = Bool;\nbots.getPreviewInfo#423ab3ad bot:InputUser lang_code:string = bots.PreviewInfo;\nbots.getPreviewMedias#a2a5594d bot:InputUser = Vector<BotPreviewMedia>;\nbots.updateUserEmojiStatus#ed9f30c5 user_id:InputUser emoji_status:EmojiStatus = Bool;\nbots.toggleUserEmojiStatusPermission#6de6392 bot:InputUser enabled:Bool = Bool;\nbots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool;\nbots.getAdminedBots#b0711d83 = Vector<User>;\nbots.updateStarRefProgram#778b5ab3 flags:# bot:InputUser commission_permille:int duration_months:flags.0?int = StarRefProgram;\nbots.setCustomVerification#8b89dfbd flags:# enabled:flags.1?true bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string = Bool;\nbots.getBotRecommendations#a1b70815 bot:InputUser = users.Users;\n\npayments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;\npayments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;\npayments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;\npayments.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;\npayments.getSavedInfo#227d824b = payments.SavedInfo;\npayments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool;\npayments.getBankCardData#2e79d779 number:string = payments.BankCardData;\npayments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice;\npayments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates;\npayments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates;\npayments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector<PremiumGiftCodeOption>;\npayments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode;\npayments.applyGiftCode#f6e26854 slug:string = Updates;\npayments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;\npayments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates;\npayments.getStarsTopupOptions#c00ec7d3 = Vector<StarsTopupOption>;\npayments.getStarsStatus#4ea9b3bf flags:# ton:flags.0?true peer:InputPeer = payments.StarsStatus;\npayments.getStarsTransactions#69da4557 flags:# inbound:flags.0?true outbound:flags.1?true ascending:flags.2?true ton:flags.4?true subscription_id:flags.3?string peer:InputPeer offset:string limit:int = payments.StarsStatus;\npayments.sendStarsForm#7998c914 form_id:long invoice:InputInvoice = payments.PaymentResult;\npayments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;\npayments.getStarsRevenueStats#d91ffad6 flags:# dark:flags.0?true ton:flags.1?true peer:InputPeer = payments.StarsRevenueStats;\npayments.getStarsRevenueWithdrawalUrl#2433dc92 flags:# ton:flags.0?true peer:InputPeer amount:flags.1?long password:InputCheckPasswordSRP = payments.StarsRevenueWithdrawalUrl;\npayments.getStarsRevenueAdsAccountUrl#d1d7efc5 peer:InputPeer = payments.StarsRevenueAdsAccountUrl;\npayments.getStarsTransactionsByID#2dca16b8 flags:# ton:flags.0?true peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;\npayments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;\npayments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;\npayments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool;\npayments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool;\npayments.getStarsGiveawayOptions#bd1efd3e = Vector<StarsGiveawayOption>;\npayments.getStarGifts#c4563590 hash:int = payments.StarGifts;\npayments.saveStarGift#2a2a697c flags:# unsave:flags.0?true stargift:InputSavedStarGift = Bool;\npayments.convertStarGift#74bf076b stargift:InputSavedStarGift = Bool;\npayments.botCancelStarsSubscription#6dfa0622 flags:# restore:flags.0?true user_id:InputUser charge_id:string = Bool;\npayments.getConnectedStarRefBots#5869a553 flags:# peer:InputPeer offset_date:flags.2?int offset_link:flags.2?string limit:int = payments.ConnectedStarRefBots;\npayments.getConnectedStarRefBot#b7d998f0 peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots;\npayments.getSuggestedStarRefBots#d6b48f7 flags:# order_by_revenue:flags.0?true order_by_date:flags.1?true peer:InputPeer offset:string limit:int = payments.SuggestedStarRefBots;\npayments.connectStarRefBot#7ed5348a peer:InputPeer bot:InputUser = payments.ConnectedStarRefBots;\npayments.editConnectedStarRefBot#e4fca4a3 flags:# revoked:flags.0?true peer:InputPeer link:string = payments.ConnectedStarRefBots;\npayments.getStarGiftUpgradePreview#9c9abcb1 gift_id:long = payments.StarGiftUpgradePreview;\npayments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true stargift:InputSavedStarGift = Updates;\npayments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates;\npayments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;\npayments.getSavedStarGifts#a319e569 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_unique:flags.4?true sort_by_value:flags.5?true exclude_upgradable:flags.7?true exclude_unupgradable:flags.8?true peer_color_available:flags.9?true exclude_hosted:flags.10?true peer:InputPeer collection_id:flags.6?int offset:string limit:int = payments.SavedStarGifts;\npayments.getSavedStarGift#b455a106 stargift:Vector<InputSavedStarGift> = payments.SavedStarGifts;\npayments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl;\npayments.toggleChatStarGiftNotifications#60eaefa1 flags:# enabled:flags.0?true peer:InputPeer = Bool;\npayments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool;\npayments.canPurchaseStore#4fdc5ea7 purpose:InputStorePaymentPurpose = Bool;\npayments.getResaleStarGifts#7a5fa236 flags:# sort_by_price:flags.1?true sort_by_num:flags.2?true for_craft:flags.4?true attributes_hash:flags.0?long gift_id:long attributes:flags.3?Vector<StarGiftAttributeId> offset:string limit:int = payments.ResaleStarGifts;\npayments.updateStarGiftPrice#edbe6ccb stargift:InputSavedStarGift resell_amount:StarsAmount = Updates;\npayments.createStarGiftCollection#1f4a0e87 peer:InputPeer title:string stargift:Vector<InputSavedStarGift> = StarGiftCollection;\npayments.updateStarGiftCollection#4fddbee7 flags:# peer:InputPeer collection_id:int title:flags.0?string delete_stargift:flags.1?Vector<InputSavedStarGift> add_stargift:flags.2?Vector<InputSavedStarGift> order:flags.3?Vector<InputSavedStarGift> = StarGiftCollection;\npayments.reorderStarGiftCollections#c32af4cc peer:InputPeer order:Vector<int> = Bool;\npayments.deleteStarGiftCollection#ad5648e8 peer:InputPeer collection_id:int = Bool;\npayments.getStarGiftCollections#981b91dd peer:InputPeer hash:long = payments.StarGiftCollections;\npayments.getUniqueStarGiftValueInfo#4365af6b slug:string = payments.UniqueStarGiftValueInfo;\npayments.checkCanSendGift#c0c4edc9 gift_id:long = payments.CheckCanSendGiftResult;\npayments.getStarGiftAuctionState#5c9ff4d6 auction:InputStarGiftAuction version:int = payments.StarGiftAuctionState;\npayments.getStarGiftAuctionAcquiredGifts#6ba2cbec gift_id:long = payments.StarGiftAuctionAcquiredGifts;\npayments.getStarGiftActiveAuctions#a5d0514d hash:long = payments.StarGiftActiveAuctions;\npayments.resolveStarGiftOffer#e9ce781c flags:# decline:flags.0?true offer_msg_id:int = Updates;\npayments.sendStarGiftOffer#8fb86b41 flags:# peer:InputPeer slug:string price:StarsAmount duration:int random_id:long allow_paid_stars:flags.0?long = Updates;\npayments.getStarGiftUpgradeAttributes#6d038b58 gift_id:long = payments.StarGiftUpgradeAttributes;\npayments.getCraftStarGifts#fd05dd00 gift_id:long offset:string limit:int = payments.SavedStarGifts;\npayments.craftStarGift#b0f9684f stargift:Vector<InputSavedStarGift> = Updates;\n\nstickers.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<InputStickerSetItem> software:flags.3?string = messages.StickerSet;\nstickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet;\nstickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet;\nstickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet;\nstickers.setStickerSetThumb#a76a5392 flags:# stickerset:InputStickerSet thumb:flags.0?InputDocument thumb_document_id:flags.1?long = messages.StickerSet;\nstickers.checkShortName#284b3639 short_name:string = Bool;\nstickers.suggestShortName#4dafc503 title:string = stickers.SuggestedShortName;\nstickers.changeSticker#f5537ebc flags:# sticker:InputDocument emoji:flags.0?string mask_coords:flags.1?MaskCoords keywords:flags.2?string = messages.StickerSet;\nstickers.renameStickerSet#124b1c00 stickerset:InputStickerSet title:string = messages.StickerSet;\nstickers.deleteStickerSet#87704394 stickerset:InputStickerSet = Bool;\nstickers.replaceSticker#4696459a sticker:InputDocument new_sticker:InputStickerSetItem = messages.StickerSet;\n\nphone.getCallConfig#55451fa9 = DataJSON;\nphone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;\nphone.receivedCall#17d54f61 peer:InputPhoneCall = Bool;\nphone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates;\nphone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates;\nphone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;\nphone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;\nphone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates;\nphone.joinGroupCall#8fb53057 flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string public_key:flags.3?int256 block:flags.3?bytes params:DataJSON = Updates;\nphone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;\nphone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector<InputUser> = Updates;\nphone.discardGroupCall#7a777135 call:InputGroupCall = Updates;\nphone.toggleGroupCallSettings#974392f2 flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool messages_enabled:flags.2?Bool send_paid_messages_stars:flags.3?long = Updates;\nphone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall;\nphone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector<InputPeer> sources:Vector<int> offset:string limit:int = phone.GroupParticipants;\nphone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector<int> = Vector<int>;\nphone.toggleGroupCallRecord#f128c708 flags:# start:flags.0?true video:flags.2?true call:InputGroupCall title:flags.1?string video_portrait:flags.2?Bool = Updates;\nphone.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;\nphone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates;\nphone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers;\nphone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite;\nphone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates;\nphone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates;\nphone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool;\nphone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates;\nphone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates;\nphone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels;\nphone.getGroupCallStreamRtmpUrl#5af4c73a flags:# live_story:flags.0?true peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl;\nphone.saveCallLog#41248786 peer:InputPhoneCall file:InputFile = Bool;\nphone.createConferenceCall#7d0444bb flags:# muted:flags.0?true video_stopped:flags.2?true join:flags.3?true random_id:int public_key:flags.3?int256 block:flags.3?bytes params:flags.3?DataJSON = Updates;\nphone.deleteConferenceCallParticipants#8ca60525 flags:# only_left:flags.0?true kick:flags.1?true call:InputGroupCall ids:Vector<long> block:bytes = Updates;\nphone.sendConferenceCallBroadcast#c6701900 call:InputGroupCall block:bytes = Updates;\nphone.inviteConferenceCallParticipant#bcf22685 flags:# video:flags.0?true call:InputGroupCall user_id:InputUser = Updates;\nphone.declineConferenceCallInvite#3c479971 msg_id:int = Updates;\nphone.getGroupCallChainBlocks#ee9f88a6 call:InputGroupCall sub_chain_id:int offset:int limit:int = Updates;\nphone.sendGroupCallMessage#b1d11410 flags:# call:InputGroupCall random_id:long message:TextWithEntities allow_paid_stars:flags.0?long send_as:flags.1?InputPeer = Updates;\nphone.sendGroupCallEncryptedMessage#e5afa56d call:InputGroupCall encrypted_message:bytes = Bool;\nphone.deleteGroupCallMessages#f64f54f7 flags:# report_spam:flags.0?true call:InputGroupCall messages:Vector<int> = Updates;\nphone.deleteGroupCallParticipantMessages#1dbfeca0 flags:# report_spam:flags.0?true call:InputGroupCall participant:InputPeer = Updates;\nphone.getGroupCallStars#6f636302 call:InputGroupCall = phone.GroupCallStars;\nphone.saveDefaultSendAs#4167add1 call:InputGroupCall send_as:InputPeer = Bool;\n\nlangpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;\nlangpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;\nlangpack.getDifference#cd984aa5 lang_pack:string lang_code:string from_version:int = LangPackDifference;\nlangpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;\nlangpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage;\n\nfolders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;\n\nstats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats;\nstats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;\nstats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;\nstats.getMessagePublicForwards#5f150144 channel:InputChannel msg_id:int offset:string limit:int = stats.PublicForwards;\nstats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;\nstats.getStoryStats#374fef40 flags:# dark:flags.0?true peer:InputPeer id:int = stats.StoryStats;\nstats.getStoryPublicForwards#a6437ef6 peer:InputPeer id:int offset:string limit:int = stats.PublicForwards;\n\nchatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;\nchatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;\nchatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;\nchatlists.getExportedInvites#ce03da83 chatlist:InputChatlist = chatlists.ExportedInvites;\nchatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite;\nchatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector<InputPeer> = Updates;\nchatlists.getChatlistUpdates#89419521 chatlist:InputChatlist = chatlists.ChatlistUpdates;\nchatlists.joinChatlistUpdates#e089f8f5 chatlist:InputChatlist peers:Vector<InputPeer> = Updates;\nchatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool;\nchatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector<Peer>;\nchatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;\n\nstories.canSendStory#30eb63f0 peer:InputPeer = stories.CanSendStoryCount;\nstories.sendStory#737fc2ec flags:# pinned:flags.2?true noforwards:flags.4?true fwd_modified:flags.7?true peer:InputPeer media:InputMedia media_areas:flags.5?Vector<MediaArea> caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long period:flags.3?int fwd_from_id:flags.6?InputPeer fwd_from_story:flags.6?int albums:flags.8?Vector<int> = Updates;\nstories.editStory#b583ba46 flags:# peer:InputPeer id:int media:flags.0?InputMedia media_areas:flags.3?Vector<MediaArea> caption:flags.1?string entities:flags.1?Vector<MessageEntity> privacy_rules:flags.2?Vector<InputPrivacyRule> = Updates;\nstories.deleteStories#ae59db5f peer:InputPeer id:Vector<int> = Vector<int>;\nstories.togglePinned#9a75a1ef peer:InputPeer id:Vector<int> pinned:Bool = Vector<int>;\nstories.getAllStories#eeb0d625 flags:# next:flags.1?true hidden:flags.2?true state:flags.0?string = stories.AllStories;\nstories.getPinnedStories#5821a5dc peer:InputPeer offset_id:int limit:int = stories.Stories;\nstories.getStoriesArchive#b4352016 peer:InputPeer offset_id:int limit:int = stories.Stories;\nstories.getStoriesByID#5774ca74 peer:InputPeer id:Vector<int> = stories.Stories;\nstories.toggleAllStoriesHidden#7c2557c4 hidden:Bool = Bool;\nstories.readStories#a556dac8 peer:InputPeer max_id:int = Vector<int>;\nstories.incrementStoryViews#b2028afb peer:InputPeer id:Vector<int> = Bool;\nstories.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;\nstories.getStoriesViews#28e16cc8 peer:InputPeer id:Vector<int> = stories.StoryViews;\nstories.exportStoryLink#7b8def20 peer:InputPeer id:int = ExportedStoryLink;\nstories.report#19d8eb45 peer:InputPeer id:Vector<int> option:bytes message:string = ReportResult;\nstories.activateStealthMode#57bbd166 flags:# past:flags.0?true future:flags.1?true = Updates;\nstories.sendReaction#7fd736b2 flags:# add_to_recent:flags.0?true peer:InputPeer story_id:int reaction:Reaction = Updates;\nstories.getPeerStories#2c4ada50 peer:InputPeer = stories.PeerStories;\nstories.getAllReadPeerStories#9b5ae7f9 = Updates;\nstories.getPeerMaxIDs#78499170 id:Vector<InputPeer> = Vector<RecentStory>;\nstories.getChatsToSend#a56a8b60 = messages.Chats;\nstories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool;\nstories.getStoryReactionsList#b9b2881f flags:# forwards_first:flags.2?true peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = stories.StoryReactionsList;\nstories.togglePinnedToTop#b297e9b peer:InputPeer id:Vector<int> = Bool;\nstories.searchPosts#d1810907 flags:# hashtag:flags.0?string area:flags.1?MediaArea peer:flags.2?InputPeer offset:string limit:int = stories.FoundStories;\nstories.createAlbum#a36396e5 peer:InputPeer title:string stories:Vector<int> = StoryAlbum;\nstories.updateAlbum#5e5259b6 flags:# peer:InputPeer album_id:int title:flags.0?string delete_stories:flags.1?Vector<int> add_stories:flags.2?Vector<int> order:flags.3?Vector<int> = StoryAlbum;\nstories.reorderAlbums#8535fbd9 peer:InputPeer order:Vector<int> = Bool;\nstories.deleteAlbum#8d3456d0 peer:InputPeer album_id:int = Bool;\nstories.getAlbums#25b3eac7 peer:InputPeer hash:long = stories.Albums;\nstories.getAlbumStories#ac806d61 peer:InputPeer album_id:int offset:int limit:int = stories.Stories;\nstories.startLive#d069ccde flags:# pinned:flags.2?true noforwards:flags.4?true rtmp_stream:flags.5?true peer:InputPeer caption:flags.0?string entities:flags.1?Vector<MessageEntity> privacy_rules:Vector<InputPrivacyRule> random_id:long messages_enabled:flags.6?Bool send_paid_messages_stars:flags.7?long = Updates;\n\npremium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList;\npremium.getMyBoosts#be77b4a = premium.MyBoosts;\npremium.applyBoost#6b7da746 flags:# slots:flags.0?Vector<int> peer:InputPeer = premium.MyBoosts;\npremium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;\npremium.getUserBoosts#39854d1f peer:InputPeer user_id:InputUser = premium.BoostsList;\n\nsmsjobs.isEligibleToJoin#edc39d0 = smsjobs.EligibilityToJoin;\nsmsjobs.join#a74ece2d = Bool;\nsmsjobs.leave#9898ad73 = Bool;\nsmsjobs.updateSettings#93fa0bf flags:# allow_international:flags.0?true = Bool;\nsmsjobs.getStatus#10a698e8 = smsjobs.Status;\nsmsjobs.getSmsJob#778d902f job_id:string = SmsJob;\nsmsjobs.finishJob#4f1ebf24 flags:# job_id:string error:flags.0?string = Bool;\n\nfragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;\n\n// LAYER 222\n"
  },
  {
    "path": "telethon_generator/data/errors.csv",
    "content": "name,codes,description\n2FA_CONFIRM_WAIT_X,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds}\nABOUT_TOO_LONG,400,The provided bio is too long\nACCESS_TOKEN_EXPIRED,400,Bot token expired\nACCESS_TOKEN_INVALID,400,The provided token is not valid\nACTIVE_USER_REQUIRED,401,The method is only available to already activated users\nADMINS_TOO_MUCH,400,Too many admins\nADMIN_ID_INVALID,400,The specified admin ID is invalid\nADMIN_RANK_EMOJI_NOT_ALLOWED,400,Emoji are not allowed in admin titles or ranks\nADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly larger than 16 characters)\nALBUM_PHOTOS_TOO_MANY,400,Too many photos were included in the album\nAPI_ID_INVALID,400,The api_id/api_hash combination is invalid\nAPI_ID_PUBLISHED_FLOOD,400,\"This API id was published somewhere, you can't use it now\"\nARTICLE_TITLE_EMPTY,400,The title of the article is empty\nAUDIO_CONTENT_URL_EMPTY,400,The remote URL specified in the content field is empty\nAUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty\nAUTH_BYTES_INVALID,400,The provided authorization is invalid\nAUTH_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\"\nAUTH_KEY_INVALID,401,The key is invalid\nAUTH_KEY_PERM_EMPTY,401,\"The method is unavailable for temporary authorization key, not bound to permanent\"\nAUTH_KEY_UNREGISTERED,401,The key is not registered in the system\nAUTH_RESTART,500,Restart the authorization process\nAUTH_TOKEN_ALREADY_ACCEPTED,400,The authorization token was already used\nAUTH_TOKEN_EXCEPTION,400,An error occurred while importing the auth token\nAUTH_TOKEN_EXPIRED,400,The provided authorization token has expired and the updated QR-code must be re-scanned\nAUTH_TOKEN_INVALID,400,An invalid authorization token was provided\nAUTH_TOKEN_INVALID2,400,An invalid authorization token was provided\nAUTH_TOKEN_INVALIDX,400,The specified auth token is invalid\nAUTOARCHIVE_NOT_AVAILABLE,400,You cannot use this feature yet\nBANK_CARD_NUMBER_INVALID,400,Incorrect credit card number\nBANNED_RIGHTS_INVALID,400,\"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default\"\nBASE_PORT_LOC_INVALID,400,Base port location invalid\nBOTS_TOO_MUCH,400,There are too many bots in this chat/channel\nBOT_CHANNELS_NA,400,Bots can't edit admin privileges\nBOT_COMMAND_DESCRIPTION_INVALID,400,\"The command description was empty, too long or had invalid characters used\"\nBOT_COMMAND_INVALID,400,The specified command is invalid\nBOT_COMMANDS_TOO_MUCH,400,\"The provided commands are too many\"\nBOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather\nBOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat\nBOT_GROUPS_BLOCKED,400,This bot can't be added to groups\nBOT_INLINE_DISABLED,400,This bot can't be used in inline mode\nBOT_INVALID,400,This is not a valid bot\nBOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot\nBOT_MISSING,400,This method can only be run by a bot\nBOT_ONESIDE_NOT_AVAIL,400,Bots can't pin messages in PM just for themselves\nBOT_PAYMENTS_DISABLED,400,This method can only be run by a bot\nBOT_POLLS_DISABLED,400,You cannot create polls under a bot account\nBOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time\nBOT_SCORE_NOT_MODIFIED,400,The score wasn't modified\nBROADCAST_CALLS_DISABLED,400,\nBROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels\nBROADCAST_ID_INVALID,400,The channel is invalid\nBROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public\nBROADCAST_REQUIRED,400,The request can only be used with a broadcast channel\nBUTTON_DATA_INVALID,400,The provided button data is invalid\nBUTTON_TEXT_INVALID,400,The specified button text is invalid\nBUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid\nBUTTON_URL_INVALID,400,Button URL invalid\nBUTTON_USER_PRIVACY_RESTRICTED,400,The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button\nCALL_ALREADY_ACCEPTED,400,The call was already accepted\nCALL_ALREADY_DECLINED,400,The call was already declined\nCALL_OCCUPY_FAILED,500,The call failed because the user is already making another call\nCALL_PEER_INVALID,400,The provided call peer object is invalid\nCALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid\nCDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods\nCDN_UPLOAD_TIMEOUT,500,A server-side timeout occurred while reuploading the file to the CDN DC\nCHANNELS_ADMIN_LOCATED_TOO_MUCH,400,The user has reached the limit of public geogroups\nCHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,\"You're admin of too many public channels, make some channels private to change the username of this channel\"\nCHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups\nCHANNEL_BANNED,400,The channel is banned\nCHANNEL_FORUM_MISSING,400,\nCHANNEL_ID_INVALID,400,The specified supergroup ID is invalid\nCHANNEL_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\"\nCHANNEL_PARICIPANT_MISSING,400,The current user is not in the channel\nCHANNEL_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\nCHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available\nCHANNEL_TOO_BIG,400,\nCHANNEL_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)\nCHAT_ABOUT_NOT_MODIFIED,400,About text has not changed\nCHAT_ABOUT_TOO_LONG,400,Chat about too long\nCHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this\nCHAT_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\"\nCHAT_DISCUSSION_UNALLOWED,400,\nCHAT_FORBIDDEN,403,You cannot write in this chat\nCHAT_FORWARDS_RESTRICTED,400 406,You can't forward messages from a protected chat\nCHAT_GET_FAILED,500,\nCHAT_GUEST_SEND_FORBIDDEN,403,\"You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info\"\nCHAT_ID_EMPTY,400,The provided chat ID is empty\nCHAT_ID_GENERATE_FAILED,500,Failure while generating the chat ID\nCHAT_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\"\nCHAT_INVALID,400,The chat is invalid for this request\nCHAT_INVITE_PERMANENT,400,You can't set an expiration date on permanent invite links\nCHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request\nCHAT_NOT_MODIFIED,400,\"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)\"\nCHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request\nCHAT_REVOKE_DATE_UNSUPPORTED,400,`min_date` and `max_date` are not available for using with non-user peers\nCHAT_SEND_GAME_FORBIDDEN,403,You can't send a game to this chat\nCHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat\nCHAT_SEND_INLINE_FORBIDDEN,400 403,You cannot send inline results in this chat\nCHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat\nCHAT_SEND_POLL_FORBIDDEN,403,You can't send polls in this chat\nCHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat\nCHAT_TITLE_EMPTY,400,No chat title provided\nCHAT_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)\"\nCHAT_WRITE_FORBIDDEN,403,You can't write in this chat\nCHP_CALL_FAIL,500,The statistics cannot be retrieved at this time\nCODE_EMPTY,400,The provided code is empty\nCODE_HASH_INVALID,400,Code hash invalid\nCODE_INVALID,400,Code invalid (i.e. from email)\nCONNECTION_API_ID_INVALID,400,The provided API id is invalid\nCONNECTION_APP_VERSION_EMPTY,400,App version is empty\nCONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty\nCONNECTION_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\"\nCONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest\nCONNECTION_NOT_INITED,400,Connection not initialized\nCONNECTION_SYSTEM_EMPTY,400,Connection system empty\nCONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection\nCONTACT_ADD_MISSING,400,Contact to add is missing\nCONTACT_ID_INVALID,400,The provided contact ID is invalid\nCONTACT_NAME_EMPTY,400,The provided contact name cannot be empty\nCONTACT_REQ_MISSING,400,Missing contact request\nCREATE_CALL_FAILED,400,An error occurred while creating the call\nCURRENCY_TOTAL_AMOUNT_INVALID,400,The total amount of all prices is invalid\nDATA_INVALID,400,Encrypted data invalid\nDATA_JSON_INVALID,400,The provided JSON data is invalid\nDATA_TOO_LONG,400,Data too long\nDATE_EMPTY,400,Date empty\nDC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to\nDH_G_A_INVALID,400,g_a invalid\nDOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode\nEDIT_BOT_INVITE_FORBIDDEN,403,Normal users can't edit invites that were created by bots\nEMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it\nEMAIL_INVALID,400,The given email is invalid\nEMAIL_UNCONFIRMED,400,Email unconfirmed\nEMAIL_UNCONFIRMED_X,400,\"Email unconfirmed, the length of the code must be {code_length}\"\nEMAIL_VERIFY_EXPIRED,400,The verification email has expired\nEMOJI_INVALID,400,The specified theme emoji is valid\nEMOJI_NOT_MODIFIED,400,The theme wasn't changed\nEMOTICON_EMPTY,400,The emoticon field cannot be empty\nEMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon\nEMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing\nENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid\nENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted\nENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined\nENCRYPTION_DECLINED,400,The secret chat was declined\nENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid\nENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420\nENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs)\nENTITY_BOUNDS_INVALID,400,Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string)\nENTITY_MENTION_USER_INVALID,400,You can't use this entity\nERROR_TEXT_EMPTY,400,The provided error message is empty\nEXPIRE_DATE_INVALID,400,The specified expiration date is invalid\nEXPIRE_FORBIDDEN,400,\nEXPORT_CARD_INVALID,400,Provided card is invalid\nEXTERNAL_URL_INVALID,400,External URL invalid\nFIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing\nFIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid\nFILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again\nFILE_CONTENT_TYPE_INVALID,400,File content-type is invalid\nFILE_EMTPY,400,An empty file was provided\nFILE_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 ...)\"\nFILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc}\nFILE_PARTS_INVALID,400,The number of file parts is invalid\nFILE_PART_0_MISSING,400,File part 0 missing\nFILE_PART_EMPTY,400,The provided file part is empty\nFILE_PART_INVALID,400,The file part number is invalid\nFILE_PART_LENGTH_INVALID,400,The length of a file part is invalid\nFILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload\nFILE_PART_SIZE_INVALID,400,The provided file part size is invalid\nFILE_PART_TOO_BIG,400,The uploaded file part is too big\nFILE_PART_X_MISSING,400,Part {which} of the file is missing from storage\nFILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty\nFILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent\nFILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message\nFILE_TITLE_EMPTY,400,An empty file title was specified\nFILTER_ID_INVALID,400,The specified filter ID is invalid\nFILTER_INCLUDE_EMPTY,400,The include_peers vector of the filter is empty\nFILTER_NOT_SUPPORTED,400,The specified filter cannot be used in this context\nFILTER_TITLE_EMPTY,400,The title field of the filter is empty\nFIRSTNAME_INVALID,400,The first name is invalid\nFLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers\nFLOOD_WAIT_X,420,A wait of {seconds} seconds is required\nFLOOD_PREMIUM_WAIT_X,420,A wait of {seconds} seconds is required in non-premium accounts\nFOLDER_ID_EMPTY,400,The folder you tried to delete was already empty\nFOLDER_ID_INVALID,400,The folder you tried to use was not valid\nFRESH_CHANGE_ADMINS_FORBIDDEN,400 406,Recently logged-in users cannot add or change admins\nFRESH_CHANGE_PHONE_FORBIDDEN,406,Recently logged-in users cannot use this request\nFRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet\nFROM_MESSAGE_BOT_DISABLED,400,Bots can't use fromMessage min constructors\nFROM_PEER_INVALID,400,The given from_user peer cannot be used for the parameter\nGAME_BOT_INVALID,400,You cannot send that game with the current bot\nGEO_POINT_INVALID,400,Invalid geoposition provided\nGIF_CONTENT_TYPE_INVALID,400,GIF content-type invalid\nGIF_ID_INVALID,400,The provided GIF ID is invalid\nGRAPH_EXPIRED_RELOAD,400,\"This graph has expired, please obtain a new graph token\"\nGRAPH_INVALID_RELOAD,400,\"Invalid graph token provided, please reload the stats and provide the updated token\"\nGRAPH_OUTDATED_RELOAD,400,\"Data can't be used for the channel statistics, graphs outdated\"\nGROUPCALL_ADD_PARTICIPANTS_FAILED,500,\nGROUPCALL_ALREADY_DISCARDED,400,The group call was already discarded\nGROUPCALL_ALREADY_STARTED,403,\"The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)\"\nGROUPCALL_FORBIDDEN,403,The group call has already ended\nGROUPCALL_INVALID,400,The specified group call is invalid\nGROUPCALL_JOIN_MISSING,400,You haven't joined this group call\nGROUPCALL_NOT_MODIFIED,400,Group call settings weren't modified\nGROUPCALL_SSRC_DUPLICATE_MUCH,400,The app needs to retry joining the group call with a new SSRC value\nGROUPED_MEDIA_INVALID,400,Invalid grouped media\nGROUP_CALL_INVALID,400,Group call invalid\nHASH_INVALID,400,The provided hash is invalid\nHIDE_REQUESTER_MISSING,400,The join request was missing or was already handled\nHISTORY_GET_FAILED,500,Fetching of history failed\nIMAGE_PROCESS_FAILED,400,Failure while processing image\nIMPORT_FILE_INVALID,400,The file is too large to be imported\nIMPORT_FORMAT_UNRECOGNIZED,400,Unknown import format\nIMPORT_ID_INVALID,400,The specified import ID is invalid\nINLINE_BOT_REQUIRED,403,The action must be performed through an inline bot callback\nINLINE_RESULT_EXPIRED,400,The inline query expired\nINPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid\nINPUT_FETCH_ERROR,400,An error occurred while deserializing TL parameters\nINPUT_FETCH_FAIL,400,Failed deserializing TL payload\nINPUT_FILTER_INVALID,400,The search query filter is invalid\nINPUT_LAYER_INVALID,400,The provided layer is invalid\nINPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed\nINPUT_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)\nINPUT_TEXT_EMPTY,400,The specified text is empty\nINPUT_USER_DEACTIVATED,400,The specified user was deleted\nINTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc}\nINTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc}\nINVITE_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\"\nINVITE_HASH_EMPTY,400,The invite hash is empty\nINVITE_HASH_EXPIRED,400 406,The chat the user tried to join has expired and is not valid anymore\nINVITE_HASH_INVALID,400,The invite hash is invalid\nINVITE_REQUEST_SENT,400,You have successfully requested to join this chat or channel\nINVITE_REVOKED_MISSING,400,The specified invite link was already revoked or is invalid\nINVOICE_PAYLOAD_INVALID,400,The specified invoice payload is invalid\nJOIN_AS_PEER_INVALID,400,The specified peer cannot be used to join a group call\nLANG_CODE_INVALID,400,The specified language code is invalid\nLANG_CODE_NOT_SUPPORTED,400,The specified language code is not supported\nLANG_PACK_INVALID,400,The provided language pack is invalid\nLASTNAME_INVALID,400,The last name is invalid\nLIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files\nLINK_NOT_MODIFIED,400,The channel is already linked to this group\nLOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files\nMAX_DATE_INVALID,400,The specified maximum date is invalid\nMAX_ID_INVALID,400,The provided max ID is invalid\nMAX_QTS_INVALID,400,The provided QTS were invalid\nMD5_CHECKSUM_INVALID,400,The MD5 check-sums do not match\nMEDIA_CAPTION_TOO_LONG,400,The caption is too long\nMEDIA_EMPTY,400,The provided media object is invalid or the current account may not be able to send it (such as games as users)\nMEDIA_GROUPED_INVALID,400,You tried to send media of different types in an album\nMEDIA_INVALID,400,Media invalid\nMEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes)\nMEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes)\nMEDIA_TTL_INVALID,400,\nMEGAGROUP_ID_INVALID,400,The group is invalid\nMEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden\nMEGAGROUP_REQUIRED,400,The request can only be used with a megagroup channel\nMEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location)\nMEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed\nMESSAGE_AUTHOR_REQUIRED,403,Message author required\nMESSAGE_DELETE_FORBIDDEN,403,\"You can't delete one of the messages you tried to delete, most likely because it is a service message.\"\nMESSAGE_EDIT_TIME_EXPIRED,400,\"You can't edit this message anymore, too much time has passed since its creation.\"\nMESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent\nMESSAGE_IDS_EMPTY,400,No message ids were provided\nMESSAGE_ID_INVALID,400,The specified message ID is invalid or you can't do that operation on such message\nMESSAGE_NOT_MODIFIED,400,Content of the message was not modified\nMESSAGE_POLL_CLOSED,400,The poll was closed and can no longer be voted on\nMESSAGE_TOO_LONG,400,Message was too long\nMETHOD_INVALID,400,The API method is invalid and cannot be used\nMIN_DATE_INVALID,400,The specified minimum date is invalid\nMSGID_DECREASE_RETRY,500,The request should be retried with a lower message ID\nMSG_ID_INVALID,400,The message ID used in the peer was invalid\nMSG_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\"\nMSG_WAIT_FAILED,400,A waiting call returned an error\nMT_SEND_QUEUE_TOO_LONG,500,\nMULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album\nNEED_CHAT_INVALID,500,The provided chat is invalid\nNEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size)\nNETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc}\nNEW_SALT_INVALID,400,The new salt is invalid\nNEW_SETTINGS_EMPTY,400,\"No password is set on the current account, and no new password was specified in `new_settings`\"\nNEW_SETTINGS_INVALID,400,The new settings are invalid\nNEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long\nNOT_ALLOWED,403,\nOFFSET_INVALID,400,\"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files\"\nOFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid\nOPTIONS_TOO_MUCH,400,You defined too many options for the poll\nOPTION_INVALID,400,The option specified is invalid and does not exist in the target poll\nPACK_SHORT_NAME_INVALID,400,\"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in \"\"_by_<bot username>\"\".\"\nPACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists\nPACK_TITLE_INVALID,400,The stickerpack title is invalid\nPARTICIPANTS_TOO_FEW,400,Not enough participants\nPARTICIPANT_CALL_FAILED,500,Failure while making call\nPARTICIPANT_ID_INVALID,400,The specified participant ID is invalid\nPARTICIPANT_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)\"\nPARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls\nPASSWORD_EMPTY,400,The provided password is empty\nPASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid\nPASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used\nPASSWORD_RECOVERY_EXPIRED,400,The recovery code has expired\nPASSWORD_RECOVERY_NA,400,\"No email was set, can't recover password via email\"\nPASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used\nPASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method\nPAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid\nPEER_FLOOD,400,Too many requests\nPEER_HISTORY_EMPTY,400,\nPEER_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)\"\nPEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported\nPERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty\nPERSISTENT_TIMESTAMP_INVALID,400,Persistent timestamp invalid\nPERSISTENT_TIMESTAMP_OUTDATED,500,Persistent timestamp outdated\nPHONE_CODE_EMPTY,400,The phone code is missing\nPHONE_CODE_EXPIRED,400,The confirmation code has expired\nPHONE_CODE_HASH_EMPTY,400,The phone code hash is missing\nPHONE_CODE_INVALID,400,The phone code entered was invalid\nPHONE_HASH_EXPIRED,400,An invalid or expired `phone_code_hash` was provided\nPHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc}\nPHONE_NOT_OCCUPIED,400,No user is associated to the specified phone number\nPHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app\nPHONE_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\nPHONE_NUMBER_FLOOD,400,You asked for the code too many times.\nPHONE_NUMBER_INVALID,400 406,The phone number is invalid\nPHONE_NUMBER_OCCUPIED,400,The phone number is already in use\nPHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used\nPHONE_PASSWORD_FLOOD,406,You have tried logging in too many times\nPHONE_PASSWORD_PROTECTED,400,This phone is password protected\nPHOTO_CONTENT_TYPE_INVALID,400,Photo mime-type invalid\nPHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error\nPHOTO_CROP_FILE_MISSING,400,Photo crop file missing\nPHOTO_CROP_SIZE_SMALL,400,Photo is too small\nPHOTO_EXT_INVALID,400,The extension of the photo is invalid\nPHOTO_FILE_MISSING,400,Profile photo file missing\nPHOTO_ID_INVALID,400,Photo id is invalid\nPHOTO_INVALID,400,Photo invalid\nPHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images)\nPHOTO_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\nPHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error\nPINNED_DIALOGS_TOO_MUCH,400,Too many pinned dialogs\nPIN_RESTRICTED,400,You can't pin messages in private chats with other people\nPOLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many\nPOLL_ANSWER_INVALID,400,One of the poll answers is not acceptable\nPOLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll\nPOLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long)\nPOLL_QUESTION_INVALID,400,The poll question was either empty or too long\nPOLL_UNSUPPORTED,400,This layer does not support polls in the issued method\nPOLL_VOTE_REQUIRED,403,Cast a vote in the poll before calling this method\nPOSTPONED_TIMEOUT,500,The postponed call has timed out\nPREMIUM_ACCOUNT_REQUIRED,403,A premium account is required to execute this action\nPREMIUM_CURRENTLY_UNAVAILABLE,406,\nPREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,\"Similar to a flood wait, must wait {minutes} minutes\"\nPRIVACY_KEY_INVALID,400,The privacy key is invalid\nPRIVACY_TOO_LONG,400,Cannot add that many entities in a single request\nPRIVACY_VALUE_INVALID,400,The privacy value is invalid\nPTS_CHANGE_EMPTY,500,No PTS change\nPUBLIC_CHANNEL_MISSING,403,You can only export group call invite links for public chats or channels\nPUBLIC_KEY_REQUIRED,400,A public key is required\nQUERY_ID_EMPTY,400,The query ID is empty\nQUERY_ID_INVALID,400,The query ID is invalid\nQUERY_TOO_SHORT,400,The query string is too short\nQUIZ_ANSWER_MISSING,400,You can forward a quiz while hiding the original author only after choosing an option in the quiz\nQUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer\nQUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer\nQUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer\nQUIZ_MULTIPLE_INVALID,400,A poll cannot be both multiple choice and quiz\nRANDOM_ID_DUPLICATE,500,You provided a random ID that was already used\nRANDOM_ID_EMPTY,400,Random ID empty\nRANDOM_ID_INVALID,400,A provided random ID is invalid\nRANDOM_LENGTH_INVALID,400,Random length invalid\nRANGES_INVALID,400,Invalid range provided\nREACTIONS_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)\"\nREACTION_EMPTY,400,No reaction provided\nREACTION_INVALID,400,Invalid reaction provided (only emoji are allowed)\nREFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server\nREG_ID_GENERATE_FAILED,500,Failure while generating registration ID\nREPLY_MARKUP_BUY_EMPTY,400,Reply markup for buy button empty\nREPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty\nREPLY_MARKUP_INVALID,400,The provided reply markup is invalid\nREPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much\nRESET_REQUEST_MISSING,400,No password reset is in progress\nRESULTS_TOO_MUCH,400,\"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit\"\nRESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs\nRESULT_ID_EMPTY,400,Result ID empty\nRESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot\nRESULT_TYPE_INVALID,400,Result type invalid\nREVOTE_NOT_ALLOWED,400,You cannot change your vote\nRIGHTS_NOT_MODIFIED,400,\"The new admin rights are equal to the old rights, no change was made\"\nRIGHT_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)\nRPC_CALL_FAIL,500,\"Telegram is having internal issues, please try again later.\"\nRPC_MCGET_FAIL,500,\"Telegram is having internal issues, please try again later.\"\nRSA_DECRYPT_FAILED,400,Internal RSA decryption failed\nSCHEDULE_BOT_NOT_ALLOWED,400,Bots are not allowed to schedule messages\nSCHEDULE_DATE_INVALID,400,Invalid schedule date provided\nSCHEDULE_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)\nSCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information\nSCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat)\nSCORE_INVALID,400,The specified game score is invalid\nSEARCH_QUERY_EMPTY,400,The search query is empty\nSEARCH_WITH_LINK_NOT_SUPPORTED,400,You cannot provide a search query and an invite link at the same time\nSECONDS_INVALID,400,\"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)\"\nSEND_AS_PEER_INVALID,400,You can't send messages as the specified peer\nSEND_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)\"\nSEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified\nSEND_MESSAGE_TYPE_INVALID,400,The message type is invalid\nSENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time\nSESSION_EXPIRED,401,The authorization has expired\nSESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required\nSESSION_REVOKED,401,\"The authorization has been invalidated, because of the user terminating all sessions\"\nSESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method\nSETTINGS_INVALID,400,Invalid settings were provided\nSHA256_HASH_INVALID,400,The provided SHA256 hash is invalid\nSHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name\nSHORT_NAME_INVALID,400,The specified short name is invalid\nSHORT_NAME_OCCUPIED,400,The specified short name is already in use\nSIGN_IN_FAILED,500,Failure while signing in\nSLOWMODE_MULTI_MSGS_DISABLED,400,\"Slowmode is enabled, you cannot forward multiple messages to this group\"\nSLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat\nSMS_CODE_CREATE_FAILED,400,An error occurred while creating the SMS code\nSRP_ID_INVALID,400,Invalid SRP ID provided\nSRP_PASSWORD_CHANGED,400,Password has changed\nSTART_PARAM_EMPTY,400,The start parameter is empty\nSTART_PARAM_INVALID,400,Start parameter invalid\nSTART_PARAM_TOO_LONG,400,Start parameter is too long\nSTATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc}\nSTICKERPACK_STICKERS_TOO_MUCH,400,\"There are too many stickers in this stickerpack, you can't add any more\"\nSTICKERSET_INVALID,400 406,The provided sticker set is invalid\nSTICKERSET_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\nSTICKERS_EMPTY,400,No sticker provided\nSTICKERS_TOO_MUCH,400,\"There are too many stickers in this stickerpack, you can't add any more\"\nSTICKER_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)\"\nSTICKER_EMOJI_INVALID,400,Sticker emoji invalid\nSTICKER_FILE_INVALID,400,Sticker file invalid\nSTICKER_GIF_DIMENSIONS,400,The specified video sticker has invalid dimensions\nSTICKER_ID_INVALID,400,The provided sticker ID is invalid\nSTICKER_INVALID,400,The provided sticker is invalid\nSTICKER_MIME_INVALID,400,Make sure to pass a valid image file for the right InputFile parameter\nSTICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid\nSTICKER_PNG_NOPNG,400,Stickers must be a png file but the used image was not a png\nSTICKER_TGS_NODOC,400,You must send the animated sticker as a document\nSTICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs\nSTICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png\nSTICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs\nSTICKER_VIDEO_BIG,400,The specified video sticker is too big\nSTICKER_VIDEO_NODOC,400,You must send the video sticker as a document\nSTICKER_VIDEO_NOWEBM,400,The specified video sticker is not in webm format\nSTORAGE_CHECK_FAILED,500,Server storage check failed\nSTORE_INVALID_SCALAR_TYPE,500,\nSWITCH_PM_TEXT_EMPTY,400,The switch_pm.text field was empty\nTAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout\nTAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session\nTAKEOUT_REQUIRED,400 403,You must initialize a takeout request first\nTEMP_AUTH_KEY_ALREADY_BOUND,400,The passed temporary key is already bound to another **perm_auth_key_id**\nTEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided\nTHEME_FILE_INVALID,400,Invalid theme file provided\nTHEME_FORMAT_INVALID,400,Invalid theme format provided\nTHEME_INVALID,400,Theme invalid\nTHEME_MIME_INVALID,400,\"You cannot create this theme, the mime-type is invalid\"\nTHEME_TITLE_INVALID,400,The specified theme title is invalid\nTIMEOUT,500,A timeout occurred while fetching data from the worker\nTITLE_INVALID,400,The specified stickerpack title is invalid\nTMP_PASSWORD_DISABLED,400,The temporary password is disabled\nTMP_PASSWORD_INVALID,400,Password auth needs to be regenerated\nTOKEN_INVALID,400,The provided token is invalid\nTOPIC_DELETED,400,The topic was deleted\nTO_LANG_INVALID,400,The specified destination language is invalid\nTTL_DAYS_INVALID,400,The provided TTL is invalid\nTTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL\nTTL_PERIOD_INVALID,400,The provided TTL Period is invalid\nTYPES_EMPTY,400,The types field is empty\nTYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid\nTimedout,-503,Timeout while fetching data\nTimeout,-503,Timeout while fetching data\nUNKNOWN_ERROR,400,\nUNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs\nUNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None)\nUPDATE_APP_TO_LOGIN,406,\nURL_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)\nUSAGE_LIMIT_INVALID,400,The specified usage limit is invalid\nUSERNAME_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]\"\"\"\nUSERNAME_NOT_MODIFIED,400,The username is not different from the current username\nUSERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet\nUSERNAME_OCCUPIED,400,The username is already taken\nUSERNAME_PURCHASE_AVAILABLE,400,\nUSERPIC_PRIVACY_REQUIRED,406,You need to disable privacy settings for your profile picture in order to make your geolocation public\nUSERPIC_UPLOAD_REQUIRED,400 406,You must have a profile picture before using this method\nUSERS_TOO_FEW,400,\"Not enough users (to create a chat, for example)\"\nUSERS_TOO_MUCH,400,\"The maximum number of users has been exceeded (to create a chat, for example)\"\nUSER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote\nUSER_ALREADY_INVITED,400,You have already invited this user\nUSER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat\nUSER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels\nUSER_BLOCKED,400,User blocked\nUSER_BOT,400,Bots can only be admins in channels.\nUSER_BOT_INVALID,400 403,This method can only be called by a bot\nUSER_BOT_REQUIRED,400,This method can only be called by a bot\nUSER_CHANNELS_TOO_MUCH,400 403,One of the users you tried to add is already in too many channels/supergroups\nUSER_CREATOR,400,\"You can't leave this channel, because you're its creator\"\nUSER_DEACTIVATED,401,The user has been deleted/deactivated\nUSER_DEACTIVATED_BAN,401,The user has been deleted/deactivated\nUSER_DELETED,403,You can't send this secret message because the other participant deleted their account\nUSER_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\"\nUSER_INVALID,400 403,The given user was invalid\nUSER_IS_BLOCKED,400 403,User is blocked\nUSER_IS_BOT,400,Bots can't send messages to other bots\nUSER_KICKED,400,This user was kicked from this supergroup/channel\nUSER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc}\nUSER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact\nUSER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel\nUSER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this\nUSER_RESTRICTED,403 406,\"You're spamreported, you can't create channels or chats.\"\nUSER_VOLUME_INVALID,400,The specified user volume is invalid\nVIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming)\nVIDEO_FILE_INVALID,400,The given video cannot be used\nVIDEO_TITLE_EMPTY,400,The specified video title is empty\nVOICE_MESSAGES_FORBIDDEN,400,This user's privacy settings forbid you from sending voice messages\nWALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper\nWALLPAPER_INVALID,400,The input wallpaper was not valid\nWALLPAPER_MIME_INVALID,400,The specified wallpaper MIME type is invalid\nWC_CONVERT_URL_INVALID,400,WC convert URL invalid\nWEBDOCUMENT_INVALID,400,Invalid webdocument URL provided\nWEBDOCUMENT_MIME_INVALID,400,Invalid webdocument mime type provided\nWEBDOCUMENT_SIZE_TOO_BIG,400,Webdocument is too big!\nWEBDOCUMENT_URL_INVALID,400,The given URL cannot be used\nWEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL\nWEBPAGE_MEDIA_EMPTY,400,Webpage media empty\nWEBPUSH_AUTH_INVALID,400,The specified web push authentication secret is invalid\nWEBPUSH_KEY_INVALID,400,The specified web push elliptic curve Diffie-Hellman public key is invalid\nWEBPUSH_TOKEN_INVALID,400,The specified web push token is invalid\nWORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately\nYOU_BLOCKED_USER,400,You blocked this user\nFROZEN_METHOD_INVALID,420,You tried to use a method that is not available for frozen accounts\nFROZEN_PARTICIPANT_MISSING,400,Your account is frozen and can't access the chat\nCHAT_SEND_VOICES_FORBIDDEN,403,You cannot send voices results in this chat\nCHAT_SEND_PHOTOS_FORBIDDEN,403,You cannot send photos results in this chat\nCHAT_SEND_VIDEOS_FORBIDDEN,403,You cannot send videos results in this Chat\nCHAT_SEND_PLAIN_FORBIDDEN,403,You cannot send plain results in this chat\n"
  },
  {
    "path": "telethon_generator/data/friendly.csv",
    "content": "ns,friendly,raw\naccount.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession\nauth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization\nauth.AuthMethods,send_code_request,auth.sendCode auth.resendCode\nauth.AuthMethods,log_out,auth.logOut\nauth.AuthMethods,edit_2fa,account.updatePasswordSettings\nbots.BotMethods,inline_query,messages.getInlineBotResults\nchats.ChatMethods,action,messages.setTyping\nchats.ChatMethods,edit_admin,channels.editAdmin messages.editChatAdmin\nchats.ChatMethods,edit_permissions,channels.editBanned messages.editChatDefaultBannedRights\nchats.ChatMethods,iter_participants,channels.getParticipants\nchats.ChatMethods,iter_admin_log,channels.getAdminLog\ndialogs.DialogMethods,iter_dialogs,messages.getDialogs\ndialogs.DialogMethods,iter_drafts,messages.getAllDrafts\ndialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders\ndownloads.DownloadMethods,download_media,upload.getFile\nmessages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages\nmessages.MessageMethods,send_message,messages.sendMessage\nmessages.MessageMethods,forward_messages,messages.forwardMessages\nmessages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage\nmessages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages\nmessages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory\nupdates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference\nuploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia\nuploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart\nusers.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername\n"
  },
  {
    "path": "telethon_generator/data/html/404.html",
    "content": "<!DOCTYPE html>\n<html><head>\n    <title>Oopsie! | Telethon</title>\n\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <style type=\"text/css\">\n    body {\n        background-color: #f0f4f8;\n        font-family: \"Open Sans\", \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n    }\n    div {\n        width: 560px;\n        margin: 5em auto;\n        padding: 50px;\n        background-color: #fff;\n        border-radius: 1em;\n    }\n    a:link, a:visited {\n        color: #38488f;\n        text-decoration: none;\n    }\n    @media (max-width: 700px) {\n        body {\n            background-color: #fff;\n        }\n        div {\n            width: auto;\n            margin: 0 auto;\n            border-radius: 0;\n            padding: 1em;\n        }\n    }\n    </style>\n</head>\n<body>\n<div>\n    <h1>You seem a bit lost…</h1>\n    <p>You seem to be lost! Don't worry, that's just Telegram's API being\n    itself. Shall we go back to the <a href=\"index.html\">Main Page</a>?</p>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "telethon_generator/data/html/core.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <title>Telethon API</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link id=\"style\" href=\"css/docs.dark.css\" rel=\"stylesheet\">\n    <script>\n        (function () {\n            var style = document.getElementById('style');\n\n            // setTheme(<link />, 'light' / 'dark')\n            function setTheme(theme) {\n                localStorage.setItem('theme', theme);\n                return style.href = 'css/docs.' + theme + '.css';\n            }\n\n            // setThemeOnClick(<link />, 'light' / 'dark', <a />)\n            function setThemeOnClick(theme, button) {\n                return button.addEventListener('click', function (e) {\n                    setTheme(theme);\n                    e.preventDefault();\n                    return false;\n                });\n            }\n\n            setTheme(localStorage.getItem('theme') || 'light');\n\n            document.addEventListener('DOMContentLoaded', function () {\n                setThemeOnClick('light', document.getElementById('themeLight'));\n                setThemeOnClick('dark', document.getElementById('themeDark'));\n            });\n        })();\n    </script>\n    <link href=\"https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro\" rel=\"stylesheet\">\n</head>\n<body>\n<div id=\"main_div\">\n    <noscript>Please enable JavaScript if you would like to use search.</noscript>\n    <h1>Telethon API</h1>\n    <p>This documentation was generated straight from the <code>scheme.tl</code>\n    provided by Telegram. However, there is no official documentation per se\n    on what the methods, constructors and types mean. Nevertheless, this\n    page aims to provide easy access to all the available methods, their\n    definition and parameters.</p>\n    <p id=\"themeSelect\">\n        <a href=\"#\" id=\"themeLight\">light</a> /\n        <a href=\"#\" id=\"themeDark\">dark</a> theme.\n    </p>\n    <p>Please note that when you see this:</p>\n    <pre>---functions---\nusers.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>\n\n    <p>This is <b>not</b> Python code. It's the \"TL definition\". It's\n    an easy-to-read line that gives a quick overview on the parameters\n    and its result. You don't need to worry about this. See\n    <a href=\"https://docs.telethon.dev/en/stable/developing/understanding-the-type-language.html\">Understanding\n    the Type Language</a> for more details on it.</p>\n\n    <h3>Index</h3>\n    <ul>\n        <li>\n            <a href=\"#methods\">Methods</a>\n            (<a href=\"methods/index.html\">full list</a>)\n        </li>\n        <li>\n            <a href=\"#types\">Types</a>\n            (<a href=\"types/index.html\">full list</a>)\n        </li>\n        <li>\n            <a href=\"#constructors\">Constructors</a>\n            (<a href=\"constructors/index.html\">full list</a>)\n        </li>\n        <li><a href=\"#core\">Core types</a></li>\n        <li><a href=\"#example\">Full example</a></li>\n    </ul>\n\n    <h3 id=\"methods\">Methods</h3>\n    <p>Currently there are <b>{method_count} methods</b> available for the layer\n    {layer}. <a href=\"methods/index.html\">See the complete method list</a>.\n    <br /><br />\n    Methods, also known as <i>requests</i>, are used to interact with the\n    Telegram API itself and are invoked through <code>client(Request(...))</code>.\n    <b>Only these</b> can be used like that! You cannot invoke types or\n    constructors, only requests. After this, Telegram will return a\n    <code>result</code>, which may be, for instance, a bunch of messages,\n    some dialogs, users, etc.</p>\n\n    <h3 id=\"types\">Types</h3>\n    <p>Currently there are <b>{type_count} types</b>.\n    <a href=\"types/index.html\">See the complete list of types</a>.</p>\n\n    <p>The Telegram types are the <i>abstract</i> results that you receive\n    after invoking a request. They are \"abstract\" because they can have\n    multiple constructors. For instance, the abstract type <code>User</code>\n    can be either <code>UserEmpty</code> or <code>User</code>. You should,\n    most of the time, make sure you received the desired type by using\n    the <code>isinstance(result, Constructor)</code> Python function.\n\n    When a request needs a Telegram type as argument, you should create\n    an instance of it by using one of its, possibly multiple, constructors.</p>\n\n    <h3 id=\"constructors\">Constructors</h3>\n    <p>Currently there are <b>{constructor_count} constructors</b>.\n    <a href=\"constructors/index.html\">See the list of all constructors</a>.</p>\n\n    <p>Constructors are the way you can create instances of the abstract types\n    described above, and also the instances which are actually returned from\n    the functions although they all share a common abstract type.</p>\n\n    <h3 id=\"core\">Core types</h3>\n    <p>Core types are types from which the rest of Telegram types build upon:</p>\n    <ul>\n    <li id=\"int\"><b>int</b>:\n        The value should be an integer type, like <span class=\"sh1\">42</span>.\n        It should have 32 bits or less. You can check the bit length by\n        calling <code>a.bit_length()</code>, where <code>a</code> is an\n        integer variable.\n    </li>\n    <li id=\"long\"><b>long</b>:\n        Different name for an integer type. The numbers given should have\n        64 bits or less.\n    </li>\n    <li id=\"int128\"><b>int128</b>:\n        Another integer type, should have 128 bits or less.\n    </li>\n    <li id=\"int256\"><b>int256</b>:\n        The largest integer type, allowing 256 bits or less.\n    </li>\n    <li id=\"double\"><b>double</b>:\n        The value should be a floating point value, such as\n        <span class=\"sh1\">123.456</span>.\n    </li>\n    <li id=\"vector\"><b>Vector&lt;T&gt;</b>:\n        If a type <code>T</code> is wrapped around <code>Vector&lt;T&gt;</code>,\n        then it means that the argument should be a <i>list</i> of it.\n        For instance, a valid value for <code>Vector&lt;int&gt;</code>\n        would be <code>[1, 2, 3]</code>.\n    </li>\n    <li id=\"string\"><b>string</b>:\n        A valid UTF-8 string should be supplied. This is right how\n        Python strings work, no further encoding is required.\n    </li>\n    <li id=\"bool\"><b>Bool</b>:\n        Either <code>True</code> or <code>False</code>.\n    </li>\n    <li id=\"true\"><b>flag</b>:\n        These arguments aren't actually sent but rather encoded as flags.\n        Any truthy value (<code>True</code>, <code>7</code>) will enable\n        this flag, although it's recommended to use <code>True</code> or\n        <code>None</code> to symbolize that it's not present.\n    </li>\n    <li id=\"bytes\"><b>bytes</b>:\n        A sequence of bytes, like <code>b'hello'</code>, should be supplied.\n    </li>\n    <li id=\"date\"><b>date</b>:\n        Although this type is internally used as an <code>int</code>,\n        you can pass a <code>datetime</code> or <code>date</code> object\n        instead to work with date parameters.<br />\n        Note that the library uses the date in <b>UTC+0</b>, since timezone\n        conversion is not responsibility of the library. Furthermore, this\n        eases converting into any other timezone without the need for a middle\n        step.\n    </li>\n    </ul>\n\n    <h3 id=\"example\">Full example</h3>\n    <p>All methods shown here have dummy examples on how to write them,\n    so you don't get confused with their TL definition. However, this may\n    not always run. They are just there to show the right syntax.</p>\n\n    <p>You should check out\n    <a href=\"https://docs.telethon.dev/en/stable/concepts/full-api.html\">how\n    to access the full API</a> in ReadTheDocs.\n    </p>\n</div>\n<script src=\"js/search.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "telethon_generator/data/html/css/docs.dark.css",
    "content": "body {\n    font-family: 'Nunito', sans-serif;\n    color: #bbb;\n    background-color:#000;\n    font-size: 16px;\n}\n\na {\n    color: #42aaed;\n    text-decoration: none;\n}\n\npre {\n    font-family: 'Source Code Pro', monospace;\n    padding: 8px;\n    color: #567;\n    background: #080a0c;\n    border-radius: 0;\n    overflow-x: auto;\n}\n\na:hover {\n    color: #64bbdd;\n    text-decoration: underline;\n}\n\ntable {\n    width: 100%;\n    max-width: 100%;\n}\n\ntable td {\n    border-top: 1px solid #111;\n    padding: 8px;\n}\n\n.horizontal {\n    margin-bottom: 16px;\n    list-style: none;\n    background: #080a0c;\n    border-radius: 4px;\n    padding: 8px 16px;\n}\n\n.horizontal li {\n    display: inline-block;\n    margin: 0 8px 0 0;\n}\n\n.horizontal img {\n    display: inline-block;\n    margin: 0 8px -2px 0;\n}\n\nh1, summary.title {\n    font-size: 24px;\n}\n\nh3 {\n    font-size: 20px;\n}\n\n#main_div {\n  padding: 20px 0;\n  max-width: 800px;\n  margin: 0 auto;\n}\n\npre::-webkit-scrollbar {\n    visibility: visible;\n    display: block;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-track:horizontal {\n    background: #222;\n    border-radius: 0;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-thumb:horizontal {\n    background: #444;\n    border-radius: 0;\n    height: 12px;\n}\n\n:target {\n    border: 2px solid #149;\n    background: #246;\n    padding: 4px;\n}\n\n/* 'sh' stands for Syntax Highlight */\nspan.sh1 {\n    color: #f93;\n}\n\nspan.tooltip {\n    border-bottom: 1px dashed #ddd;\n}\n\n#searchBox {\n    width: 100%;\n    border: none;\n    height: 20px;\n    padding: 8px;\n    font-size: 16px;\n    border-radius: 2px;\n    border: 2px solid #222;\n    background: #000;\n    color: #eee;\n}\n\n#searchBox:placeholder-shown {\n    color: #bbb;\n    font-style: italic;\n}\n\nbutton {\n    border-radius: 2px;\n    font-size: 16px;\n    padding: 8px;\n    color: #bbb;\n    background-color: #111;\n    border: 2px solid #146;\n    transition-duration: 300ms;\n}\n\nbutton:hover {\n    background-color: #146;\n    color: #fff;\n}\n\n/* https://www.w3schools.com/css/css_navbar.asp */\nul.together {\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n    overflow: hidden;\n}\n\nul.together li {\n    float: left;\n}\n\nul.together li a {\n    display: block;\n    border-radius: 8px;\n    background: #111;\n    padding: 4px 8px;\n    margin: 8px;\n}\n\n/* https://stackoverflow.com/a/30810322 */\n.invisible {\n    left: 0;\n    top: -99px;\n    padding: 0;\n    width: 2em;\n    height: 2em;\n    border: none;\n    outline: none;\n    position: fixed;\n    box-shadow: none;\n    color: transparent;\n    background: transparent;\n}\n\n@media (max-width: 640px) {\n    h1, summary.title {\n        font-size: 18px;\n    }\n    h3 {\n        font-size: 16px;\n    }\n\n    #dev_page_content_wrap {\n        padding-top: 12px;\n    }\n\n    #dev_page_title {\n        margin-top: 10px;\n        margin-bottom: 20px;\n    }\n}\n"
  },
  {
    "path": "telethon_generator/data/html/css/docs.h4x0r.css",
    "content": "/* Begin of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css\n *\n *  Hack typeface https://github.com/source-foundry/Hack\n *  License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md\n */\n@font-face {\n  font-family: 'Hack';\n  src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff');\n  font-weight: 400;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Hack';\n  src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff');\n  font-weight: 700;\n  font-style: normal;\n}\n\n@font-face {\n  font-family: 'Hack';\n  src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff');\n  font-weight: 400;\n  font-style: italic;\n}\n\n@font-face {\n  font-family: 'Hack';\n  src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff');\n  font-weight: 700;\n  font-style: italic;\n}\n\n/* End of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css */\n\nbody {\n    font-family: 'Hack', monospace;\n    color: #0a0;\n    background-color: #000;\n    font-size: 16px;\n}\n\n::-moz-selection {\n    color: #000;\n    background: #0a0;\n}\n\n::selection {\n    color: #000;\n    background: #0a0;\n}\n\na {\n    color: #0a0;\n}\n\npre {\n    padding: 8px;\n    color: #0c0;\n    background: #010;\n    border-radius: 0;\n    overflow-x: auto;\n}\n\na:hover {\n    color: #0f0;\n    text-decoration: underline;\n}\n\ntable {\n    width: 100%;\n    max-width: 100%;\n}\n\ntable td {\n    border-top: 1px solid #111;\n    padding: 8px;\n}\n\n.horizontal {\n    margin-bottom: 16px;\n    list-style: none;\n    background: #010;\n    border-radius: 4px;\n    padding: 8px 16px;\n}\n\n.horizontal li {\n    display: inline-block;\n    margin: 0 8px 0 0;\n}\n\n.horizontal img {\n    opacity: 0;\n    display: inline-block;\n    margin: 0 8px -2px 0;\n}\n\nh1, summary.title {\n    font-size: 24px;\n}\n\nh3 {\n    font-size: 20px;\n}\n\n#main_div {\n  padding: 20px 0;\n  max-width: 800px;\n  margin: 0 auto;\n}\n\npre::-webkit-scrollbar {\n    visibility: visible;\n    display: block;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-track:horizontal {\n    background: #222;\n    border-radius: 0;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-thumb:horizontal {\n    background: #444;\n    border-radius: 0;\n    height: 12px;\n}\n\n:target {\n    border: 2px solid #0f0;\n    background: #010;\n    padding: 4px;\n}\n\n/* 'sh' stands for Syntax Highlight */\nspan.sh1 {\n    color: #0f0;\n}\n\nspan.tooltip {\n    border-bottom: 1px dashed #ddd;\n}\n\n#searchBox {\n    width: 100%;\n    border: none;\n    height: 20px;\n    padding: 8px;\n    font-size: 16px;\n    border-radius: 2px;\n    border: 2px solid #222;\n    background: #000;\n    color: #0e0;\n    font-family: 'Hack', monospace;\n}\n\n#searchBox:placeholder-shown {\n    color: #0b0;\n    font-style: italic;\n}\n\nbutton {\n    font-size: 16px;\n    padding: 8px;\n    color: #0f0;\n    background-color: #071007;\n    border: 2px solid #131;\n    transition-duration: 300ms;\n    font-family: 'Hack', monospace;\n}\n\nbutton:hover {\n    background-color: #131;\n}\n\n/* https://www.w3schools.com/css/css_navbar.asp */\nul.together {\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n    overflow: hidden;\n}\n\nul.together li {\n    float: left;\n}\n\nul.together li a {\n    display: block;\n    border-radius: 8px;\n    background: #121;\n    padding: 4px 8px;\n    margin: 8px;\n}\n\n/* https://stackoverflow.com/a/30810322 */\n.invisible {\n    left: 0;\n    top: -99px;\n    padding: 0;\n    width: 2em;\n    height: 2em;\n    border: none;\n    outline: none;\n    position: fixed;\n    box-shadow: none;\n    color: transparent;\n    background: transparent;\n}\n\n@media (max-width: 640px) {\n    h1, summary.title {\n        font-size: 18px;\n    }\n    h3 {\n        font-size: 16px;\n    }\n\n    #dev_page_content_wrap {\n        padding-top: 12px;\n    }\n\n    #dev_page_title {\n        margin-top: 10px;\n        margin-bottom: 20px;\n    }\n}\n"
  },
  {
    "path": "telethon_generator/data/html/css/docs.light.css",
    "content": "body {\n    font-family: 'Nunito', sans-serif;\n    color: #333;\n    background-color:#eee;\n    font-size: 16px;\n}\n\na {\n    color: #329add;\n    text-decoration: none;\n}\n\npre {\n    font-family: 'Source Code Pro', monospace;\n    padding: 8px;\n    color: #567;\n    background: #e0e4e8;\n    border-radius: 0;\n    overflow-x: auto;\n}\n\na:hover {\n    color: #64bbdd;\n    text-decoration: underline;\n}\n\ntable {\n    width: 100%;\n    max-width: 100%;\n}\n\ntable td {\n    border-top: 1px solid #ddd;\n    padding: 8px;\n}\n\n.horizontal {\n    margin-bottom: 16px;\n    list-style: none;\n    background: #e0e4e8;\n    border-radius: 4px;\n    padding: 8px 16px;\n}\n\n.horizontal li {\n    display: inline-block;\n    margin: 0 8px 0 0;\n}\n\n.horizontal img {\n    display: inline-block;\n    margin: 0 8px -2px 0;\n}\n\nh1, summary.title {\n    font-size: 24px;\n}\n\nh3 {\n    font-size: 20px;\n}\n\n#main_div {\n  padding: 20px 0;\n  max-width: 800px;\n  margin: 0 auto;\n}\n\npre::-webkit-scrollbar {\n    visibility: visible;\n    display: block;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-track:horizontal {\n    background: #def;\n    border-radius: 0;\n    height: 12px;\n}\n\npre::-webkit-scrollbar-thumb:horizontal {\n    background: #bdd;\n    border-radius: 0;\n    height: 12px;\n}\n\n:target {\n    border: 2px solid #f8f800;\n    background: #f8f8f8;\n    padding: 4px;\n}\n\n/* 'sh' stands for Syntax Highlight */\nspan.sh1 {\n    color: #f70;\n}\n\nspan.tooltip {\n    border-bottom: 1px dashed #444;\n}\n\n#searchBox {\n    width: 100%;\n    border: none;\n    height: 20px;\n    padding: 8px;\n    font-size: 16px;\n    border-radius: 2px;\n    border: 2px solid #ddd;\n}\n\n#searchBox:placeholder-shown {\n    font-style: italic;\n}\n\nbutton {\n    border-radius: 2px;\n    font-size: 16px;\n    padding: 8px;\n    color: #000;\n    background-color: #f7f7f7;\n    border: 2px solid #329add;\n    transition-duration: 300ms;\n}\n\nbutton:hover {\n    background-color: #329add;\n    color: #f7f7f7;\n}\n\n/* https://www.w3schools.com/css/css_navbar.asp */\nul.together {\n    list-style-type: none;\n    margin: 0;\n    padding: 0;\n    overflow: hidden;\n}\n\nul.together li {\n    float: left;\n}\n\nul.together li a {\n    display: block;\n    border-radius: 8px;\n    background: #e0e4e8;\n    padding: 4px 8px;\n    margin: 8px;\n}\n\n/* https://stackoverflow.com/a/30810322 */\n.invisible {\n    left: 0;\n    top: -99px;\n    padding: 0;\n    width: 2em;\n    height: 2em;\n    border: none;\n    outline: none;\n    position: fixed;\n    box-shadow: none;\n    color: transparent;\n    background: transparent;\n}\n\n@media (max-width: 640px) {\n    h1, summary.title {\n        font-size: 18px;\n    }\n    h3 {\n        font-size: 16px;\n    }\n\n    #dev_page_content_wrap {\n        padding-top: 12px;\n    }\n\n    #dev_page_title {\n        margin-top: 10px;\n        margin-bottom: 20px;\n    }\n}\n"
  },
  {
    "path": "telethon_generator/data/html/js/search.js",
    "content": "root = document.getElementById(\"main_div\");\nroot.innerHTML = `\n<!-- You can append '?q=query' to the URL to default to a search -->\n<input id=\"searchBox\" type=\"text\" onkeyup=\"updateSearch(event)\"\n       placeholder=\"Search for requests and types…\" />\n\n<div id=\"searchDiv\">\n    <div id=\"exactMatch\" style=\"display:none;\">\n        <b>Exact match:</b>\n        <ul id=\"exactList\" class=\"together\">\n        </ul>\n    </div>\n\n    <details id=\"methods\" open><summary class=\"title\">Methods (<span id=\"methodsCount\">0</span>)</summary>\n        <ul id=\"methodsList\" class=\"together\">\n        </ul>\n    </details>\n\n    <details id=\"types\" open><summary class=\"title\">Types (<span id=\"typesCount\">0</span>)</summary>\n        <ul id=\"typesList\" class=\"together\">\n        </ul>\n    </details>\n\n    <details id=\"constructors\"><summary class=\"title\">Constructors (<span id=\"constructorsCount\">0</span>)</summary>\n        <ul id=\"constructorsList\" class=\"together\">\n        </ul>\n    </details>\n</div>\n<div id=\"contentDiv\">\n` + root.innerHTML + \"</div>\";\n\n// HTML modified, now load documents\ncontentDiv = document.getElementById(\"contentDiv\");\nsearchDiv = document.getElementById(\"searchDiv\");\nsearchBox = document.getElementById(\"searchBox\");\n\n// Search lists\nmethodsDetails = document.getElementById(\"methods\");\nmethodsList = document.getElementById(\"methodsList\");\nmethodsCount = document.getElementById(\"methodsCount\");\n\ntypesDetails = document.getElementById(\"types\");\ntypesList = document.getElementById(\"typesList\");\ntypesCount = document.getElementById(\"typesCount\");\n\nconstructorsDetails = document.getElementById(\"constructors\");\nconstructorsList = document.getElementById(\"constructorsList\");\nconstructorsCount = document.getElementById(\"constructorsCount\");\n\n// Exact match\nexactMatch = document.getElementById(\"exactMatch\");\nexactList = document.getElementById(\"exactList\");\n\ntry {\n    requests = [{request_names}];\n    types = [{type_names}];\n    constructors = [{constructor_names}];\n\n    requestsu = [{request_urls}];\n    typesu = [{type_urls}];\n    constructorsu = [{constructor_urls}];\n} catch (e) {\n    requests = [];\n    types = [];\n    constructors = [];\n    requestsu = [];\n    typesu = [];\n    constructorsu = [];\n}\n\nif (typeof prependPath !== 'undefined') {\n    for (var i = 0; i != requestsu.length; ++i) {\n        requestsu[i] = prependPath + requestsu[i];\n    }\n    for (var i = 0; i != typesu.length; ++i) {\n        typesu[i] = prependPath + typesu[i];\n    }\n    for (var i = 0; i != constructorsu.length; ++i) {\n        constructorsu[i] = prependPath + constructorsu[i];\n    }\n}\n\n// Assumes haystack has no whitespace and both are lowercase.\n//\n// Returns the penalty for finding the needle in the haystack\n// or -1 if the needle wasn't found at all.\nfunction find(haystack, needle) {\n    if (haystack.indexOf(needle) != -1) {\n        return 0;\n    }\n    var hi = 0;\n    var ni = 0;\n    var penalty = 0;\n    var started = false;\n    while (true) {\n        while (needle[ni] < 'a' || needle[ni] > 'z') {\n            ++ni;\n            if (ni == needle.length) {\n                return penalty;\n            }\n        }\n        while (haystack[hi] != needle[ni]) {\n            ++hi;\n            if (started) {\n                ++penalty;\n            }\n            if (hi == haystack.length) {\n                return -1;\n            }\n        }\n        ++hi;\n        ++ni;\n        started = true;\n        if (ni == needle.length) {\n            return penalty;\n        }\n        if (hi == haystack.length) {\n            return -1;\n        }\n    }\n}\n\n// Given two input arrays \"original\" and \"original urls\" and a query,\n// return a pair of arrays with matching \"query\" elements from \"original\".\n//\n// TODO Perhaps return an array of pairs instead a pair of arrays (for cache).\nfunction getSearchArray(original, originalu, query) {\n    var destination = [];\n    var destinationu = [];\n\n    for (var i = 0; i < original.length; ++i) {\n        var penalty = find(original[i].toLowerCase(), query);\n        if (penalty > -1 && penalty < original[i].length / 3) {\n            destination.push(original[i]);\n            destinationu.push(originalu[i]);\n        }\n    }\n\n    return [destination, destinationu];\n}\n\n// Modify \"countSpan\" and \"resultList\" accordingly based on the elements\n// given as [[elements], [element urls]] (both with the same length)\nfunction buildList(countSpan, resultList, foundElements) {\n    var result = \"\";\n    for (var i = 0; i < foundElements[0].length; ++i) {\n        result += '<li>';\n        result += '<a href=\"' + foundElements[1][i] + '\">';\n        result += foundElements[0][i];\n        result += '</a></li>';\n    }\n\n    if (countSpan) {\n        countSpan.innerHTML = \"\" + foundElements[0].length;\n    }\n    resultList.innerHTML = result;\n}\n\nfunction updateSearch(event) {\n    var query = searchBox.value.toLowerCase();\n    if (!query) {\n        contentDiv.style.display = \"\";\n        searchDiv.style.display = \"none\";\n        return;\n    }\n\n    contentDiv.style.display = \"none\";\n    searchDiv.style.display = \"\";\n\n    var foundRequests = getSearchArray(requests, requestsu, query);\n    var foundTypes = getSearchArray(types, typesu, query);\n    var foundConstructors = getSearchArray(constructors, constructorsu, query);\n\n    var original = requests.concat(constructors);\n    var originalu = requestsu.concat(constructorsu);\n    var destination = [];\n    var destinationu = [];\n\n    for (var i = 0; i < original.length; ++i) {\n        if (original[i].toLowerCase().replace(\"request\", \"\") == query) {\n            destination.push(original[i]);\n            destinationu.push(originalu[i]);\n        }\n    }\n\n    if (event && event.keyCode == 13) {\n        if (destination.length != 0) {\n            window.location = destinationu[0];\n        } else if (methodsDetails.open && foundRequests[1].length) {\n            window.location = foundRequests[1][0];\n        } else if (typesDetails.open && foundTypes[1].length) {\n            window.location = foundTypes[1][0];\n        } else if (constructorsDetails.open && foundConstructors[1].length) {\n            window.location = foundConstructors[1][0];\n        }\n        return;\n    }\n\n    buildList(methodsCount, methodsList, foundRequests);\n    buildList(typesCount, typesList, foundTypes);\n    buildList(constructorsCount, constructorsList, foundConstructors);\n\n    // Now look for exact matches\n    if (destination.length == 0) {\n        exactMatch.style.display = \"none\";\n    } else {\n        exactMatch.style.display = \"\";\n        buildList(null, exactList, [destination, destinationu]);\n        return destinationu[0];\n    }\n}\n\nfunction getQuery(name) {\n    var query = window.location.search.substring(1);\n    var vars = query.split(\"&\");\n    for (var i = 0; i != vars.length; ++i) {\n        var pair = vars[i].split(\"=\");\n        if (pair[0] == name)\n            return decodeURI(pair[1]);\n    }\n}\n\ndocument.onkeydown = function (e) {\n    if (e.key == '/' || e.key == 's' || e.key == 'S') {\n        if (document.activeElement != searchBox) {\n            searchBox.focus();\n            return false;\n        }\n    } else if (e.key == '?') {\n        alert('Pressing any of: /sS\\nWill focus the search bar\\n\\n' +\n              'Pressing: enter\\nWill navigate to the first match')\n    }\n}\n\nvar query = getQuery('q');\nif (query) {\n    searchBox.value = query;\n}\n\nvar exactUrl = updateSearch();\nvar redirect = getQuery('redirect');\nif (exactUrl && redirect != 'no') {\n    window.location = exactUrl;\n}\n"
  },
  {
    "path": "telethon_generator/data/methods.csv",
    "content": "method,usability,errors\naccount.acceptAuthorization,user,\naccount.cancelPasswordEmail,user,\naccount.changePhone,user,PHONE_NUMBER_INVALID\naccount.checkUsername,user,USERNAME_INVALID\naccount.confirmPasswordEmail,user,\naccount.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY\naccount.createTheme,user,THEME_MIME_INVALID\naccount.declinePasswordReset,user,RESET_REQUEST_MISSING\naccount.deleteAccount,user,2FA_CONFIRM_WAIT_X\naccount.deleteSecureValue,user,\naccount.finishTakeoutSession,user,\naccount.getAccountTTL,user,\naccount.getAllSecureValues,user,\naccount.getAuthorizationForm,user,PUBLIC_KEY_REQUIRED\naccount.getAuthorizations,user,\naccount.getAutoDownloadSettings,user,\naccount.getContactSignUpNotification,user,\naccount.getContentSettings,user,\naccount.getMultiWallPapers,user,\naccount.getNotifyExceptions,user,\naccount.getNotifySettings,user,PEER_ID_INVALID\naccount.getPassword,user,\naccount.getPasswordSettings,user,PASSWORD_HASH_INVALID\naccount.getPrivacy,user,PRIVACY_KEY_INVALID\naccount.getSecureValue,user,\naccount.getTheme,user,\naccount.getThemes,user,\naccount.getTmpPassword,user,PASSWORD_HASH_INVALID TMP_PASSWORD_DISABLED\naccount.getWallPaper,user,WALLPAPER_INVALID\naccount.getWallPapers,user,\naccount.getWebAuthorizations,user,\naccount.initTakeoutSession,user,\naccount.installTheme,user,\naccount.installWallPaper,user,WALLPAPER_INVALID\naccount.registerDevice,user,TOKEN_INVALID\naccount.reportPeer,user,PEER_ID_INVALID\naccount.resendPasswordEmail,user,\naccount.resetAuthorization,user,HASH_INVALID\naccount.resetNotifySettings,user,\naccount.resetWallPapers,user,\naccount.resetWebAuthorization,user,\naccount.resetWebAuthorizations,user,\naccount.saveAutoDownloadSettings,user,\naccount.saveSecureValue,user,PASSWORD_REQUIRED\naccount.saveTheme,user,\naccount.saveWallPaper,user,WALLPAPER_INVALID\naccount.sendChangePhoneCode,user,FRESH_CHANGE_PHONE_FORBIDDEN PHONE_NUMBER_INVALID\naccount.sendConfirmPhoneCode,user,HASH_INVALID\naccount.sendVerifyEmailCode,user,EMAIL_INVALID\naccount.sendVerifyPhoneCode,user,\naccount.setAccountTTL,user,TTL_DAYS_INVALID\naccount.setContactSignUpNotification,user,\naccount.setContentSettings,user,SENSITIVE_CHANGE_FORBIDDEN\naccount.setGlobalPrivacySettings,user,AUTOARCHIVE_NOT_AVAILABLE\naccount.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG\naccount.unregisterDevice,user,TOKEN_INVALID\naccount.updateDeviceLocked,user,\naccount.updateNotifySettings,user,PEER_ID_INVALID\naccount.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID\naccount.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID\naccount.updateStatus,user,SESSION_PASSWORD_NEEDED\naccount.updateTheme,user,THEME_INVALID\naccount.updateUsername,user,USERNAME_INVALID USERNAME_NOT_MODIFIED USERNAME_OCCUPIED\naccount.uploadTheme,user,\naccount.uploadWallPaper,user,WALLPAPER_FILE_INVALID WALLPAPER_MIME_INVALID\naccount.verifyEmail,user,EMAIL_INVALID\naccount.verifyPhone,user,\nauth.acceptLoginToken,user,\nauth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY TIMEOUT\nauth.cancelCode,user,PHONE_NUMBER_INVALID\nauth.checkPassword,user,PASSWORD_HASH_INVALID\nauth.checkRecoveryPassword,user,PASSWORD_RECOVERY_EXPIRED\nauth.dropTempAuthKeys,both,\nauth.exportAuthorization,both,DC_ID_INVALID\nauth.exportLoginToken,user,\nauth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID\nauth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID\nauth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID\nauth.logOut,both,\nauth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID\nauth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA\nauth.resendCode,user,PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE\nauth.resetAuthorizations,user,TIMEOUT\nauth.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\nauth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED\nauth.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\nbots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID\nbots.sendCustomRequest,bot,USER_BOT_INVALID\nbots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID\nchannels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID\nchannels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW\nchannels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED\nchannels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE\nchannels.deleteHistory,user,CHANNEL_TOO_BIG\nchannels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN\nchannels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED\nchannels.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\nchannels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID\nchannels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X SRP_ID_INVALID\nchannels.editLocation,user,\nchannels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID\nchannels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED\nchannels.exportMessageLink,user,CHANNEL_INVALID\nchannels.getAdminLog,user,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED\nchannels.getAdminedPublicChannels,user,\nchannels.getChannels,both,CHANNEL_INVALID CHANNEL_PRIVATE NEED_CHAT_INVALID\nchannels.getFullChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA TIMEOUT\nchannels.getGroupsForDiscussion,user,\nchannels.getInactiveChannels,user,\nchannels.getLeftChannels,user,\nchannels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY\nchannels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT\nchannels.getParticipants,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID TIMEOUT\nchannels.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\nchannels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE INVITE_REQUEST_SENT\nchannels.leaveChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT\nchannels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE\nchannels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE\nchannels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED\nchannels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN\nchannels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS\nchannels.toggleForum,user,CHAT_DISCUSSION_UNALLOWED\nchannels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS\nchannels.toggleSignatures,user,CHANNEL_INVALID\nchannels.toggleSlowMode,user,SECONDS_INVALID\nchannels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED USERNAME_PURCHASE_AVAILABLE\nchannels.viewSponsoredMessage,user,UNKNOWN_ERROR\ncontacts.acceptContact,user,\ncontacts.addContact,user,CONTACT_NAME_EMPTY\ncontacts.block,user,CONTACT_ID_INVALID\ncontacts.deleteByPhones,user,\ncontacts.deleteContacts,user,NEED_MEMBER_INVALID TIMEOUT\ncontacts.getBlocked,user,\ncontacts.getContactIDs,user,\ncontacts.getContacts,user,\ncontacts.getLocated,user,USERPIC_UPLOAD_REQUIRED\ncontacts.getSaved,user,TAKEOUT_REQUIRED\ncontacts.getStatuses,user,\ncontacts.getTopPeers,user,TYPES_EMPTY\ncontacts.importContacts,user,\ncontacts.resetSaved,user,\ncontacts.resetTopPeerRating,user,PEER_ID_INVALID\ncontacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNAME_INVALID USERNAME_NOT_OCCUPIED\ncontacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY TIMEOUT\ncontacts.toggleTopPeers,user,\ncontacts.unblock,user,CONTACT_ID_INVALID\nfolders.deleteFolder,user,FOLDER_ID_EMPTY\nfolders.editPeerFolders,user,FOLDER_ID_INVALID\ngetFutureSalts,both,\nhelp.acceptTermsOfService,user,\nhelp.editUserInfo,user,ENTITY_BOUNDS_INVALID USER_INVALID\nhelp.getAppChangelog,user,\nhelp.getAppConfig,user,\nhelp.getAppUpdate,user,\nhelp.getCdnConfig,both,AUTH_KEY_PERM_EMPTY TIMEOUT\nhelp.getConfig,both,AUTH_KEY_DUPLICATED TIMEOUT\nhelp.getDeepLinkInfo,user,\nhelp.getInviteText,user,\nhelp.getNearestDc,user,\nhelp.getPassportConfig,user,\nhelp.getProxyData,user,\nhelp.getRecentMeUrls,user,\nhelp.getSupport,user,\nhelp.getSupportName,user,USER_INVALID\nhelp.getTermsOfServiceUpdate,user,\nhelp.getUserInfo,user,USER_INVALID\nhelp.saveAppLog,user,\nhelp.setBotUpdatesStatus,both,\ninitConnection,both,CONNECTION_LAYER_INVALID INPUT_FETCH_FAIL\ninvokeAfterMsg,both,\ninvokeAfterMsgs,both,\ninvokeWithLayer,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\ninvokeWithMessagesRange,both,\ninvokeWithTakeout,both,\ninvokeWithoutUpdates,both,\nlangpack.getDifference,user,LANG_PACK_INVALID\nlangpack.getLangPack,user,LANG_PACK_INVALID\nlangpack.getLanguage,user,\nlangpack.getLanguages,user,LANG_PACK_INVALID\nlangpack.getStrings,user,LANG_PACK_INVALID\nmessages.acceptEncryption,user,CHAT_ID_INVALID ENCRYPTION_ALREADY_ACCEPTED ENCRYPTION_ALREADY_DECLINED ENCRYPTION_OCCUPY_FAILED\nmessages.acceptUrlAuth,user,\nmessages.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\nmessages.checkChatInvite,user,INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID\nmessages.clearAllDrafts,user,\nmessages.clearRecentStickers,user,\nmessages.createChat,user,USERS_TOO_FEW USER_RESTRICTED\nmessages.deleteChatUser,both,CHAT_ID_INVALID PEER_ID_INVALID USER_NOT_PARTICIPANT\nmessages.deleteHistory,user,PEER_ID_INVALID\nmessages.deleteMessages,both,MESSAGE_DELETE_FORBIDDEN\nmessages.deleteScheduledMessages,user,\nmessages.discardEncryption,user,CHAT_ID_EMPTY ENCRYPTION_ALREADY_DECLINED ENCRYPTION_ID_INVALID\nmessages.editChatAbout,both,\nmessages.editChatAdmin,user,CHAT_ID_INVALID\nmessages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID\nmessages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID\nmessages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID\nmessages.editInlineBotMessage,both,ENTITY_BOUNDS_INVALID MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED\nmessages.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\nmessages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID\nmessages.faveSticker,user,STICKER_ID_INVALID\nmessages.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\nmessages.getAllChats,user,\nmessages.getAllDrafts,user,\nmessages.getAllStickers,user,\nmessages.getArchivedStickers,user,\nmessages.getAttachedStickers,user,\nmessages.getBotCallbackAnswer,user,BOT_RESPONSE_TIMEOUT CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID TIMEOUT\nmessages.getChats,both,CHAT_ID_INVALID PEER_ID_INVALID\nmessages.getCommonChats,user,USER_ID_INVALID\nmessages.getDhConfig,user,RANDOM_LENGTH_INVALID\nmessages.getDialogFilters,user,\nmessages.getDialogUnreadMarks,user,\nmessages.getDialogs,user,INPUT_CONSTRUCTOR_INVALID OFFSET_PEER_ID_INVALID SESSION_PASSWORD_NEEDED TIMEOUT\nmessages.getDocumentByHash,both,SHA256_HASH_INVALID\nmessages.getEmojiKeywords,user,\nmessages.getEmojiKeywordsDifference,user,\nmessages.getEmojiKeywordsLanguages,user,\nmessages.getEmojiURL,user,\nmessages.getFavedStickers,user,\nmessages.getFeaturedStickers,user,\nmessages.getFullChat,both,CHAT_ID_INVALID PEER_ID_INVALID\nmessages.getGameHighScores,bot,PEER_ID_INVALID USER_BOT_REQUIRED\nmessages.getHistory,user,AUTH_KEY_DUPLICATED AUTH_KEY_PERM_EMPTY CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID TIMEOUT\nmessages.getInlineBotResults,user,BOT_INLINE_DISABLED BOT_INVALID CHANNEL_PRIVATE TIMEOUT\nmessages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED\nmessages.getMaskStickers,user,\nmessages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID\nmessages.getMessages,both,\nmessages.getMessagesReadParticipants,user,CHAT_TOO_BIG MESSAGE_ID_INVALID\nmessages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID\nmessages.getOnlines,user,\nmessages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID\nmessages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID\nmessages.getPinnedDialogs,user,\nmessages.getPollResults,user,\nmessages.getPollVotes,user,BROADCAST_FORBIDDEN POLL_VOTE_REQUIRED\nmessages.getRecentLocations,user,\nmessages.getRecentStickers,user,\nmessages.getSavedGifs,user,\nmessages.getScheduledHistory,user,\nmessages.getScheduledMessages,user,\nmessages.getSearchCounters,user,\nmessages.getSplitRanges,user,\nmessages.getStatsURL,user,\nmessages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID\nmessages.getStickers,user,EMOTICON_EMPTY\nmessages.getSuggestedDialogFilters,user,\nmessages.getUnreadMentions,user,PEER_ID_INVALID\nmessages.getWebPage,user,WC_CONVERT_URL_INVALID\nmessages.getWebPagePreview,user,ENTITY_BOUNDS_INVALID\nmessages.hideAllChatJoinRequests,user,HIDE_REQUESTER_MISSING\nmessages.hidePeerSettingsBar,user,\nmessages.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\nmessages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT\nmessages.installStickerSet,user,STICKERSET_INVALID\nmessages.markDialogUnread,user,\nmessages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID\nmessages.readEncryptedHistory,user,MSG_WAIT_FAILED\nmessages.readFeaturedStickers,user,\nmessages.readHistory,user,PEER_ID_INVALID TIMEOUT\nmessages.readMentions,user,\nmessages.readMessageContents,user,\nmessages.receivedMessages,user,\nmessages.receivedQueue,user,MAX_QTS_INVALID MSG_WAIT_FAILED\nmessages.reorderPinnedDialogs,user,PEER_ID_INVALID\nmessages.reorderStickerSets,user,\nmessages.report,user,\nmessages.reportEncryptedSpam,user,CHAT_ID_INVALID\nmessages.reportSpam,user,PEER_ID_INVALID\nmessages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID\nmessages.requestUrlAuth,user,\nmessages.saveDraft,user,ENTITY_BOUNDS_INVALID PEER_ID_INVALID\nmessages.saveGif,user,GIF_ID_INVALID\nmessages.saveRecentSticker,user,STICKER_ID_INVALID\nmessages.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\nmessages.searchGifs,user,METHOD_INVALID SEARCH_QUERY_EMPTY\nmessages.searchGlobal,user,SEARCH_QUERY_EMPTY\nmessages.searchStickerSets,user,\nmessages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED\nmessages.sendEncryptedFile,user,MSG_WAIT_FAILED\nmessages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED\nmessages.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\nmessages.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\nmessages.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\nmessages.sendMultiMedia,both,ENTITY_BOUNDS_INVALID MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED\nmessages.sendScheduledMessages,user,\nmessages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID\nmessages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID\nmessages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY\nmessages.setBotShippingResults,both,QUERY_ID_INVALID\nmessages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED PEER_ID_INVALID\nmessages.setEncryptedTyping,user,CHAT_ID_INVALID\nmessages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED\nmessages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID\nmessages.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\nmessages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED\nmessages.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\nmessages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID\nmessages.startHistoryImport,user,IMPORT_ID_INVALID\nmessages.toggleDialogPin,user,PEER_HISTORY_EMPTY PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH\nmessages.toggleStickerSets,user,\nmessages.uninstallStickerSet,user,STICKERSET_INVALID\nmessages.updateDialogFilter,user,\nmessages.updateDialogFiltersOrder,user,\nmessages.updatePinnedMessage,both,BOT_ONESIDE_NOT_AVAIL\nmessages.uploadEncryptedFile,user,\nmessages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID POSTPONED_TIMEOUT\npayments.clearSavedInfo,user,\npayments.getBankCardData,user,BANK_CARD_NUMBER_INVALID\npayments.getPaymentForm,user,MESSAGE_ID_INVALID\npayments.getPaymentReceipt,user,MESSAGE_ID_INVALID\npayments.getSavedInfo,user,\npayments.sendPaymentForm,user,MESSAGE_ID_INVALID\npayments.validateRequestedInfo,user,MESSAGE_ID_INVALID\nphone.acceptCall,user,CALL_ALREADY_ACCEPTED CALL_ALREADY_DECLINED CALL_OCCUPY_FAILED CALL_PEER_INVALID CALL_PROTOCOL_FLAGS_INVALID\nphone.confirmCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID\nphone.createGroupCall,user,SCHEDULE_DATE_INVALID\nphone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID\nphone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED\nphone.editGroupCallParticipant,user,USER_VOLUME_INVALID\nphone.getCallConfig,user,\nphone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN INVITE_FORBIDDEN_WITH_JOINAS USER_ALREADY_INVITED\nphone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH\nphone.joinGroupCallPresentation,user,PARTICIPANT_JOIN_MISSING\nphone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID\nphone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED\nphone.saveCallDebug,user,CALL_PEER_INVALID DATA_JSON_INVALID\nphone.setCallRating,user,CALL_PEER_INVALID\nphone.toggleGroupCallSettings,user,GROUPCALL_NOT_MODIFIED\nphotos.deletePhotos,user,\nphotos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID\nphotos.updateProfilePhoto,user,PHOTO_ID_INVALID\nphotos.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\nping,both,\nreqDHParams,both,\nreqPq,both,\nreqPqMulti,both,\nrpcDropAnswer,both,\nsetClientDHParams,both,\nstats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X\nstats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X\nstats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD\nstickers.addStickerToSet,both,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS\nstickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID\nstickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED\nstickers.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\nstickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID\nstickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS\nstickers.suggestShortName,user,TITLE_INVALID\nupdates.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\nupdates.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\nupdates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED TIMEOUT\nupload.getCdnFile,user,UNKNOWN_METHOD\nupload.getCdnFileHashes,both,CDN_METHOD_INVALID RSA_DECRYPT_FAILED\nupload.getFile,both,AUTH_KEY_PERM_EMPTY FILE_ID_INVALID INPUT_FETCH_FAIL LIMIT_INVALID LOCATION_INVALID OFFSET_INVALID TIMEOUT\nupload.getFileHashes,both,\nupload.getWebFile,user,LOCATION_INVALID\nupload.reuploadCdnFile,both,RSA_DECRYPT_FAILED\nupload.saveBigFilePart,both,FILE_PARTS_INVALID FILE_PART_EMPTY FILE_PART_INVALID FILE_PART_SIZE_CHANGED FILE_PART_SIZE_INVALID TIMEOUT\nupload.saveFilePart,both,FILE_PART_EMPTY FILE_PART_INVALID INPUT_FETCH_FAIL SESSION_PASSWORD_NEEDED\nusers.getFullUser,both,TIMEOUT USER_ID_INVALID\nusers.getUsers,both,AUTH_KEY_PERM_EMPTY MEMBER_NO_LOCATION NEED_MEMBER_INVALID SESSION_PASSWORD_NEEDED TIMEOUT\nusers.setSecureValueErrors,bot,\n"
  },
  {
    "path": "telethon_generator/data/mtproto.tl",
    "content": "// Core types (no need to gen)\n\n//vector#1cb5c415 {t:Type} # [ t ] = Vector t;\n\n///////////////////////////////\n/// Authorization key creation\n///////////////////////////////\n\nresPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector<long> = ResPQ;\n\np_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data;\np_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;\np_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;\np_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;\n\nbind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner;\n\nserver_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params;\nserver_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params;\n\nserver_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data;\n\nclient_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data;\n\ndh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer;\ndh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer;\ndh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer;\n\ndestroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes;\ndestroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes;\ndestroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes;\n\n---functions---\n\nreq_pq#60469778 nonce:int128 = ResPQ;\nreq_pq_multi#be7e8ef1 nonce:int128 = ResPQ;\n\nreq_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params;\n\nset_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer;\n\ndestroy_auth_key#d1435160 = DestroyAuthKeyRes;\n\n///////////////////////////////\n////////////// System messages\n///////////////////////////////\n\n---types---\n\nmsgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck;\n\nbad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification;\nbad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification;\n\nmsgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq;\nmsgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo;\nmsgs_all_info#8cc0d131 msg_ids:Vector<long> info:string = MsgsAllInfo;\n\nmsg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo;\nmsg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo;\n\nmsg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq;\n\n//rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually\n\nrpc_error#2144ca19 error_code:int error_message:string = RpcError;\n\nrpc_answer_unknown#5e2ad36e = RpcDropAnswer;\nrpc_answer_dropped_running#cd78e586 = RpcDropAnswer;\nrpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer;\n\nfuture_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt;\nfuture_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts;\n\npong#347773c5 msg_id:long ping_id:long = Pong;\n\ndestroy_session_ok#e22045fc session_id:long = DestroySessionRes;\ndestroy_session_none#62d350c9 session_id:long = DestroySessionRes;\n\nnew_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession;\n\n//message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually\n//msg_container#73f1f8dc messages:vector<message> = MessageContainer; // parsed manually\n//msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container\n//gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually\n\nhttp_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait;\n\n//ipPort ipv4:int port:int = IpPort;\n//help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector<ipPort> = help.ConfigSimple;\n\nipPort#d433ad73 ipv4:int port:int = IpPort;\nipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort;\naccessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector<IpPort> = AccessPointRule;\nhelp.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple;\n\ntlsClientHello blocks:vector<TlsBlock> = TlsClientHello;\n\ntlsBlockString data:string = TlsBlock;\ntlsBlockRandom length:int = TlsBlock;\ntlsBlockZero length:int = TlsBlock;\ntlsBlockDomain = TlsBlock;\ntlsBlockGrease seed:int = TlsBlock;\ntlsBlockPublicKey = TlsBlock;\ntlsBlockScope entries:Vector<TlsBlock> = TlsBlock;\n\n---functions---\n\nrpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer;\n\nget_future_salts#b921bd04 num:int = FutureSalts;\n\nping#7abe77ec ping_id:long = Pong;\nping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong;\n\ndestroy_session#e7512126 session_id:long = DestroySessionRes;\n"
  },
  {
    "path": "telethon_generator/docswriter.py",
    "content": "import os\nimport re\n\n\nclass DocsWriter:\n    \"\"\"\n    Utility class used to write the HTML files used on the documentation.\n    \"\"\"\n    def __init__(self, filename, type_to_path):\n        \"\"\"\n        Initializes the writer to the specified output file,\n        creating the parent directories when used if required.\n        \"\"\"\n        self.filename = filename\n        self._parent = str(self.filename.parent)\n        self.handle = None\n        self.title = ''\n\n        # Should be set before calling adding items to the menu\n        self.menu_separator_tag = None\n\n        # Utility functions\n        self.type_to_path = lambda t: self._rel(type_to_path(t))\n\n        # Control signals\n        self.menu_began = False\n        self.table_columns = 0\n        self.table_columns_left = None\n        self.write_copy_script = False\n        self._script = ''\n\n    def _rel(self, path):\n        \"\"\"\n        Get the relative path for the given path from the current\n        file by working around https://bugs.python.org/issue20012.\n        \"\"\"\n        return os.path.relpath(\n            str(path), self._parent).replace(os.path.sep, '/')\n\n    # High level writing\n    def write_head(self, title, css_path, default_css):\n        \"\"\"Writes the head part for the generated document,\n           with the given title and CSS\n        \"\"\"\n        self.title = title\n        self.write(\n            '''<!DOCTYPE html>\n<html>\n<head>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n    <title>{title}</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <link id=\"style\" href=\"{rel_css}/docs.dark.css\" rel=\"stylesheet\">\n    <script>\n    document.getElementById(\"style\").href = \"{rel_css}/docs.\"\n        + (localStorage.getItem(\"theme\") || \"{def_css}\")\n        + \".css\";\n    </script>\n    <link href=\"https://fonts.googleapis.com/css?family=Nunito|Source+Code+Pro\"\n          rel=\"stylesheet\">\n</head>\n<body>\n<div id=\"main_div\">''',\n            title=title,\n            rel_css=self._rel(css_path),\n            def_css=default_css\n        )\n\n    def set_menu_separator(self, img):\n        \"\"\"Sets the menu separator.\n           Must be called before adding entries to the menu\n        \"\"\"\n        if img:\n            self.menu_separator_tag = '<img src=\"{}\" alt=\"/\" />'.format(\n                self._rel(img))\n        else:\n            self.menu_separator_tag = None\n\n    def add_menu(self, name, link=None):\n        \"\"\"Adds a menu entry, will create it if it doesn't exist yet\"\"\"\n        if self.menu_began:\n            if self.menu_separator_tag:\n                self.write(self.menu_separator_tag)\n        else:\n            # First time, create the menu tag\n            self.write('<ul class=\"horizontal\">')\n            self.menu_began = True\n\n        self.write('<li>')\n        if link:\n            self.write('<a href=\"{}\">', self._rel(link))\n\n        # Write the real menu entry text\n        self.write(name)\n\n        if link:\n            self.write('</a>')\n        self.write('</li>')\n\n    def end_menu(self):\n        \"\"\"Ends an opened menu\"\"\"\n        if not self.menu_began:\n            raise RuntimeError('No menu had been started in the first place.')\n        self.write('</ul>')\n\n    def write_title(self, title, level=1, id=None):\n        \"\"\"Writes a title header in the document body,\n           with an optional depth level\n        \"\"\"\n        if id:\n            self.write('<h{lv} id=\"{id}\">{title}</h{lv}>',\n                       title=title, lv=level, id=id)\n        else:\n            self.write('<h{lv}>{title}</h{lv}>',\n                       title=title, lv=level)\n\n    def write_code(self, tlobject):\n        \"\"\"Writes the code for the given 'tlobject' properly\n           formatted with hyperlinks\n        \"\"\"\n        self.write('<pre>---{}---\\n',\n                   'functions' if tlobject.is_function else 'types')\n\n        # Write the function or type and its ID\n        if tlobject.namespace:\n            self.write(tlobject.namespace)\n            self.write('.')\n\n        self.write('{}#{:08x}', tlobject.name, tlobject.id)\n\n        # Write all the arguments (or do nothing if there's none)\n        for arg in tlobject.args:\n            self.write(' ')\n            add_link = not arg.generic_definition and not arg.is_generic\n\n            # \"Opening\" modifiers\n            if arg.generic_definition:\n                self.write('{')\n\n            # Argument name\n            self.write(arg.name)\n            self.write(':')\n\n            # \"Opening\" modifiers\n            if arg.flag:\n                self.write('{}.{}?', arg.flag, arg.flag_index)\n\n            if arg.is_generic:\n                self.write('!')\n\n            if arg.is_vector:\n                self.write('<a href=\"{}\">Vector</a>&lt;',\n                           self.type_to_path('vector'))\n\n            # Argument type\n            if arg.type:\n                if add_link:\n                    self.write('<a href=\"{}\">', self.type_to_path(arg.type))\n                self.write(arg.type)\n                if add_link:\n                    self.write('</a>')\n            else:\n                self.write('#')\n\n            # \"Closing\" modifiers\n            if arg.is_vector:\n                self.write('&gt;')\n\n            if arg.generic_definition:\n                self.write('}')\n\n        # Now write the resulting type (result from a function/type)\n        self.write(' = ')\n        generic_name = next((arg.name for arg in tlobject.args\n                             if arg.generic_definition), None)\n\n        if tlobject.result == generic_name:\n            # Generic results cannot have any link\n            self.write(tlobject.result)\n        else:\n            if re.search('^vector<', tlobject.result, re.IGNORECASE):\n                # Notice that we don't simply make up the \"Vector\" part,\n                # because some requests (as of now, only FutureSalts),\n                # use a lower type name for it (see #81)\n                vector, inner = tlobject.result.split('<')\n                inner = inner.strip('>')\n                self.write('<a href=\"{}\">{}</a>&lt;',\n                           self.type_to_path(vector), vector)\n\n                self.write('<a href=\"{}\">{}</a>&gt;',\n                           self.type_to_path(inner), inner)\n            else:\n                self.write('<a href=\"{}\">{}</a>',\n                           self.type_to_path(tlobject.result), tlobject.result)\n\n        self.write('</pre>')\n\n    def begin_table(self, column_count):\n        \"\"\"Begins a table with the given 'column_count', required to automatically\n           create the right amount of columns when adding items to the rows\"\"\"\n        self.table_columns = column_count\n        self.table_columns_left = 0\n        self.write('<table>')\n\n    def add_row(self, text, link=None, bold=False, align=None):\n        \"\"\"This will create a new row, or add text to the next column\n           of the previously created, incomplete row, closing it if complete\"\"\"\n        if not self.table_columns_left:\n            # Starting a new row\n            self.write('<tr>')\n            self.table_columns_left = self.table_columns\n\n        self.write('<td')\n        if align:\n            self.write(' style=\"text-align:{}\"', align)\n        self.write('>')\n\n        if bold:\n            self.write('<b>')\n        if link:\n            self.write('<a href=\"{}\">', self._rel(link))\n\n        # Finally write the real table data, the given text\n        self.write(text)\n\n        if link:\n            self.write('</a>')\n        if bold:\n            self.write('</b>')\n\n        self.write('</td>')\n\n        self.table_columns_left -= 1\n        if not self.table_columns_left:\n            self.write('</tr>')\n\n    def end_table(self):\n        # If there was any column left, finish it before closing the table\n        if self.table_columns_left:\n            self.write('</tr>')\n\n        self.write('</table>')\n\n    def write_text(self, text):\n        \"\"\"Writes a paragraph of text\"\"\"\n        self.write('<p>{}</p>', text)\n\n    def write_copy_button(self, text, text_to_copy):\n        \"\"\"Writes a button with 'text' which can be used\n           to copy 'text_to_copy' to clipboard when it's clicked.\"\"\"\n        self.write_copy_script = True\n        self.write('<button onclick=\"cp(\\'{}\\');\">{}</button>'\n                   .format(text_to_copy, text))\n\n    def add_script(self, src='', path=None):\n        if path:\n            self._script += '<script src=\"{}\"></script>'.format(\n                self._rel(path))\n        elif src:\n            self._script += '<script>{}</script>'.format(src)\n\n    def end_body(self):\n        \"\"\"Ends the whole document. This should be called the last\"\"\"\n        if self.write_copy_script:\n            self.write(\n                '<textarea id=\"c\" class=\"invisible\"></textarea>'\n                '<script>'\n                'function cp(t){'\n                'var c=document.getElementById(\"c\");'\n                'c.value=t;'\n                'c.select();'\n                'try{document.execCommand(\"copy\")}'\n                'catch(e){}}'\n                '</script>'\n            )\n\n        self.write('</div>{}</body></html>', self._script)\n\n    # \"Low\" level writing\n    def write(self, s, *args, **kwargs):\n        \"\"\"Wrapper around handle.write\"\"\"\n        if args or kwargs:\n            self.handle.write(s.format(*args, **kwargs))\n        else:\n            self.handle.write(s)\n\n    # With block\n    def __enter__(self):\n        # Sanity check\n        self.filename.parent.mkdir(parents=True, exist_ok=True)\n        self.handle = self.filename.open('w', encoding='utf-8')\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.handle.close()\n"
  },
  {
    "path": "telethon_generator/generators/__init__.py",
    "content": "from .errors import generate_errors\nfrom .tlobject import generate_tlobjects, clean_tlobjects\nfrom .docs import generate_docs\n"
  },
  {
    "path": "telethon_generator/generators/docs.py",
    "content": "#!/usr/bin/env python3\nimport functools\nimport os\nimport pathlib\nimport re\nimport shutil\nfrom collections import defaultdict\nfrom pathlib import Path\n\nfrom ..docswriter import DocsWriter\nfrom ..parsers import TLObject, Usability\nfrom ..utils import snake_to_camel_case\n\nCORE_TYPES = {\n    'int', 'long', 'int128', 'int256', 'double',\n    'vector', 'string', 'bool', 'true', 'bytes', 'date'\n}\n\n\ndef _get_file_name(tlobject):\n    \"\"\"``ClassName -> class_name.html``.\"\"\"\n    name = tlobject.name if isinstance(tlobject, TLObject) else tlobject\n    # Courtesy of http://stackoverflow.com/a/1176023/4759433\n    s1 = re.sub('(.)([A-Z][a-z]+)', r'\\1_\\2', name)\n    result = re.sub('([a-z0-9])([A-Z])', r'\\1_\\2', s1).lower()\n    return '{}.html'.format(result)\n\n\ndef get_import_code(tlobject):\n    \"\"\"``TLObject -> from ... import ...``.\"\"\"\n    kind = 'functions' if tlobject.is_function else 'types'\n    ns = '.' + tlobject.namespace if tlobject.namespace else ''\n    return 'from telethon.tl.{}{} import {}'\\\n        .format(kind, ns, tlobject.class_name)\n\n\ndef _get_path_for(tlobject):\n    \"\"\"Returns the path for the given TLObject.\"\"\"\n    out_dir = pathlib.Path('methods' if tlobject.is_function else 'constructors')\n    if tlobject.namespace:\n        out_dir /= tlobject.namespace\n\n    return out_dir / _get_file_name(tlobject)\n\n\ndef _get_path_for_type(type_):\n    \"\"\"Similar to `_get_path_for` but for only type names.\"\"\"\n    if type_.lower() in CORE_TYPES:\n        return Path('index.html#%s' % type_.lower())\n    elif '.' in type_:\n        namespace, name = type_.split('.')\n        return Path('types', namespace, _get_file_name(name))\n    else:\n        return Path('types',  _get_file_name(type_))\n\n\ndef _find_title(html_file):\n    \"\"\"Finds the <title> for the given HTML file, or (Unknown).\"\"\"\n    # TODO Is it necessary to read files like this?\n    with html_file.open() as f:\n        for line in f:\n            if '<title>' in line:\n                # + 7 to skip len('<title>')\n                return line[line.index('<title>') + 7:line.index('</title>')]\n\n    return '(Unknown)'\n\n\ndef _build_menu(docs):\n    \"\"\"\n    Builds the menu used for the current ``DocumentWriter``.\n    \"\"\"\n\n    paths = []\n    current = docs.filename\n    top = pathlib.Path('.')\n    while current != top:\n        current = current.parent\n        paths.append(current)\n\n    for path in reversed(paths):\n        docs.add_menu(path.stem.title() or 'API', link=path / 'index.html')\n\n    if docs.filename.stem != 'index':\n        docs.add_menu(docs.title, link=docs.filename)\n\n    docs.end_menu()\n\n\ndef _generate_index(folder, paths,\n                    bots_index=False, bots_index_paths=()):\n    \"\"\"Generates the index file for the specified folder\"\"\"\n    # Determine the namespaces listed here (as sub folders)\n    # and the files (.html files) that we should link to\n    namespaces = []\n    files = []\n    INDEX = 'index.html'\n    BOT_INDEX = 'botindex.html'\n\n    for item in (bots_index_paths or folder.iterdir()):\n        if item.is_dir():\n            namespaces.append(item)\n        elif item.name not in (INDEX, BOT_INDEX):\n            files.append(item)\n\n    # Now that everything is setup, write the index.html file\n    filename = folder / (BOT_INDEX if bots_index else INDEX)\n    with DocsWriter(filename, _get_path_for_type) as docs:\n        # Title should be the current folder name\n        docs.write_head(str(folder).replace(os.path.sep, '/').title(),\n                        css_path=paths['css'],\n                        default_css=paths['default_css'])\n\n        docs.set_menu_separator(paths['arrow'])\n        _build_menu(docs)\n        docs.write_title(str(filename.parent)\n                         .replace(os.path.sep, '/').title())\n\n        if bots_index:\n            docs.write_text('These are the requests that you may be able to '\n                            'use as a bot. Click <a href=\"{}\">here</a> to '\n                            'view them all.'.format(INDEX))\n        else:\n            docs.write_text('Click <a href=\"{}\">here</a> to view the requests '\n                            'that you can use as a bot.'.format(BOT_INDEX))\n        if namespaces:\n            docs.write_title('Namespaces', level=3)\n            docs.begin_table(4)\n            namespaces.sort()\n            for namespace in namespaces:\n                # For every namespace, also write the index of it\n                namespace_paths = []\n                if bots_index:\n                    for item in bots_index_paths:\n                        if item.parent == namespace:\n                            namespace_paths.append(item)\n\n                _generate_index(namespace, paths,\n                                bots_index, namespace_paths)\n\n                docs.add_row(\n                    namespace.stem.title(),\n                    link=namespace / (BOT_INDEX if bots_index else INDEX))\n\n            docs.end_table()\n\n        docs.write_title('Available items')\n        docs.begin_table(2)\n\n        files = [(f, _find_title(f)) for f in files]\n        files.sort(key=lambda t: t[1])\n\n        for file, title in files:\n            docs.add_row(title, link=file)\n\n        docs.end_table()\n        docs.end_body()\n\n\ndef _get_description(arg):\n    \"\"\"Generates a proper description for the given argument.\"\"\"\n    desc = []\n    otherwise = False\n    if arg.can_be_inferred:\n        desc.append('If left unspecified, it will be inferred automatically.')\n        otherwise = True\n    elif arg.flag:\n        desc.append('This argument defaults to '\n                    '<code>None</code> and can be omitted.')\n        otherwise = True\n\n    if arg.type in {'InputPeer', 'InputUser', 'InputChannel',\n                    'InputNotifyPeer', 'InputDialogPeer'}:\n        desc.append(\n            'Anything entity-like will work if the library can find its '\n            '<code>Input</code> version (e.g., usernames, <code>Peer</code>, '\n            '<code>User</code> or <code>Channel</code> objects, etc.).'\n        )\n\n    if arg.is_vector:\n        if arg.is_generic:\n            desc.append('A list of other Requests must be supplied.')\n        else:\n            desc.append('A list must be supplied.')\n    elif arg.is_generic:\n        desc.append('A different Request must be supplied for this argument.')\n    else:\n        otherwise = False  # Always reset to false if no other text is added\n\n    if otherwise:\n        desc.insert(1, 'Otherwise,')\n        desc[-1] = desc[-1][:1].lower() + desc[-1][1:]\n\n    return ' '.join(desc).replace(\n        'list',\n        '<span class=\"tooltip\" title=\"Any iterable that supports len() '\n        'will work too\">list</span>'\n    )\n\n\ndef _copy_replace(src, dst, replacements):\n    \"\"\"Copies the src file into dst applying the replacements dict\"\"\"\n    with src.open() as infile, dst.open('w') as outfile:\n        outfile.write(re.sub(\n            '|'.join(re.escape(k) for k in replacements),\n            lambda m: str(replacements[m.group(0)]),\n            infile.read()\n        ))\n\n\ndef _write_html_pages(tlobjects, methods, layer, input_res):\n    \"\"\"\n    Generates the documentation HTML files from from ``scheme.tl``\n    to ``/methods`` and ``/constructors``, etc.\n    \"\"\"\n    # Save 'Type: [Constructors]' for use in both:\n    # * Seeing the return type or constructors belonging to the same type.\n    # * Generating the types documentation, showing available constructors.\n    paths = {k: pathlib.Path(v) for k, v in (\n        ('css', 'css'),\n        ('arrow', 'img/arrow.svg'),\n        ('search.js', 'js/search.js'),\n        ('404', '404.html'),\n        ('index_all', 'index.html'),\n        ('bot_index', 'botindex.html'),\n        ('index_types', 'types/index.html'),\n        ('index_methods', 'methods/index.html'),\n        ('index_constructors', 'constructors/index.html')\n    )}\n    paths['default_css'] = 'light'  # docs.<name>.css, local path\n    type_to_constructors = defaultdict(list)\n    type_to_functions = defaultdict(list)\n    for tlobject in tlobjects:\n        d = type_to_functions if tlobject.is_function else type_to_constructors\n        d[tlobject.innermost_result].append(tlobject)\n\n    for t, cs in type_to_constructors.items():\n        type_to_constructors[t] = list(sorted(cs, key=lambda c: c.name))\n\n    methods = {m.name: m for m in methods}\n    bot_docs_paths = []\n\n    for tlobject in tlobjects:\n        filename = _get_path_for(tlobject)\n        with DocsWriter(filename, _get_path_for_type) as docs:\n            docs.write_head(title=tlobject.class_name,\n                            css_path=paths['css'],\n                            default_css=paths['default_css'])\n\n            # Create the menu (path to the current TLObject)\n            docs.set_menu_separator(paths['arrow'])\n            _build_menu(docs)\n\n            # Create the page title\n            docs.write_title(tlobject.class_name)\n\n            if tlobject.is_function:\n                if tlobject.usability == Usability.USER:\n                    start = '<strong>Only users</strong> can'\n                elif tlobject.usability == Usability.BOT:\n                    bot_docs_paths.append(filename)\n                    start = '<strong>Only bots</strong> can'\n                elif tlobject.usability == Usability.BOTH:\n                    bot_docs_paths.append(filename)\n                    start = '<strong>Both users and bots</strong> can'\n                else:\n                    bot_docs_paths.append(filename)\n                    start = \\\n                        'Both users and bots <strong>may</strong> be able to'\n\n                docs.write_text('{} use this request. <a href=\"#examples\">'\n                                'See code examples.</a>'.format(start))\n\n            # Write the code definition for this TLObject\n            docs.write_code(tlobject)\n            docs.write_copy_button('Copy import to the clipboard',\n                                   get_import_code(tlobject))\n\n            # Write the return type (or constructors belonging to the same type)\n            docs.write_title('Returns' if tlobject.is_function\n                             else 'Belongs to', level=3)\n\n            generic_arg = next((arg.name for arg in tlobject.args\n                                if arg.generic_definition), None)\n\n            if tlobject.result == generic_arg:\n                # We assume it's a function returning a generic type\n                generic_arg = next((arg.name for arg in tlobject.args\n                                    if arg.is_generic))\n                docs.write_text('This request returns the result of whatever '\n                                'the result from invoking the request passed '\n                                'through <i>{}</i> is.'.format(generic_arg))\n            else:\n                if re.search('^vector<', tlobject.result, re.IGNORECASE):\n                    docs.write_text('A list of the following type is returned.')\n                    inner = tlobject.innermost_result\n                else:\n                    inner = tlobject.result\n\n                docs.begin_table(column_count=1)\n                docs.add_row(inner, link=_get_path_for_type(inner))\n                docs.end_table()\n\n                cs = type_to_constructors.get(inner, [])\n                if not cs:\n                    docs.write_text('This type has no instances available.')\n                elif len(cs) == 1:\n                    docs.write_text('This type can only be an instance of:')\n                else:\n                    docs.write_text('This type can be an instance of either:')\n\n                docs.begin_table(column_count=2)\n                for constructor in cs:\n                    link = _get_path_for(constructor)\n                    docs.add_row(constructor.class_name, link=link)\n                docs.end_table()\n\n            # Return (or similar types) written. Now parameters/members\n            docs.write_title(\n                'Parameters' if tlobject.is_function else 'Members', level=3\n            )\n\n            # Sort the arguments in the same way they're sorted\n            # on the generated code (flags go last)\n            args = [\n                a for a in tlobject.sorted_args()\n                if not a.flag_indicator and not a.generic_definition\n            ]\n\n            if args:\n                # Writing parameters\n                docs.begin_table(column_count=3)\n\n                for arg in args:\n                    # Name row\n                    docs.add_row(arg.name,\n                                 bold=True)\n\n                    # Type row\n                    friendly_type = 'flag' if arg.type == 'true' else arg.type\n                    if arg.is_generic:\n                        docs.add_row('!' + friendly_type, align='center')\n                    else:\n                        docs.add_row(\n                            friendly_type, align='center',\n                            link=_get_path_for_type(arg.type)\n                         )\n\n                    # Add a description for this argument\n                    docs.add_row(_get_description(arg))\n\n                docs.end_table()\n            else:\n                if tlobject.is_function:\n                    docs.write_text('This request takes no input parameters.')\n                else:\n                    docs.write_text('This type has no members.')\n\n            if tlobject.is_function:\n                docs.write_title('Known RPC errors')\n                method_info = methods.get(tlobject.fullname)\n                errors = method_info and method_info.errors\n                if not errors:\n                    docs.write_text(\"This request can't cause any RPC error \"\n                                    \"as far as we know.\")\n                else:\n                    docs.write_text(\n                        'This request can cause {} known error{}:'.format(\n                            len(errors), '' if len(errors) == 1 else 's'\n                    ))\n                    docs.begin_table(column_count=2)\n                    for error in errors:\n                        docs.add_row('<code>{}</code>'.format(error.name))\n                        docs.add_row('{}.'.format(error.description))\n                    docs.end_table()\n                    docs.write_text('You can import these from '\n                                    '<code>telethon.errors</code>.')\n\n                docs.write_title('Example', id='examples')\n                if tlobject.friendly:\n                    ns, friendly = tlobject.friendly\n                    docs.write_text(\n                        'Please refer to the documentation of <a href=\"'\n                        'https://docs.telethon.dev/en/stable/modules/client.html'\n                        '#telethon.client.{0}.{1}\"><code>client.{1}()</code></a> '\n                        'to learn about the parameters and see several code '\n                        'examples on how to use it.'\n                        .format(ns, friendly)\n                    )\n                    docs.write_text(\n                        'The method above is the recommended way to do it. '\n                        'If you need more control over the parameters or want '\n                        'to learn how it is implemented, open the details by '\n                        'clicking on the \"Details\" text.'\n                    )\n                    docs.write('<details>')\n\n                docs.write('''<pre>\\\n<strong>from</strong> telethon.sync <strong>import</strong> TelegramClient\n<strong>from</strong> telethon <strong>import</strong> functions, types\n\n<strong>with</strong> TelegramClient(name, api_id, api_hash) <strong>as</strong> client:\n    result = client(''')\n                tlobject.as_example(docs, indent=1)\n                docs.write(')\\n')\n                if tlobject.result.startswith('Vector'):\n                    docs.write('''\\\n    <strong>for</strong> x <strong>in</strong> result:\n        print(x''')\n                else:\n                    docs.write('    print(result')\n                    if tlobject.result != 'Bool' \\\n                            and not tlobject.result.startswith('Vector'):\n                        docs.write('.stringify()')\n\n                docs.write(')</pre>')\n                if tlobject.friendly:\n                    docs.write('</details>')\n\n            depth = '../' * (2 if tlobject.namespace else 1)\n            docs.add_script(src='prependPath = \"{}\";'.format(depth))\n            docs.add_script(path=paths['search.js'])\n            docs.end_body()\n\n    # Find all the available types (which are not the same as the constructors)\n    # Each type has a list of constructors associated to it, hence is a map\n    for t, cs in type_to_constructors.items():\n        filename = _get_path_for_type(t)\n        out_dir = filename.parent\n        if out_dir:\n            out_dir.mkdir(parents=True, exist_ok=True)\n\n        # Since we don't have access to the full TLObject, split the type\n        if '.' in t:\n            namespace, name = t.split('.')\n        else:\n            namespace, name = None, t\n\n        with DocsWriter(filename, _get_path_for_type) as docs:\n            docs.write_head(title=snake_to_camel_case(name),\n                            css_path=paths['css'],\n                            default_css=paths['default_css'])\n\n            docs.set_menu_separator(paths['arrow'])\n            _build_menu(docs)\n\n            # Main file title\n            docs.write_title(snake_to_camel_case(name))\n\n            # List available constructors for this type\n            docs.write_title('Available constructors', level=3)\n            if not cs:\n                docs.write_text('This type has no constructors available.')\n            elif len(cs) == 1:\n                docs.write_text('This type has one constructor available.')\n            else:\n                docs.write_text('This type has %d constructors available.' %\n                                len(cs))\n\n            docs.begin_table(2)\n            for constructor in cs:\n                # Constructor full name\n                link = _get_path_for(constructor)\n                docs.add_row(constructor.class_name, link=link)\n            docs.end_table()\n\n            # List all the methods which return this type\n            docs.write_title('Requests returning this type', level=3)\n            functions = type_to_functions.get(t, [])\n            if not functions:\n                docs.write_text('No request returns this type.')\n            elif len(functions) == 1:\n                docs.write_text('Only the following request returns this type.')\n            else:\n                docs.write_text(\n                    'The following %d requests return this type as a result.' %\n                    len(functions)\n                )\n\n            docs.begin_table(2)\n            for func in functions:\n                link = _get_path_for(func)\n                docs.add_row(func.class_name, link=link)\n            docs.end_table()\n\n            # List all the methods which take this type as input\n            docs.write_title('Requests accepting this type as input', level=3)\n            other_methods = sorted(\n                (u for u in tlobjects\n                 if any(a.type == t for a in u.args) and u.is_function),\n                key=lambda u: u.name\n            )\n            if not other_methods:\n                docs.write_text(\n                    'No request accepts this type as an input parameter.')\n            elif len(other_methods) == 1:\n                docs.write_text(\n                    'Only this request has a parameter with this type.')\n            else:\n                docs.write_text(\n                    'The following %d requests accept this type as an input '\n                    'parameter.' % len(other_methods))\n\n            docs.begin_table(2)\n            for ot in other_methods:\n                link = _get_path_for(ot)\n                docs.add_row(ot.class_name, link=link)\n            docs.end_table()\n\n            # List every other type which has this type as a member\n            docs.write_title('Other types containing this type', level=3)\n            other_types = sorted(\n                (u for u in tlobjects\n                 if any(a.type == t for a in u.args) and not u.is_function),\n                key=lambda u: u.name\n            )\n\n            if not other_types:\n                docs.write_text(\n                    'No other types have a member of this type.')\n            elif len(other_types) == 1:\n                docs.write_text(\n                    'You can find this type as a member of this other type.')\n            else:\n                docs.write_text(\n                    'You can find this type as a member of any of '\n                    'the following %d types.' % len(other_types))\n\n            docs.begin_table(2)\n            for ot in other_types:\n                link = _get_path_for(ot)\n                docs.add_row(ot.class_name, link=link)\n            docs.end_table()\n            docs.end_body()\n\n    # After everything's been written, generate an index.html per folder.\n    # This will be done automatically and not taking into account any extra\n    # information that we have available, simply a file listing all the others\n    # accessible by clicking on their title\n    for folder in ['types', 'methods', 'constructors']:\n        _generate_index(pathlib.Path(folder), paths)\n\n    _generate_index(pathlib.Path('methods'), paths, True,\n                    bot_docs_paths)\n\n    # Write the final core index, the main index for the rest of files\n    types = set()\n    methods = []\n    cs = []\n    for tlobject in tlobjects:\n        if tlobject.is_function:\n            methods.append(tlobject)\n        else:\n            cs.append(tlobject)\n\n        if not tlobject.result.lower() in CORE_TYPES:\n            if re.search('^vector<', tlobject.result, re.IGNORECASE):\n                types.add(tlobject.innermost_result)\n            else:\n                types.add(tlobject.result)\n\n    types = sorted(types)\n    methods = sorted(methods, key=lambda m: m.name)\n    cs = sorted(cs, key=lambda c: c.name)\n\n    shutil.copy(str(input_res / '404.html'), str(paths['404']))\n    _copy_replace(input_res / 'core.html', paths['index_all'], {\n        '{type_count}': len(types),\n        '{method_count}': len(methods),\n        '{constructor_count}': len(tlobjects) - len(methods),\n        '{layer}': layer,\n    })\n\n    def fmt(xs):\n        zs = {}  # create a dict to hold those which have duplicated keys\n        for x in xs:\n            zs[x.class_name] = x.class_name in zs\n        return ', '.join(\n            '\"{}.{}\"'.format(x.namespace, x.class_name)\n            if zs[x.class_name] and x.namespace\n            else '\"{}\"'.format(x.class_name) for x in xs\n        )\n\n    request_names = fmt(methods)\n    constructor_names = fmt(cs)\n\n    def fmt(xs, formatter):\n        return ', '.join('\"{}\"'.format(\n            formatter(x)).replace(os.path.sep, '/') for x in xs)\n\n    type_names = fmt(types, formatter=lambda x: x)\n\n    request_urls = fmt(methods, _get_path_for)\n    type_urls = fmt(types, _get_path_for_type)\n    constructor_urls = fmt(cs, _get_path_for)\n\n    paths['search.js'].parent.mkdir(parents=True, exist_ok=True)\n    _copy_replace(input_res / 'js/search.js', paths['search.js'], {\n        '{request_names}': request_names,\n        '{type_names}': type_names,\n        '{constructor_names}': constructor_names,\n        '{request_urls}': request_urls,\n        '{type_urls}': type_urls,\n        '{constructor_urls}': constructor_urls\n    })\n\n\ndef _copy_resources(res_dir):\n    for dirname, files in [('css', ['docs.light.css', 'docs.dark.css']),\n                           ('img', ['arrow.svg'])]:\n        dirpath = pathlib.Path(dirname)\n        dirpath.mkdir(parents=True, exist_ok=True)\n        for file in files:\n            shutil.copy(str(res_dir / dirname / file), str(dirpath))\n\n\ndef _create_structure(tlobjects):\n    \"\"\"\n    Pre-create the required directory structure.\n    \"\"\"\n    types_ns = set()\n    method_ns = set()\n    for obj in tlobjects:\n        if obj.namespace:\n            if obj.is_function:\n                method_ns.add(obj.namespace)\n            else:\n                types_ns.add(obj.namespace)\n\n    output_dir = pathlib.Path('.')\n    type_dir = output_dir / 'types'\n    type_dir.mkdir(exist_ok=True)\n\n    cons_dir = output_dir / 'constructors'\n    cons_dir.mkdir(exist_ok=True)\n    for ns in types_ns:\n        (type_dir / ns).mkdir(exist_ok=True)\n        (cons_dir / ns).mkdir(exist_ok=True)\n\n    meth_dir = output_dir / 'methods'\n    meth_dir.mkdir(exist_ok=True)\n    for ns in types_ns:\n        (meth_dir / ns).mkdir(exist_ok=True)\n\n\ndef generate_docs(tlobjects, methods_info, layer, input_res):\n    _create_structure(tlobjects)\n    _write_html_pages(tlobjects, methods_info, layer, input_res)\n    _copy_resources(input_res)\n"
  },
  {
    "path": "telethon_generator/generators/errors.py",
    "content": "def generate_errors(errors, f):\n    # Exact/regex match to create {CODE: ErrorClassName}\n    exact_match = []\n    regex_match = []\n\n    # Find out what subclasses to import and which to create\n    import_base, create_base = set(), {}\n    for error in errors:\n        if error.subclass_exists:\n            import_base.add(error.subclass)\n        else:\n            create_base[error.subclass] = error.int_code\n\n        if error.has_captures:\n            regex_match.append(error)\n        else:\n            exact_match.append(error)\n\n    # Imports and new subclass creation\n    f.write('from .rpcbaseerrors import RPCError, {}\\n'\n            .format(\", \".join(sorted(import_base))))\n\n    for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]):\n        f.write('\\n\\nclass {}(RPCError):\\n    code = {}\\n'\n                .format(cls, int_code))\n\n    # Error classes generation\n    for error in errors:\n        f.write('\\n\\nclass {}({}):\\n    '.format(error.name, error.subclass))\n\n        if error.has_captures:\n            f.write('def __init__(self, request, capture=0):\\n    '\n                    '    self.request = request\\n    ')\n            f.write('    self.{} = int(capture)\\n        '\n                    .format(error.capture_name))\n        else:\n            f.write('def __init__(self, request):\\n    '\n                    '    self.request = request\\n        ')\n\n        f.write('super(Exception, self).__init__('\n                '{}'.format(repr(error.description)))\n\n        if error.has_captures:\n            f.write('.format({0}=self.{0})'.format(error.capture_name))\n\n        f.write(' + self._fmt_request(self.request))\\n\\n')\n        f.write('    def __reduce__(self):\\n        ')\n        if error.has_captures:\n            f.write('return type(self), (self.request, self.{})\\n'.format(error.capture_name))\n        else:\n            f.write('return type(self), (self.request,)\\n')\n\n    # Create the actual {CODE: ErrorClassName} dict once classes are defined\n    f.write('\\n\\nrpc_errors_dict = {\\n')\n    for error in exact_match:\n        f.write('    {}: {},\\n'.format(repr(error.pattern), error.name))\n    f.write('}\\n\\nrpc_errors_re = (\\n')\n    for error in regex_match:\n        f.write('    ({}, {}),\\n'.format(repr(error.pattern), error.name))\n    f.write(')\\n')\n"
  },
  {
    "path": "telethon_generator/generators/tlobject.py",
    "content": "import functools\nimport os\nimport re\nimport shutil\nimport struct\nfrom collections import defaultdict\nfrom zlib import crc32\n\nfrom ..sourcebuilder import SourceBuilder\nfrom ..utils import snake_to_camel_case\n\nAUTO_GEN_NOTICE = \\\n    '\"\"\"File generated by TLObjects\\' generator. All changes will be ERASED\"\"\"'\n\n\nAUTO_CASTS = {\n    'InputPeer':\n        'utils.get_input_peer(await client.get_input_entity({}))',\n    'InputChannel':\n        'utils.get_input_channel(await client.get_input_entity({}))',\n    'InputUser':\n        'utils.get_input_user(await client.get_input_entity({}))',\n\n    'InputDialogPeer': 'await client._get_input_dialog({})',\n    'InputNotifyPeer': 'await client._get_input_notify({})',\n    'InputMedia': 'utils.get_input_media({})',\n    'InputPhoto': 'utils.get_input_photo({})',\n    'InputMessage': 'utils.get_input_message({})',\n    'InputDocument': 'utils.get_input_document({})',\n    'InputChatPhoto': 'utils.get_input_chat_photo({})',\n    'InputGroupCall': 'utils.get_input_group_call({})',\n}\n\nNAMED_AUTO_CASTS = {\n    ('chat_id', 'int'): 'await client.get_peer_id({}, add_mark=False)'\n}\n\n# Secret chats have a chat_id which may be negative.\n# With the named auto-cast above, we would break it.\n# However there are plenty of other legit requests\n# with `chat_id:int` where it is useful.\n#\n# NOTE: This works because the auto-cast is not recursive.\n#       There are plenty of types that would break if we\n#       did recurse into them to resolve them.\nNAMED_BLACKLIST = {\n    'messages.discardEncryption'\n}\n\nBASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128',\n              'int256', 'double', 'Bool', 'true', 'date')\n\n\ndef _write_modules(\n        out_dir, depth, kind, namespace_tlobjects, type_constructors):\n    # namespace_tlobjects: {'namespace', [TLObject]}\n    out_dir.mkdir(parents=True, exist_ok=True)\n    for ns, tlobjects in namespace_tlobjects.items():\n        file = out_dir / '{}.py'.format(ns or '__init__')\n        with file.open('w') as f, SourceBuilder(f) as builder:\n            builder.writeln(AUTO_GEN_NOTICE)\n\n            builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth)\n            if kind != 'TLObject':\n                builder.writeln(\n                    'from {}.tl.tlobject import {}', '.' * depth, kind)\n\n            builder.writeln('from typing import Optional, List, '\n                            'Union, TYPE_CHECKING')\n\n            # Add the relative imports to the namespaces,\n            # unless we already are in a namespace.\n            if not ns:\n                builder.writeln('from . import {}', ', '.join(sorted(\n                    x for x in namespace_tlobjects.keys() if x\n                )))\n\n            # Import 'os' for those needing access to 'os.urandom()'\n            # Currently only 'random_id' needs 'os' to be imported,\n            # for all those TLObjects with arg.can_be_inferred.\n            builder.writeln('import os')\n\n            # Import struct for the .__bytes__(self) serialization\n            builder.writeln('import struct')\n\n            # Import datetime for type hinting\n            builder.writeln('from datetime import datetime')\n\n            tlobjects.sort(key=lambda x: x.name)\n\n            type_names = set()\n            type_defs = []\n\n            # Find all the types in this file and generate type definitions\n            # based on the types. The type definitions are written to the\n            # file at the end.\n            for t in tlobjects:\n                if not t.is_function:\n                    type_name = t.result\n                    if '.' in type_name:\n                        type_name = type_name[type_name.rindex('.'):]\n                    if type_name in type_names:\n                        continue\n                    type_names.add(type_name)\n                    constructors = type_constructors[type_name]\n                    if not constructors:\n                        pass\n                    elif len(constructors) == 1:\n                        type_defs.append('Type{} = {}'.format(\n                            type_name, constructors[0].class_name))\n                    else:\n                        type_defs.append('Type{} = Union[{}]'.format(\n                            type_name, ','.join(c.class_name\n                                                for c in constructors)))\n\n            imports = {}\n            primitives = {'int', 'long', 'int128', 'int256', 'double',\n                          'string', 'date', 'bytes', 'Bool', 'true'}\n            # Find all the types in other files that are used in this file\n            # and generate the information required to import those types.\n            for t in tlobjects:\n                for arg in t.args:\n                    name = arg.type\n                    if not name or name in primitives:\n                        continue\n\n                    import_space = '{}.tl.types'.format('.' * depth)\n                    if '.' in name:\n                        namespace = name.split('.')[0]\n                        name = name.split('.')[1]\n                        import_space += '.{}'.format(namespace)\n\n                    if name not in type_names:\n                        type_names.add(name)\n                        if name == 'date':\n                            imports['datetime'] = ['datetime']\n                            continue\n                        elif import_space not in imports:\n                            imports[import_space] = set()\n                        imports[import_space].add('Type{}'.format(name))\n\n            # Add imports required for type checking\n            if imports:\n                builder.writeln('if TYPE_CHECKING:')\n                for namespace, names in imports.items():\n                    builder.writeln('from {} import {}',\n                                    namespace, ', '.join(sorted(names)))\n\n                builder.end_block()\n\n            # Generate the class for every TLObject\n            for t in tlobjects:\n                _write_source_code(t, kind, builder, type_constructors)\n                builder.current_indent = 0\n\n            # Write the type definitions generated earlier.\n            builder.writeln()\n            for line in type_defs:\n                builder.writeln(line)\n\n\ndef _write_source_code(tlobject, kind, builder, type_constructors):\n    \"\"\"\n    Writes the source code corresponding to the given TLObject\n    by making use of the ``builder`` `SourceBuilder`.\n\n    Additional information such as file path depth and\n    the ``Type: [Constructors]`` must be given for proper\n    importing and documentation strings.\n    \"\"\"\n    _write_class_init(tlobject, kind, type_constructors, builder)\n    _write_resolve(tlobject, builder)\n    _write_to_dict(tlobject, builder)\n    _write_to_bytes(tlobject, builder)\n    _write_from_reader(tlobject, builder)\n    _write_read_result(tlobject, builder)\n\n\ndef _write_class_init(tlobject, kind, type_constructors, builder):\n    builder.writeln()\n    builder.writeln()\n    builder.writeln('class {}({}):', tlobject.class_name, kind)\n\n    # Class-level variable to store its Telegram's constructor ID\n    builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id)\n    builder.writeln('SUBCLASS_OF_ID = {:#x}',\n                    crc32(tlobject.result.encode('ascii')))\n    builder.writeln()\n\n    # Convert the args to string parameters, those with flag having =None\n    args = ['{}: {}{}'.format(\n        a.name, a.type_hint(), '=None' if a.flag or a.can_be_inferred else '')\n        for a in tlobject.real_args\n    ]\n\n    # Write the __init__ function if it has any argument\n    if not tlobject.real_args:\n        return\n\n    if any(hasattr(__builtins__, a.name) for a in tlobject.real_args):\n        builder.writeln('# noinspection PyShadowingBuiltins')\n\n    builder.writeln(\"def __init__({}):\", ', '.join(['self'] + args))\n    builder.writeln('\"\"\"')\n    if tlobject.is_function:\n        builder.write(':returns {}: ', tlobject.result)\n    else:\n        builder.write('Constructor for {}: ', tlobject.result)\n\n    constructors = type_constructors[tlobject.result]\n    if not constructors:\n        builder.writeln('This type has no constructors.')\n    elif len(constructors) == 1:\n        builder.writeln('Instance of {}.',\n                        constructors[0].class_name)\n    else:\n        builder.writeln('Instance of either {}.', ', '.join(\n            c.class_name for c in constructors))\n\n    builder.writeln('\"\"\"')\n\n    # Set the arguments\n    for arg in tlobject.real_args:\n        if not arg.can_be_inferred:\n            builder.writeln('self.{0} = {0}', arg.name)\n\n        # Currently the only argument that can be\n        # inferred are those called 'random_id'\n        elif arg.name == 'random_id':\n            # Endianness doesn't really matter, and 'big' is shorter\n            code = \"int.from_bytes(os.urandom({}), 'big', signed=True)\" \\\n                .format(8 if arg.type == 'long' else 4)\n\n            if arg.is_vector:\n                # Currently for the case of \"messages.forwardMessages\"\n                # Ensure we can infer the length from id:Vector<>\n                if not next(a for a in tlobject.real_args\n                            if a.name == 'id').is_vector:\n                    raise ValueError(\n                        'Cannot infer list of random ids for ', tlobject\n                    )\n                code = '[{} for _ in range(len(id))]'.format(code)\n\n            builder.writeln(\n                \"self.random_id = random_id if random_id \"\n                \"is not None else {}\", code\n            )\n        else:\n            raise ValueError('Cannot infer a value for ', arg)\n\n    builder.end_block()\n\n\ndef _write_resolve(tlobject, builder):\n    if tlobject.is_function and any(\n            (arg.type in AUTO_CASTS\n             or ((arg.name, arg.type) in NAMED_AUTO_CASTS\n                 and tlobject.fullname not in NAMED_BLACKLIST))\n            for arg in tlobject.real_args\n    ):\n        builder.writeln('async def resolve(self, client, utils):')\n        for arg in tlobject.real_args:\n            ac = AUTO_CASTS.get(arg.type)\n            if not ac:\n                ac = NAMED_AUTO_CASTS.get((arg.name, arg.type))\n                if not ac:\n                    continue\n\n            if arg.flag:\n                builder.writeln('if self.{}:', arg.name)\n\n            if arg.is_vector:\n                builder.writeln('_tmp = []')\n                builder.writeln('for _x in self.{0}:', arg.name)\n                builder.writeln('_tmp.append({})', ac.format('_x'))\n                builder.end_block()\n                builder.writeln('self.{} = _tmp', arg.name)\n            else:\n                builder.writeln('self.{} = {}', arg.name,\n                              ac.format('self.' + arg.name))\n\n            if arg.flag:\n                builder.end_block()\n        builder.end_block()\n\n\ndef _write_to_dict(tlobject, builder):\n    builder.writeln('def to_dict(self):')\n    builder.writeln('return {')\n    builder.current_indent += 1\n\n    builder.write(\"'_': '{}'\", tlobject.class_name)\n    for arg in tlobject.real_args:\n        builder.writeln(',')\n        builder.write(\"'{}': \", arg.name)\n        if arg.type in BASE_TYPES:\n            if arg.is_vector:\n                builder.write('[] if self.{0} is None else self.{0}[:]',\n                              arg.name)\n            else:\n                builder.write('self.{}', arg.name)\n        else:\n            if arg.is_vector:\n                builder.write(\n                    '[] if self.{0} is None else [x.to_dict() '\n                    'if isinstance(x, TLObject) else x for x in self.{0}]',\n                    arg.name\n                )\n            else:\n                builder.write(\n                    'self.{0}.to_dict() '\n                    'if isinstance(self.{0}, TLObject) else self.{0}',\n                    arg.name\n                )\n\n    builder.writeln()\n    builder.current_indent -= 1\n    builder.writeln(\"}\")\n\n    builder.end_block()\n\n\ndef _write_to_bytes(tlobject, builder):\n    builder.writeln('def _bytes(self):')\n\n    # Some objects require more than one flag parameter to be set\n    # at the same time. In this case, add an assertion.\n    repeated_args = defaultdict(list)\n    for arg in tlobject.args:\n        if arg.flag:\n            repeated_args[(arg.flag, arg.flag_index)].append(arg)\n\n    for ra in repeated_args.values():\n        if len(ra) > 1:\n            cnd1 = ('(self.{0} or self.{0} is not None)'\n                    .format(a.name) for a in ra)\n            cnd2 = ('(self.{0} is None or self.{0} is False)'\n                    .format(a.name) for a in ra)\n            builder.writeln(\n                \"assert ({}) or ({}), '{} parameters must all \"\n                \"be False-y (like None) or all me True-y'\",\n                ' and '.join(cnd1), ' and '.join(cnd2),\n                ', '.join(a.name for a in ra)\n            )\n\n    builder.writeln(\"return b''.join((\")\n    builder.current_indent += 1\n\n    # First constructor code, we already know its bytes\n    builder.writeln('{},', repr(struct.pack('<I', tlobject.id)))\n\n    for arg in tlobject.args:\n        if _write_arg_to_bytes(builder, arg, tlobject):\n            builder.writeln(',')\n\n    builder.current_indent -= 1\n    builder.writeln('))')\n    builder.end_block()\n\n\ndef _write_from_reader(tlobject, builder):\n    builder.writeln('@classmethod')\n    builder.writeln('def from_reader(cls, reader):')\n    for arg in tlobject.args:\n        _write_arg_read_code(builder, arg, tlobject, name='_' + arg.name)\n\n    builder.writeln('return cls({})', ', '.join(\n        '{0}=_{0}'.format(a.name) for a in tlobject.real_args))\n\n\ndef _write_read_result(tlobject, builder):\n    # Only requests can have a different response that's not their\n    # serialized body, that is, we'll be setting their .result.\n    #\n    # The default behaviour is reading a TLObject too, so no need\n    # to override it unless necessary.\n    if not tlobject.is_function:\n        return\n\n    # https://core.telegram.org/mtproto/serialize#boxed-and-bare-types\n    # TL;DR; boxed types start with uppercase always, so we can use\n    # this to check whether everything in it is boxed or not.\n    #\n    # Currently only un-boxed responses are Vector<int>/Vector<long>.\n    # If this weren't the case, we should check upper case after\n    # max(index('<'), index('.')) (and if it is, it's boxed, so return).\n    m = re.match(r'Vector<(int|long)>', tlobject.result)\n    if not m:\n        return\n\n    builder.end_block()\n    builder.writeln('@staticmethod')\n    builder.writeln('def read_result(reader):')\n    builder.writeln('reader.read_int()  # Vector ID')\n    builder.writeln('return [reader.read_{}() '\n                    'for _ in range(reader.read_int())]', m.group(1))\n\n\ndef _write_arg_to_bytes(builder, arg, tlobject, name=None):\n    \"\"\"\n    Writes the .__bytes__() code for the given argument\n    :param builder: The source code builder\n    :param arg: The argument to write\n    :param tlobject: The parent TLObject\n    :param name: The name of the argument. Defaults to \"self.argname\"\n                 This argument is an option because it's required when\n                 writing Vectors<>\n    \"\"\"\n    if arg.generic_definition:\n        return  # Do nothing, this only specifies a later type\n\n    if name is None:\n        name = 'self.{}'.format(arg.name)\n\n    # The argument may be a flag, only write if it's not None AND\n    # if it's not a True type.\n    # True types are not actually sent, but instead only used to\n    # determine the flags.\n    if arg.flag:\n        if arg.type == 'true':\n            return  # Exit, since True type is never written\n        elif arg.is_vector:\n            # Vector flags are special since they consist of 3 values,\n            # so we need an extra join here. Note that empty vector flags\n            # should NOT be sent either!\n            builder.write(\"b'' if {0} is None or {0} is False \"\n                          \"else b''.join((\", name)\n        elif 'Bool' == arg.type:\n            # `False` is a valid value for this type, so only check for `None`.\n            builder.write(\"b'' if {0} is None else (\", name)\n        else:\n            builder.write(\"b'' if {0} is None or {0} is False \"\n                          \"else (\", name)\n\n    if arg.is_vector:\n        if arg.use_vector_id:\n            # vector code, unsigned 0x1cb5c415 as little endian\n            builder.write(r\"b'\\x15\\xc4\\xb5\\x1c',\")\n\n        builder.write(\"struct.pack('<i', len({})),\", name)\n\n        # Cannot unpack the values for the outer tuple through *[(\n        # since that's a Python >3.5 feature, so add another join.\n        builder.write(\"b''.join(\")\n\n        # Temporary disable .is_vector, not to enter this if again\n        # Also disable .flag since it's not needed per element\n        old_flag, arg.flag = arg.flag, None\n        arg.is_vector = False\n        _write_arg_to_bytes(builder, arg, tlobject, name='x')\n        arg.is_vector = True\n        arg.flag = old_flag\n\n        builder.write(' for x in {})', name)\n\n    elif arg.flag_indicator:\n        # Calculate the flags with those items which are not None\n        if not any(f.flag for f in tlobject.args):\n            # There's a flag indicator, but no flag arguments so it's 0\n            builder.write(r\"b'\\0\\0\\0\\0'\")\n        else:\n            def fmt_flag_arg(a):\n                if a.type == 'Bool':\n                    fmt = '(0 if {0} is None else {1})'\n                else:\n                    fmt = '(0 if {0} is None or {0} is False else {1})'\n                return fmt.format('self.{}'.format(a.name), 1 << a.flag_index)\n\n            builder.write(\"struct.pack('<I', \")\n            builder.write(\n                ' | '.join(fmt_flag_arg(a) for a in tlobject.args if a.flag == arg.name)\n            )\n            builder.write(')')\n\n    elif 'int' == arg.type:\n        # User IDs are becoming larger than 2³¹ - 1, which would translate\n        # into reading a negative ID, which we would treat as a chat. So\n        # special case them to read unsigned. See https://t.me/BotNews/57.\n        if arg.name == 'user_id' or (arg.name == 'id' and tlobject.result == 'User'):\n            builder.write(\"struct.pack('<I', {})\", name)\n        else:\n            # struct.pack is around 4 times faster than int.to_bytes\n            builder.write(\"struct.pack('<i', {})\", name)\n\n    elif 'long' == arg.type:\n        builder.write(\"struct.pack('<q', {})\", name)\n\n    elif 'int128' == arg.type:\n        builder.write(\"{}.to_bytes(16, 'little', signed=True)\", name)\n\n    elif 'int256' == arg.type:\n        builder.write(\"{}.to_bytes(32, 'little', signed=True)\", name)\n\n    elif 'double' == arg.type:\n        builder.write(\"struct.pack('<d', {})\", name)\n\n    elif 'string' == arg.type:\n        builder.write('self.serialize_bytes({})', name)\n\n    elif 'Bool' == arg.type:\n        # 0x997275b5 if boolean else 0xbc799737\n        builder.write(r\"b'\\xb5ur\\x99' if {} else b'7\\x97y\\xbc'\", name)\n\n    elif 'true' == arg.type:\n        pass  # These are actually NOT written! Only used for flags\n\n    elif 'bytes' == arg.type:\n        builder.write('self.serialize_bytes({})', name)\n\n    elif 'date' == arg.type:  # Custom format\n        builder.write('self.serialize_datetime({})', name)\n\n    else:\n        # Else it may be a custom type\n        builder.write('{}._bytes()', name)\n\n        # If the type is not boxed (i.e. starts with lowercase) we should\n        # not serialize the constructor ID (so remove its first 4 bytes).\n        boxed = arg.type[arg.type.find('.') + 1].isupper()\n        if not boxed:\n            builder.write('[4:]')\n\n    if arg.flag:\n        builder.write(')')\n        if arg.is_vector:\n            builder.write(')')  # We were using a tuple\n\n    return True  # Something was written\n\n\ndef _write_arg_read_code(builder, arg, tlobject, name):\n    \"\"\"\n    Writes the read code for the given argument, setting the\n    arg.name variable to its read value.\n\n    :param builder: The source code builder\n    :param arg: The argument to write\n    :param tlobject: The parent TLObject\n    :param name: The name of the argument. Defaults to \"self.argname\"\n                 This argument is an option because it's required when\n                 writing Vectors<>\n    \"\"\"\n\n    if arg.generic_definition:\n        return  # Do nothing, this only specifies a later type\n\n    # The argument may be a flag, only write that flag was given!\n    old_flag = None\n    if arg.flag:\n        # Treat 'true' flags as a special case, since they're true if\n        # they're set, and nothing else needs to actually be read.\n        if 'true' == arg.type:\n            builder.writeln('{} = bool({} & {})',\n                            name, arg.flag, 1 << arg.flag_index)\n            return\n\n        builder.writeln('if {} & {}:', arg.flag, 1 << arg.flag_index)\n        # Temporary disable .flag not to enter this if\n        # again when calling the method recursively\n        old_flag, arg.flag = arg.flag, None\n\n    if arg.is_vector:\n        if arg.use_vector_id:\n            # We have to read the vector's constructor ID\n            builder.writeln(\"reader.read_int()\")\n\n        builder.writeln('{} = []', name)\n        builder.writeln('for _ in range(reader.read_int()):')\n        # Temporary disable .is_vector, not to enter this if again\n        arg.is_vector = False\n        _write_arg_read_code(builder, arg, tlobject, name='_x')\n        builder.writeln('{}.append(_x)', name)\n        arg.is_vector = True\n\n    elif arg.flag_indicator:\n        # Read the flags, which will indicate what items we should read next\n        builder.writeln('{} = reader.read_int()', arg.name)\n        builder.writeln()\n\n    elif 'int' == arg.type:\n        # User IDs are becoming larger than 2³¹ - 1, which would translate\n        # into reading a negative ID, which we would treat as a chat. So\n        # special case them to read unsigned. See https://t.me/BotNews/57.\n        if arg.name == 'user_id' or (arg.name == 'id' and tlobject.result == 'User'):\n            builder.writeln('{} = reader.read_int(signed=False)', name)\n        else:\n            builder.writeln('{} = reader.read_int()', name)\n\n    elif 'long' == arg.type:\n        builder.writeln('{} = reader.read_long()', name)\n\n    elif 'int128' == arg.type:\n        builder.writeln('{} = reader.read_large_int(bits=128)', name)\n\n    elif 'int256' == arg.type:\n        builder.writeln('{} = reader.read_large_int(bits=256)', name)\n\n    elif 'double' == arg.type:\n        builder.writeln('{} = reader.read_double()', name)\n\n    elif 'string' == arg.type:\n        builder.writeln('{} = reader.tgread_string()', name)\n\n    elif 'Bool' == arg.type:\n        builder.writeln('{} = reader.tgread_bool()', name)\n\n    elif 'true' == arg.type:\n        # Arbitrary not-None value, don't actually read \"true\" flags\n        builder.writeln('{} = True', name)\n\n    elif 'bytes' == arg.type:\n        builder.writeln('{} = reader.tgread_bytes()', name)\n\n    elif 'date' == arg.type:  # Custom format\n        builder.writeln('{} = reader.tgread_date()', name)\n\n    else:\n        # Else it may be a custom type\n        if not arg.skip_constructor_id:\n            builder.writeln('{} = reader.tgread_object()', name)\n        else:\n            # Import the correct type inline to avoid cyclic imports.\n            # There may be better solutions so that we can just access\n            # all the types before the files have been parsed, but I\n            # don't know of any.\n            sep_index = arg.type.find('.')\n            if sep_index == -1:\n                ns, t = '.', arg.type\n            else:\n                ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:]\n            class_name = snake_to_camel_case(t)\n\n            # There would be no need to import the type if we're in the\n            # file with the same namespace, but since it does no harm\n            # and we don't have information about such thing in the\n            # method we just ignore that case.\n            builder.writeln('from {} import {}', ns, class_name)\n            builder.writeln('{} = {}.from_reader(reader)',\n                            name, class_name)\n\n    # End vector and flag blocks if required (if we opened them before)\n    if arg.is_vector:\n        builder.end_block()\n\n    if old_flag:\n        builder.current_indent -= 1\n        builder.writeln('else:')\n        builder.writeln('{} = None', name)\n        builder.current_indent -= 1\n        # Restore .flag\n        arg.flag = old_flag\n\n\ndef _write_all_tlobjects(tlobjects, layer, builder):\n    builder.writeln(AUTO_GEN_NOTICE)\n    builder.writeln()\n\n    builder.writeln('from . import types, functions')\n    builder.writeln()\n\n    # Create a constant variable to indicate which layer this is\n    builder.writeln('LAYER = {}', layer)\n    builder.writeln()\n\n    # Then create the dictionary containing constructor_id: class\n    builder.writeln('tlobjects = {')\n    builder.current_indent += 1\n\n    # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class)\n    tlobjects.sort(key=lambda x: x.name)\n    for tlobject in tlobjects:\n        builder.write('{:#010x}: ', tlobject.id)\n        builder.write('functions' if tlobject.is_function else 'types')\n\n        if tlobject.namespace:\n            builder.write('.{}', tlobject.namespace)\n\n        builder.writeln('.{},', tlobject.class_name)\n\n    builder.current_indent -= 1\n    builder.writeln('}')\n\n\ndef generate_tlobjects(tlobjects, layer, import_depth, output_dir):\n    # Group everything by {namespace: [tlobjects]} to generate __init__.py\n    namespace_functions = defaultdict(list)\n    namespace_types = defaultdict(list)\n\n    # Group {type: [constructors]} to generate the documentation\n    type_constructors = defaultdict(list)\n    for tlobject in tlobjects:\n        if tlobject.is_function:\n            namespace_functions[tlobject.namespace].append(tlobject)\n        else:\n            namespace_types[tlobject.namespace].append(tlobject)\n            type_constructors[tlobject.result].append(tlobject)\n\n    _write_modules(output_dir / 'functions', import_depth, 'TLRequest',\n                   namespace_functions, type_constructors)\n    _write_modules(output_dir / 'types', import_depth, 'TLObject',\n                   namespace_types, type_constructors)\n\n    filename = output_dir / 'alltlobjects.py'\n    with filename.open('w') as file:\n        with SourceBuilder(file) as builder:\n            _write_all_tlobjects(tlobjects, layer, builder)\n\n\ndef clean_tlobjects(output_dir):\n    for d in ('functions', 'types'):\n        d = output_dir / d\n        if d.is_dir():\n            shutil.rmtree(str(d))\n\n    tl = output_dir / 'alltlobjects.py'\n    if tl.is_file():\n        tl.unlink()\n"
  },
  {
    "path": "telethon_generator/parsers/__init__.py",
    "content": "from .errors import Error, parse_errors\nfrom .methods import MethodInfo, Usability, parse_methods\nfrom .tlobject import TLObject, parse_tl, find_layer\n"
  },
  {
    "path": "telethon_generator/parsers/errors.py",
    "content": "import csv\nimport re\n\nfrom ..utils import snake_to_camel_case\n\n# Core base classes depending on the integer error code\nKNOWN_BASE_CLASSES = {\n    303: 'InvalidDCError',\n    400: 'BadRequestError',\n    401: 'UnauthorizedError',\n    403: 'ForbiddenError',\n    404: 'NotFoundError',\n    406: 'AuthKeyError',\n    420: 'FloodError',\n    500: 'ServerError',\n    503: 'TimedOutError'\n}\n\n\ndef _get_class_name(error_code):\n    \"\"\"\n    Gets the corresponding class name for the given error code,\n    this either being an integer (thus base error name) or str.\n    \"\"\"\n    if isinstance(error_code, int):\n        return KNOWN_BASE_CLASSES.get(\n            abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg')\n        )\n\n    if error_code.startswith('2'):\n        error_code = re.sub(r'2', 'TWO_', error_code, count=1)\n\n    if re.match(r'\\d+', error_code):\n        raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code))\n\n    return snake_to_camel_case(\n        error_code.replace('FIRSTNAME', 'FIRST_NAME')\\\n                  .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error')\n\n\nclass Error:\n    def __init__(self, codes, name, description):\n        # TODO Some errors have the same name but different integer codes\n        # Should these be split into different files or doesn't really matter?\n        # Telegram isn't exactly consistent with returned errors anyway.\n        self.int_code = codes[0]\n        self.int_codes = codes\n        self.str_code = name\n        self.subclass = _get_class_name(codes[0])\n        self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES\n        self.description = description\n\n        self.has_captures = '_X' in name\n        if self.has_captures:\n            self.name = _get_class_name(name.replace('_X', '_'))\n            self.pattern = name.replace('_X', r'_(\\d+)')\n            self.capture_name = re.search(r'{(\\w+)}', description).group(1)\n        else:\n            self.name = _get_class_name(name)\n            self.pattern = name\n            self.capture_name = None\n\n\ndef parse_errors(csv_file):\n    \"\"\"\n    Parses the input CSV file with columns (name, error codes, description)\n    and yields `Error` instances as a result.\n    \"\"\"\n    with csv_file.open(newline='') as f:\n        f = csv.reader(f)\n        next(f, None)  # header\n        for line, tup in enumerate(f, start=2):\n            try:\n                name, codes, description = tup\n            except ValueError:\n                raise ValueError('Columns count mismatch, unquoted comma in '\n                                 'desc? (line {})'.format(line)) from None\n\n            try:\n                codes = [int(x) for x in codes.split()] or [400]\n            except ValueError:\n                raise ValueError('Not all codes are integers '\n                                 '(line {})'.format(line)) from None\n\n            yield Error([int(x) for x in codes], name, description)\n"
  },
  {
    "path": "telethon_generator/parsers/methods.py",
    "content": "import csv\nimport enum\nimport warnings\n\n\nclass Usability(enum.Enum):\n    UNKNOWN = 0\n    USER = 1\n    BOT = 2\n    BOTH = 4\n\n    @property\n    def key(self):\n        return {\n            Usability.UNKNOWN: 'unknown',\n            Usability.USER: 'user',\n            Usability.BOT: 'bot',\n            Usability.BOTH: 'both',\n        }[self]\n\n\nclass MethodInfo:\n    def __init__(self, name, usability, errors, friendly):\n        self.name = name\n        self.errors = errors\n        self.friendly = friendly\n        try:\n            self.usability = {\n                'unknown': Usability.UNKNOWN,\n                'user': Usability.USER,\n                'bot': Usability.BOT,\n                'both': Usability.BOTH,\n            }[usability.lower()]\n        except KeyError:\n            raise ValueError('Usability must be either user, bot, both or '\n                             'unknown, not {}'.format(usability)) from None\n\n\ndef parse_methods(csv_file, friendly_csv_file, errors_dict):\n    \"\"\"\n    Parses the input CSV file with columns (method, usability, errors)\n    and yields `MethodInfo` instances as a result.\n    \"\"\"\n    raw_to_friendly = {}\n    with friendly_csv_file.open(newline='') as f:\n        f = csv.reader(f)\n        next(f, None)  # header\n        for ns, friendly, raw_list in f:\n            for raw in raw_list.split():\n                raw_to_friendly[raw] = (ns, friendly)\n\n    with csv_file.open(newline='') as f:\n        f = csv.reader(f)\n        next(f, None)  # header\n        for line, (method, usability, errors) in enumerate(f, start=2):\n            try:\n                errors = [errors_dict[x] for x in errors.split()]\n            except KeyError:\n                raise ValueError('Method {} references unknown errors {}'\n                                 .format(method, errors)) from None\n\n            friendly = raw_to_friendly.pop(method, None)\n            yield MethodInfo(method, usability, errors, friendly)\n\n    if raw_to_friendly:\n        warnings.warn('note: unknown raw methods in friendly mapping: {}'\n                      .format(', '.join(raw_to_friendly)))\n"
  },
  {
    "path": "telethon_generator/parsers/tlobject/__init__.py",
    "content": "from .tlarg import TLArg\nfrom .tlobject import TLObject\nfrom .parser import parse_tl, find_layer\n"
  },
  {
    "path": "telethon_generator/parsers/tlobject/parser.py",
    "content": "import collections\nimport re\n\nfrom .tlarg import TLArg\nfrom .tlobject import TLObject\nfrom ..methods import Usability\n\n\nCORE_TYPES = {\n    0xbc799737,  # boolFalse#bc799737 = Bool;\n    0x997275b5,  # boolTrue#997275b5 = Bool;\n    0x3fedd339,  # true#3fedd339 = True;\n    0xc4b9f9bb,  # error#c4b9f9bb code:int text:string = Error;\n    0x56730bcc   # null#56730bcc = Null;\n}\n\n# Telegram Desktop (C++) doesn't care about string/bytes, and the .tl files\n# don't either. However in Python we *do*, and we want to deal with bytes\n# for the authorization key process, not UTF-8 strings (they won't be).\n#\n# Every type with an ID that's in here should get their attribute types\n# with string being replaced with bytes.\nAUTH_KEY_TYPES = {\n    0x05162463,  # resPQ,\n    0x83c95aec,  # p_q_inner_data\n    0xa9f55f95,  # p_q_inner_data_dc\n    0x3c6a84d4,  # p_q_inner_data_temp\n    0x56fddf88,  # p_q_inner_data_temp_dc\n    0xd0e8075c,  # server_DH_params_ok\n    0xb5890dba,  # server_DH_inner_data\n    0x6643b654,  # client_DH_inner_data\n    0xd712e4be,  # req_DH_params\n    0xf5045f1f,  # set_client_DH_params\n    0x3072cfa1   # gzip_packed\n}\n\n\ndef _from_line(line, is_function, method_info, layer):\n    match = re.match(\n        r'^([\\w.]+)'                     # 'name'\n        r'(?:#([0-9a-fA-F]+))?'          # '#optionalcode'\n        r'(?:\\s{?\\w+:[\\w\\d<>#.?!]+}?)*'  # '{args:.0?type}'\n        r'\\s=\\s'                         # ' = '\n        r'([\\w\\d<>#.?]+);$',             # '<result.type>;'\n        line\n    )\n    if match is None:\n        # Probably \"vector#1cb5c415 {t:Type} # [ t ] = Vector t;\"\n        raise ValueError('Cannot parse TLObject {}'.format(line))\n\n    args_match = re.findall(\n        r'({)?'\n        r'(\\w+)'\n        r':'\n        r'([\\w\\d<>#.?!]+)'\n        r'}?',\n        line\n    )\n\n    name = match.group(1)\n    method_info = method_info.get(name)\n    if method_info:\n        usability = method_info.usability\n        friendly = method_info.friendly\n    else:\n        usability = Usability.UNKNOWN\n        friendly = None\n\n    return TLObject(\n        fullname=name,\n        object_id=match.group(2),\n        result=match.group(3),\n        is_function=is_function,\n        layer=layer,\n        usability=usability,\n        friendly=friendly,\n        args=[TLArg(name, arg_type, brace != '')\n              for brace, name, arg_type in args_match]\n    )\n\n\ndef parse_tl(file_path, layer, methods=None, ignored_ids=CORE_TYPES):\n    \"\"\"\n    This method yields TLObjects from a given .tl file.\n\n    Note that the file is parsed completely before the function yields\n    because references to other objects may appear later in the file.\n    \"\"\"\n    method_info = {m.name: m for m in (methods or [])}\n    obj_all = []\n    obj_by_name = {}\n    obj_by_type = collections.defaultdict(list)\n    with file_path.open() as file:\n        is_function = False\n        for line in file:\n            comment_index = line.find('//')\n            if comment_index != -1:\n                line = line[:comment_index]\n\n            line = line.strip()\n            if not line:\n                continue\n\n            match = re.match(r'---(\\w+)---', line)\n            if match:\n                following_types = match.group(1)\n                is_function = following_types == 'functions'\n                continue\n\n            try:\n                result = _from_line(\n                    line, is_function, method_info, layer=layer)\n\n                if result.id in ignored_ids:\n                    continue\n\n                obj_all.append(result)\n                if not result.is_function:\n                    obj_by_name[result.fullname] = result\n                    obj_by_type[result.result].append(result)\n            except ValueError as e:\n                if 'vector#1cb5c415' not in str(e):\n                    raise\n\n    # Once all objects have been parsed, replace the\n    # string type from the arguments with references\n    for obj in obj_all:\n        if obj.id in AUTH_KEY_TYPES:\n            for arg in obj.args:\n                if arg.type == 'string':\n                    arg.type = 'bytes'\n\n        for arg in obj.args:\n            arg.cls = obj_by_type.get(arg.type) or (\n                [obj_by_name[arg.type]] if arg.type in obj_by_name else []\n            )\n\n    yield from obj_all\n\n\ndef find_layer(file_path):\n    \"\"\"Finds the layer used on the specified scheme.tl file.\"\"\"\n    layer_regex = re.compile(r'^//\\s*LAYER\\s*(\\d+)$')\n    with file_path.open('r') as file:\n        for line in file:\n            match = layer_regex.match(line)\n            if match:\n                return int(match.group(1))\n"
  },
  {
    "path": "telethon_generator/parsers/tlobject/tlarg.py",
    "content": "import re\n\n\ndef _fmt_strings(*dicts):\n    for d in dicts:\n        for k, v in d.items():\n            if v in ('None', 'True', 'False'):\n                d[k] = '<strong>{}</strong>'.format(v)\n            else:\n                d[k] = re.sub(\n                    r'([brf]?([\\'\"]).*\\2)',\n                    lambda m: '<em>{}</em>'.format(m.group(1)),\n                    v\n                )\n\n\nKNOWN_NAMED_EXAMPLES = {\n    ('message', 'string'): \"'Hello there!'\",\n    ('expires_at', 'date'): 'datetime.timedelta(minutes=5)',\n    ('until_date', 'date'): 'datetime.timedelta(days=14)',\n    ('view_messages', 'true'): 'None',\n    ('send_messages', 'true'): 'None',\n    ('limit', 'int'): '100',\n    ('hash', 'int'): '0',\n    ('hash', 'string'): \"'A4LmkR23G0IGxBE71zZfo1'\",\n    ('min_id', 'int'): '0',\n    ('max_id', 'int'): '0',\n    ('min_id', 'long'): '0',\n    ('max_id', 'long'): '0',\n    ('add_offset', 'int'): '0',\n    ('title', 'string'): \"'My awesome title'\",\n    ('device_model', 'string'): \"'ASUS Laptop'\",\n    ('system_version', 'string'): \"'Arch Linux'\",\n    ('app_version', 'string'): \"'1.0'\",\n    ('system_lang_code', 'string'): \"'en'\",\n    ('lang_pack', 'string'): \"''\",\n    ('lang_code', 'string'): \"'en'\",\n    ('chat_id', 'int'): '478614198',\n    ('client_id', 'long'): 'random.randrange(-2**63, 2**63)',\n    ('video', 'InputFile'): \"client.upload_file('/path/to/file.mp4')\",\n}\n\nKNOWN_TYPED_EXAMPLES = {\n    'int128': \"int.from_bytes(os.urandom(16), 'big')\",\n    'int256': \"int.from_bytes(os.urandom(32), 'big')\",\n    'bytes': \"b'arbitrary\\\\x7f data \\\\xfa here'\",\n    'long': \"-12398745604826\",\n    'string': \"'some string here'\",\n    'int': '42',\n    'date': 'datetime.datetime(2018, 6, 25)',\n    'double': '7.13',\n    'Bool': 'False',\n    'true': 'True',\n    'InputChatPhoto': \"client.upload_file('/path/to/photo.jpg')\",\n    'InputFile': \"client.upload_file('/path/to/file.jpg')\",\n    'InputPeer': \"'username'\"\n}\n\n_fmt_strings(KNOWN_NAMED_EXAMPLES, KNOWN_TYPED_EXAMPLES)\n\nSYNONYMS = {\n    'InputUser': 'InputPeer',\n    'InputChannel': 'InputPeer',\n    'InputDialogPeer': 'InputPeer',\n    'InputNotifyPeer': 'InputPeer',\n    'InputMessage': 'int'\n}\n\n# These are flags that are cleaner to leave off\nOMITTED_EXAMPLES = {\n    'silent',\n    'background',\n    'clear_draft',\n    'reply_to_msg_id',\n    'random_id',\n    'reply_markup',\n    'entities',\n    'embed_links',\n    'hash',\n    'min_id',\n    'max_id',\n    'add_offset',\n    'grouped',\n    'broadcast',\n    'admins',\n    'edit',\n    'delete'\n}\n\n\nclass TLArg:\n    def __init__(self, name, arg_type, generic_definition):\n        \"\"\"\n        Initializes a new .tl argument\n        :param name: The name of the .tl argument\n        :param arg_type: The type of the .tl argument\n        :param generic_definition: Is the argument a generic definition?\n                                   (i.e. {X:Type})\n        \"\"\"\n        if name == 'self':\n            self.name = 'is_self'\n        elif name == 'from':\n            self.name = 'from_'\n        else:\n            self.name = name\n\n        # Default values\n        self.is_vector = False\n        self.flag = None  # name of the flag to check if self is present\n        self.skip_constructor_id = False\n        self.flag_index = -1  # bit index of the flag to check if self is present\n        self.cls = None\n\n        # Special case: some types can be inferred, which makes it\n        # less annoying to type. Currently the only type that can\n        # be inferred is if the name is 'random_id', to which a\n        # random ID will be assigned if left as None (the default)\n        self.can_be_inferred = name == 'random_id'\n\n        # The type can be an indicator that other arguments will be flags\n        if arg_type == '#':\n            self.flag_indicator = True\n            self.type = None\n            self.is_generic = False\n        else:\n            self.flag_indicator = False\n            self.is_generic = arg_type.startswith('!')\n            # Strip the exclamation mark always to have only the name\n            self.type = arg_type.lstrip('!')\n\n            # The type may be a flag (FLAGS.IDX?REAL_TYPE)\n            # FLAGS can be any name, but it should have appeared previously.\n            flag_match = re.match(r'(\\w+).(\\d+)\\?([\\w<>.]+)', self.type)\n            if flag_match:\n                self.flag = flag_match.group(1)\n                self.flag_index = int(flag_match.group(2))\n                # Update the type to match the exact type, not the \"flagged\" one\n                self.type = flag_match.group(3)\n\n            # Then check if the type is a Vector<REAL_TYPE>\n            vector_match = re.match(r'[Vv]ector<([\\w\\d.]+)>', self.type)\n            if vector_match:\n                self.is_vector = True\n\n                # If the type's first letter is not uppercase, then\n                # it is a constructor and we use (read/write) its ID\n                # as pinpointed on issue #81.\n                self.use_vector_id = self.type[0] == 'V'\n\n                # Update the type to match the one inside the vector\n                self.type = vector_match.group(1)\n\n            # See use_vector_id. An example of such case is ipPort in\n            # help.configSpecial\n            if self.type.split('.')[-1][0].islower():\n                self.skip_constructor_id = True\n\n            # The name may contain \"date\" in it, if this is the case and\n            # the type is \"int\", we can safely assume that this should be\n            # treated as a \"date\" object. Note that this is not a valid\n            # Telegram object, but it's easier to work with\n            if self.type == 'int' and (\n                        re.search(r'(\\b|_)(date|until|since)(\\b|_)', name) or\n                        name in ('expires', 'expires_at', 'was_online')):\n                self.type = 'date'\n\n        self.generic_definition = generic_definition\n\n    def type_hint(self):\n        cls = self.type\n        if '.' in cls:\n            cls = cls.split('.')[1]\n        result = {\n            'int': 'int',\n            'long': 'int',\n            'int128': 'int',\n            'int256': 'int',\n            'double': 'float',\n            'string': 'str',\n            'date': 'Optional[datetime]',  # None date = 0 timestamp\n            'bytes': 'bytes',\n            'Bool': 'bool',\n            'true': 'bool',\n        }.get(cls, \"'Type{}'\".format(cls))\n        if self.is_vector:\n            result = 'List[{}]'.format(result)\n        if self.flag and cls != 'date':\n            result = 'Optional[{}]'.format(result)\n\n        return result\n\n    def real_type(self):\n        # Find the real type representation by updating it as required\n        real_type = self.type\n        if self.flag_indicator:\n            real_type = '#'\n\n        if self.is_vector:\n            if self.use_vector_id:\n                real_type = 'Vector<{}>'.format(real_type)\n            else:\n                real_type = 'vector<{}>'.format(real_type)\n\n        if self.is_generic:\n            real_type = '!{}'.format(real_type)\n\n        if self.flag:\n            real_type = '{}.{}?{}'.format(self.flag, self.flag_index, real_type)\n\n        return real_type\n\n    def __str__(self):\n        name = self.orig_name()\n        if self.generic_definition:\n            return '{{{}:{}}}'.format(name, self.real_type())\n        else:\n            return '{}:{}'.format(name, self.real_type())\n\n    def __repr__(self):\n        return str(self).replace(':date', ':int').replace('?date', '?int')\n\n    def orig_name(self):\n        return self.name.replace('is_self', 'self').strip('_')\n\n    def to_dict(self):\n        return {\n            'name': self.orig_name(),\n            'type': re.sub(r'\\bdate$', 'int', self.real_type())\n        }\n\n    def as_example(self, f, indent=0):\n        if self.is_generic:\n            f.write('other_request')\n            return\n\n        known = (KNOWN_NAMED_EXAMPLES.get((self.name, self.type))\n                 or KNOWN_TYPED_EXAMPLES.get(self.type)\n                 or KNOWN_TYPED_EXAMPLES.get(SYNONYMS.get(self.type)))\n        if known:\n            f.write(known)\n            return\n\n        assert self.omit_example() or self.cls, 'TODO handle ' + str(self)\n\n        # Pick an interesting example if any\n        for cls in self.cls:\n            if cls.is_good_example():\n                cls.as_example(f, indent)\n                break\n        else:\n            # If no example is good, just pick the first\n            self.cls[0].as_example(f, indent)\n\n    def omit_example(self):\n        return (self.flag or self.can_be_inferred) \\\n               and self.name in OMITTED_EXAMPLES\n"
  },
  {
    "path": "telethon_generator/parsers/tlobject/tlobject.py",
    "content": "import re\nimport struct\nimport zlib\n\nfrom ...utils import snake_to_camel_case\n\n# https://github.com/telegramdesktop/tdesktop/blob/4bf66cb6e93f3965b40084771b595e93d0b11bcd/Telegram/SourceFiles/codegen/scheme/codegen_scheme.py#L57-L62\nWHITELISTED_MISMATCHING_IDS = {\n    # 0 represents any layer\n    0: {'channel',  # Since layer 77, there seems to be no going back...\n        'ipPortSecret', 'accessPointRule', 'help.configSimple'}\n}\n\n\nclass TLObject:\n    def __init__(self, fullname, object_id, args, result,\n                 is_function, usability, friendly, layer):\n        \"\"\"\n        Initializes a new TLObject, given its properties.\n\n        :param fullname: The fullname of the TL object (namespace.name)\n                         The namespace can be omitted.\n        :param object_id: The hexadecimal string representing the object ID\n        :param args: The arguments, if any, of the TL object\n        :param result: The result type of the TL object\n        :param is_function: Is the object a function or a type?\n        :param usability: The usability for this method.\n        :param friendly: A tuple (namespace, friendly method name) if known.\n        :param layer: The layer this TLObject belongs to.\n        \"\"\"\n        # The name can or not have a namespace\n        self.fullname = fullname\n        if '.' in fullname:\n            self.namespace, self.name = fullname.split('.', maxsplit=1)\n        else:\n            self.namespace, self.name = None, fullname\n\n        self.args = args\n        self.result = result\n        self.is_function = is_function\n        self.usability = usability\n        self.friendly = friendly\n        self.id = None\n        if object_id is None:\n            self.id = self.infer_id()\n        else:\n            self.id = int(object_id, base=16)\n            whitelist = WHITELISTED_MISMATCHING_IDS[0] |\\\n                WHITELISTED_MISMATCHING_IDS.get(layer, set())\n\n            if self.fullname not in whitelist:\n                assert self.id == self.infer_id(),\\\n                    'Invalid inferred ID for ' + repr(self)\n\n        self.class_name = snake_to_camel_case(\n            self.name, suffix='Request' if self.is_function else '')\n\n        self.real_args = list(a for a in self.sorted_args() if not\n                              (a.flag_indicator or a.generic_definition))\n\n    @property\n    def innermost_result(self):\n        index = self.result.find('<')\n        if index == -1:\n            return self.result\n        else:\n            return self.result[index + 1:-1]\n\n    def sorted_args(self):\n        \"\"\"Returns the arguments properly sorted and ready to plug-in\n           into a Python's method header (i.e., flags and those which\n           can be inferred will go last so they can default =None)\n        \"\"\"\n        return sorted(self.args,\n                      key=lambda x: bool(x.flag) or x.can_be_inferred)\n\n    def __repr__(self, ignore_id=False):\n        if self.id is None or ignore_id:\n            hex_id = ''\n        else:\n            hex_id = '#{:08x}'.format(self.id)\n\n        if self.args:\n            args = ' ' + ' '.join([repr(arg) for arg in self.args])\n        else:\n            args = ''\n\n        return '{}{}{} = {}'.format(self.fullname, hex_id, args, self.result)\n\n    def infer_id(self):\n        representation = self.__repr__(ignore_id=True)\n        representation = representation\\\n            .replace(':bytes ', ':string ')\\\n            .replace('?bytes ', '?string ')\\\n            .replace('<', ' ').replace('>', '')\\\n            .replace('{', '').replace('}', '')\n\n        # Remove optional empty values (special-cased to the true type)\n        representation = re.sub(\n            r' \\w+:\\w+\\.\\d+\\?true',\n            r'',\n            representation\n        )\n        return zlib.crc32(representation.encode('ascii'))\n\n    def to_dict(self):\n        return {\n            'id':\n                str(struct.unpack('i', struct.pack('I', self.id))[0]),\n            'method' if self.is_function else 'predicate':\n                self.fullname,\n            'params':\n                [x.to_dict() for x in self.args if not x.generic_definition],\n            'type':\n                self.result\n        }\n\n    def is_good_example(self):\n        return not self.class_name.endswith('Empty')\n\n    def as_example(self, f, indent=0):\n        f.write('functions' if self.is_function else 'types')\n        if self.namespace:\n            f.write('.')\n            f.write(self.namespace)\n\n        f.write('.')\n        f.write(self.class_name)\n        f.write('(')\n\n        args = [arg for arg in self.real_args if not arg.omit_example()]\n        if not args:\n            f.write(')')\n            return\n\n        f.write('\\n')\n        indent += 1\n        remaining = len(args)\n        for arg in args:\n            remaining -= 1\n            f.write('    ' * indent)\n            f.write(arg.name)\n            f.write('=')\n            if arg.is_vector:\n                f.write('[')\n            arg.as_example(f, indent)\n            if arg.is_vector:\n                f.write(']')\n            if remaining:\n                f.write(',')\n            f.write('\\n')\n\n        indent -= 1\n        f.write('    ' * indent)\n        f.write(')')\n"
  },
  {
    "path": "telethon_generator/sourcebuilder.py",
    "content": "class SourceBuilder:\n    \"\"\"This class should be used to build .py source files\"\"\"\n\n    def __init__(self, out_stream, indent_size=4):\n        self.current_indent = 0\n        self.on_new_line = False\n        self.indent_size = indent_size\n        self.out_stream = out_stream\n\n        # Was a new line added automatically before? If so, avoid it\n        self.auto_added_line = False\n\n    def indent(self):\n        \"\"\"Indents the current source code line\n           by the current indentation level\n        \"\"\"\n        self.write(' ' * (self.current_indent * self.indent_size))\n\n    def write(self, string, *args, **kwargs):\n        \"\"\"Writes a string into the source code,\n           applying indentation if required\n        \"\"\"\n        if self.on_new_line:\n            self.on_new_line = False  # We're not on a new line anymore\n            # If the string was not empty, indent; Else probably a new line\n            if string.strip():\n                self.indent()\n\n        if args or kwargs:\n            self.out_stream.write(string.format(*args, **kwargs))\n        else:\n            self.out_stream.write(string)\n\n    def writeln(self, string='', *args, **kwargs):\n        \"\"\"Writes a string into the source code _and_ appends a new line,\n           applying indentation if required\n        \"\"\"\n        self.write(string + '\\n', *args, **kwargs)\n        self.on_new_line = True\n\n        # If we're writing a block, increment indent for the next time\n        if string and string[-1] == ':':\n            self.current_indent += 1\n\n        # Clear state after the user adds a new line\n        self.auto_added_line = False\n\n    def end_block(self):\n        \"\"\"Ends an indentation block, leaving an empty line afterwards\"\"\"\n        self.current_indent -= 1\n\n        # If we did not add a new line automatically yet, now it's the time!\n        if not self.auto_added_line:\n            self.writeln()\n            self.auto_added_line = True\n\n    def __str__(self):\n        self.out_stream.seek(0)\n        return self.out_stream.read()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.out_stream.close()\n"
  },
  {
    "path": "telethon_generator/syncerrors.py",
    "content": "# Should be fed with the JSON obtained from https://core.telegram.org/api/errors#error-database\nimport re\nimport csv\nimport sys\nimport json\nfrom pathlib import Path\n\nsys.path.insert(0, '..')\n\nfrom telethon_generator.parsers.errors import parse_errors, Error\nfrom telethon_generator.parsers.methods import parse_methods, MethodInfo\n\nERRORS = Path('data/errors.csv')\nMETHODS = Path('data/methods.csv')\nFRIENDLY = Path('data/friendly.csv')\n\n\ndef main():\n    new_errors = []\n    new_methods = []\n\n    self_errors = {e.str_code: e for e in parse_errors(ERRORS)}\n    self_methods = {m.name: m for m in parse_methods(METHODS, FRIENDLY, self_errors)}\n\n    tg_data = json.load(sys.stdin)\n\n    def get_desc(code):\n        return re.sub(r'\\s*&\\w+;\\s*', '', (tg_data['descriptions'].get(code) or '').rstrip('.'))\n\n    for int_code, errors in tg_data['errors'].items():\n        int_code = int(int_code)  # json does not support non-string keys\n        for code, methods in errors.items():\n            if not re.match(r'\\w+', code):\n                continue  # skip, full code is unknown (contains asterisk or is multiple words)\n            str_code = code.replace('%d', 'X')\n            if error := self_errors.get(str_code):\n                error.int_codes.append(int_code)  # de-duplicated once later\n                if not error.description:  # prefer our descriptions\n                    if not error.has_captures:  # need descriptions with specific text if error has captures\n                        error.description = get_desc(code)\n            else:\n                self_errors[str_code] = Error([int_code], str_code, get_desc(code))\n\n    new_errors.extend((e.str_code, ' '.join(map(str, sorted(set(e.int_codes)))), e.description) for e in self_errors.values())\n    new_methods.extend((m.name, m.usability.key, ' '.join(sorted(e.str_code for e in m.errors))) for m in self_methods.values())\n\n    csv.register_dialect('plain', lineterminator='\\n')\n    with ERRORS.open('w', encoding='utf-8', newline='') as fd:\n        csv.writer(fd, 'plain').writerows((('name', 'codes', 'description'), *sorted(new_errors)))\n    with METHODS.open('w', encoding='utf-8', newline='') as fd:\n        csv.writer(fd, 'plain').writerows((('method', 'usability', 'errors'), *sorted(new_methods)))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "telethon_generator/utils.py",
    "content": "import re\n\n\ndef snake_to_camel_case(name, suffix=None):\n    # Courtesy of http://stackoverflow.com/a/31531797/4759433\n    result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)\n    result = result[:1].upper() + result[1:].replace('_', '')\n    return result + suffix if suffix else result\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/readthedocs/__init__.py",
    "content": ""
  },
  {
    "path": "tests/readthedocs/conftest.py",
    "content": "import pathlib\n\nimport pytest\n\n\n@pytest.fixture\ndef docs_dir():\n    return pathlib.Path('readthedocs')\n"
  },
  {
    "path": "tests/readthedocs/quick_references/__init__.py",
    "content": ""
  },
  {
    "path": "tests/readthedocs/quick_references/test_client_reference.py",
    "content": "import re\n\nfrom telethon import TelegramClient\n\n\ndef test_all_methods_present(docs_dir):\n    with (docs_dir / 'quick-references/client-reference.rst').open(encoding='utf-8') as fd:\n        present_methods = set(map(str.lstrip, re.findall(r'^ {4}\\w+$', fd.read(), re.MULTILINE)))\n\n    assert len(present_methods) > 0\n    for name in dir(TelegramClient):\n        attr = getattr(TelegramClient, name)\n        if callable(attr) and not name.startswith('_') and name != 'sign_up':\n            assert name in present_methods\n"
  },
  {
    "path": "tests/telethon/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/client/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/client/test_messages.py",
    "content": "import inspect\nfrom unittest import mock\nfrom unittest.mock import MagicMock\n\nimport pytest\n\nfrom telethon import TelegramClient\nfrom telethon.client import MessageMethods\nfrom telethon.tl.types import PeerChat, MessageMediaDocument, Message, MessageEntityBold\n\n\n@pytest.mark.asyncio\nasync def test_send_message_with_file_forwards_args():\n    arguments = {}\n    sentinel = object()\n\n    for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters):\n        if name in {'self', 'entity', 'file'}:\n            continue  # positional\n\n        if name in {'message'}:\n            continue  # renamed\n\n        if name in {'link_preview'}:\n            continue  # make no sense in send_file\n\n        arguments[name] = value\n\n    class MockedClient(TelegramClient):\n        # noinspection PyMissingConstructor\n        def __init__(self):\n            pass\n\n        async def send_file(self, entity, file, **kwargs):\n            assert entity == 'a'\n            assert file == 'b'\n            for k, v in arguments.items():\n                assert k in kwargs\n                assert kwargs[k] == v\n\n            return sentinel\n\n    client = MockedClient()\n    assert (await client.send_message('a', file='b', **arguments)) == sentinel\n\n\nclass TestMessageMethods:\n    @pytest.mark.asyncio\n    @pytest.mark.parametrize(\n        'formatting_entities',\n        ([MessageEntityBold(offset=0, length=0)], None)\n    )\n    async def test_send_msg_and_file(self, formatting_entities):\n        async def async_func(result): # AsyncMock was added only in 3.8\n            return result\n        msg_methods = MessageMethods()\n        expected_result = Message(\n            id=0, peer_id=PeerChat(chat_id=0), message='', date=None,\n        )\n        entity = 'test_entity'\n        message = Message(\n            id=1, peer_id=PeerChat(chat_id=0), message='expected_caption', date=None,\n            entities=[MessageEntityBold(offset=9, length=9)],\n        )\n        media_file = MessageMediaDocument()\n\n        with mock.patch.object(\n            target=MessageMethods, attribute='send_file',\n            new=MagicMock(return_value=async_func(expected_result)), create=True,\n        ) as mock_obj:\n            result = await msg_methods.send_message(\n                entity=entity, message=message, file=media_file,\n                formatting_entities=formatting_entities,\n            )\n            mock_obj.assert_called_once_with(\n                entity, media_file, caption=message.message,\n                formatting_entities=formatting_entities or message.entities,\n                reply_to=None, silent=None, attributes=None, parse_mode=(),\n                force_document=False, thumb=None, buttons=None,\n                clear_draft=False, schedule=None, supports_streaming=False,\n                comment_to=None, background=None, nosound_video=None,\n                send_as=None, message_effect_id=None,\n            )\n            assert result == expected_result\n"
  },
  {
    "path": "tests/telethon/crypto/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/crypto/test_rsa.py",
    "content": "\"\"\"\nTests for `telethon.crypto.rsa`.\n\"\"\"\nimport pytest\n\nfrom telethon.crypto import rsa\n\n\n@pytest.fixture\ndef server_key_fp():\n    \"\"\"Factory to return a key, old if so chosen.\"\"\"\n    def _server_key_fp(old: bool):\n        for fp, data in rsa._server_keys.items():\n            _, old_key = data\n            if old_key == old:\n                return fp\n\n    return _server_key_fp\n\n\ndef test_encryption_inv_key():\n    \"\"\"Test for #1324.\"\"\"\n    assert rsa.encrypt(\"invalid\", b\"testdata\") is None\n\n\ndef test_encryption_old_key(server_key_fp):\n    \"\"\"Test for #1324.\"\"\"\n    assert rsa.encrypt(server_key_fp(old=True), b\"testdata\") is None\n\n\ndef test_encryption_allowed_old_key(server_key_fp):\n    data = rsa.encrypt(server_key_fp(old=True), b\"testdata\", use_old=True)\n    # We can't verify the data is actually valid because we don't have\n    # the decryption keys\n    assert data is not None and len(data) == 256\n\n\ndef test_encryption_current_key(server_key_fp):\n    data = rsa.encrypt(server_key_fp(old=False), b\"testdata\")\n    # We can't verify the data is actually valid because we don't have\n    # the decryption keys\n    assert data is not None and len(data) == 256\n"
  },
  {
    "path": "tests/telethon/events/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/events/test_chataction.py",
    "content": "import pytest\n\nfrom telethon import TelegramClient, events, types, utils\n\n\ndef get_client():\n    return TelegramClient(None, 1, '1')\n\n\ndef get_user_456():\n    return types.User(\n        id=456,\n        access_hash=789,\n        first_name='User 123'\n    )\n\n\n@pytest.mark.asyncio\nasync def test_get_input_users_no_action_message_no_entities():\n    event = events.ChatAction.build(types.UpdateChatParticipantDelete(\n        chat_id=123,\n        user_id=456,\n        version=1\n    ))\n    event._set_client(get_client())\n\n    assert await event.get_input_users() == []\n\n\n@pytest.mark.asyncio\nasync def test_get_input_users_no_action_message():\n    user = get_user_456()\n    event = events.ChatAction.build(types.UpdateChatParticipantDelete(\n        chat_id=123,\n        user_id=456,\n        version=1\n    ))\n    event._set_client(get_client())\n    event._entities[user.id] = user\n\n    assert await event.get_input_users() == [utils.get_input_peer(user)]\n\n\n@pytest.mark.asyncio\nasync def test_get_users_no_action_message_no_entities():\n    event = events.ChatAction.build(types.UpdateChatParticipantDelete(\n        chat_id=123,\n        user_id=456,\n        version=1\n    ))\n    event._set_client(get_client())\n\n    assert await event.get_users() == []\n\n\n@pytest.mark.asyncio\nasync def test_get_users_no_action_message():\n    user = get_user_456()\n    event = events.ChatAction.build(types.UpdateChatParticipantDelete(\n        chat_id=123,\n        user_id=456,\n        version=1\n    ))\n    event._set_client(get_client())\n    event._entities[user.id] = user\n\n    assert await event.get_users() == [user]\n"
  },
  {
    "path": "tests/telethon/extensions/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/extensions/test_html.py",
    "content": "\"\"\"\nTests for `telethon.extensions.html`.\n\"\"\"\nfrom telethon.extensions import html\nfrom telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl\n\n\ndef test_entity_edges():\n    \"\"\"\n    Test that entities at the edges (start and end) don't crash.\n    \"\"\"\n    text = 'Hello, world'\n    entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)]\n    result = html.unparse(text, entities)\n    assert result == '<strong>Hello</strong>, <strong>world</strong>'\n\n\ndef test_malformed_entities():\n    \"\"\"\n    Test that malformed entity offsets from bad clients\n    don't crash and produce the expected results.\n    \"\"\"\n    text = '🏆Telegram Official Android Challenge is over🏆.'\n    entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]\n    result = html.unparse(text, entities)\n    assert result == '🏆<a href=\"https://example.com\">Telegram Official Android Challenge is over</a>🏆.'\n\n\ndef test_trailing_malformed_entities():\n    \"\"\"\n    Similar to `test_malformed_entities`, but for the edge\n    case where the malformed entity offset is right at the end\n    (note the lack of a trailing dot in the text string).\n    \"\"\"\n    text = '🏆Telegram Official Android Challenge is over🏆'\n    entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]\n    result = html.unparse(text, entities)\n    assert result == '🏆<a href=\"https://example.com\">Telegram Official Android Challenge is over</a>🏆'\n\n\ndef test_entities_together():\n    \"\"\"\n    Test that an entity followed immediately by a different one behaves well.\n    \"\"\"\n    original = '<strong>⚙️</strong><em>Settings</em>'\n    stripped = '⚙️Settings'\n\n    text, entities = html.parse(original)\n    assert text == stripped\n    assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)]\n\n    text = html.unparse(text, entities)\n    assert text == original\n\n\ndef test_nested_entities():\n    \"\"\"\n    Test that an entity nested inside another one behaves well.\n    \"\"\"\n    original = '<a href=\"https://example.com\"><strong>Example</strong></a>'\n    original_entities = [MessageEntityTextUrl(0, 7, url='https://example.com'), MessageEntityBold(0, 7)]\n    stripped = 'Example'\n\n    text, entities = html.parse(original)\n    assert text == stripped\n    assert entities == original_entities\n\n    text = html.unparse(text, entities)\n    assert text == original\n\n\ndef test_offset_at_emoji():\n    \"\"\"\n    Tests that an entity starting at a emoji preserves the emoji.\n    \"\"\"\n    text = 'Hi\\n👉 See example'\n    entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)]\n    parsed = '<strong>Hi</strong>\\n<em>👉</em> See <strong>example</strong>'\n\n    assert html.parse(parsed) == (text, entities)\n    assert html.unparse(text, entities) == parsed\n"
  },
  {
    "path": "tests/telethon/extensions/test_markdown.py",
    "content": "\"\"\"\nTests for `telethon.extensions.markdown`.\n\"\"\"\nfrom telethon.extensions import markdown\nfrom telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl\n\n\ndef test_entity_edges():\n    \"\"\"\n    Test that entities at the edges (start and end) don't crash.\n    \"\"\"\n    text = 'Hello, world'\n    entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)]\n    result = markdown.unparse(text, entities)\n    assert result == '**Hello**, **world**'\n\n\ndef test_malformed_entities():\n    \"\"\"\n    Test that malformed entity offsets from bad clients\n    don't crash and produce the expected results.\n    \"\"\"\n    text = '🏆Telegram Official Android Challenge is over🏆.'\n    entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]\n    result = markdown.unparse(text, entities)\n    assert result == \"🏆[Telegram Official Android Challenge is over](https://example.com)🏆.\"\n\n\ndef test_trailing_malformed_entities():\n    \"\"\"\n    Similar to `test_malformed_entities`, but for the edge\n    case where the malformed entity offset is right at the end\n    (note the lack of a trailing dot in the text string).\n    \"\"\"\n    text = '🏆Telegram Official Android Challenge is over🏆'\n    entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]\n    result = markdown.unparse(text, entities)\n    assert result == \"🏆[Telegram Official Android Challenge is over](https://example.com)🏆\"\n\n\ndef test_entities_together():\n    \"\"\"\n    Test that an entity followed immediately by a different one behaves well.\n    \"\"\"\n    original = '**⚙️**__Settings__'\n    stripped = '⚙️Settings'\n\n    text, entities = markdown.parse(original)\n    assert text == stripped\n    assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)]\n\n    text = markdown.unparse(text, entities)\n    assert text == original\n\n\ndef test_nested_entities():\n    \"\"\"\n    Test that an entity nested inside another one behaves well.\n    \"\"\"\n    original = '**[Example](https://example.com)**'\n    stripped = 'Example'\n\n    text, entities = markdown.parse(original)\n    assert text == stripped\n    assert entities == [MessageEntityBold(0, 7), MessageEntityTextUrl(0, 7, url='https://example.com')]\n\n    text = markdown.unparse(text, entities)\n    assert text == original\n\n\ndef test_offset_at_emoji():\n    \"\"\"\n    Tests that an entity starting at a emoji preserves the emoji.\n    \"\"\"\n    text = 'Hi\\n👉 See example'\n    entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)]\n    parsed = '**Hi**\\n__👉__ See **example**'\n\n    assert markdown.parse(parsed) == (text, entities)\n    assert markdown.unparse(text, entities) == parsed\n"
  },
  {
    "path": "tests/telethon/test_helpers.py",
    "content": "\"\"\"\ntests for telethon.helpers\n\"\"\"\n\nimport asyncio\nfrom base64 import b64decode\n\nimport pytest\n\nfrom telethon import helpers\nfrom telethon.utils import get_inner_text\nfrom telethon.tl.types import MessageEntityUnknown as Meu\n\n\ndef test_strip_text():\n    text = ' text '\n    text_stripped = 'text'\n    entities_before_and_after = (\n        ([], []),\n        ([Meu(i, 0) for i in range(10)], []),  # del ''\n        ([Meu(0, 0), Meu(0, 1), Meu(5, 1)], []),  # del '', ' ', ' '\n        ([Meu(0, 3)], [Meu(0, 2)]),  # ' te' -> 'te'\n        ([Meu(3, 1)], [Meu(2, 1)]),  # 'x'\n        ([Meu(3, 2)], [Meu(2, 2)]),  # 'xt'\n        ([Meu(3, 3)], [Meu(2, 2)]),  # 'xt ' -> 'xt'\n        ([Meu(0, 6)], [Meu(0, 4)]),  # ' text ' -> 'text'\n    )\n    for entities_before, entities_expected in entities_before_and_after:\n        entities_for_test = [Meu(meu.offset, meu.length) for meu in entities_before]  # deep copy\n        text_after = helpers.strip_text(text, entities_for_test)\n        assert text_after == text_stripped\n        assert sorted((e.offset, e.length) for e in entities_for_test) \\\n               == sorted((e.offset, e.length) for e in entities_expected)\n        inner_text_before = get_inner_text(text, entities_before)\n        inner_text_before_stripped = [t.strip() for t in inner_text_before]\n        inner_text_after = get_inner_text(text_after, entities_for_test)\n        for t in inner_text_after:\n            assert t in inner_text_before_stripped\n\n\nclass TestSyncifyAsyncContext:\n    class NoopContextManager:\n        def __init__(self, loop=None):\n            self.count = 0\n            self.loop = loop or helpers.get_running_loop()\n\n        async def __aenter__(self):\n            self.count += 1\n            return self\n\n        async def __aexit__(self, exc_type, *args):\n            assert exc_type is None\n            self.count -= 1\n\n        __enter__ = helpers._sync_enter\n        __exit__ = helpers._sync_exit\n\n    def test_sync_acontext(self):\n        contm = self.NoopContextManager(loop=asyncio.new_event_loop())\n        assert contm.count == 0\n\n        with contm:\n            assert contm.count == 1\n\n        assert contm.count == 0\n\n    @pytest.mark.asyncio\n    async def test_async_acontext(self):\n        contm = self.NoopContextManager()\n        assert contm.count == 0\n\n        async with contm:\n            assert contm.count == 1\n\n        assert contm.count == 0\n\n\ndef test_generate_key_data_from_nonce():\n    gkdfn = helpers.generate_key_data_from_nonce\n\n    key_expect = b64decode(b'NFwRFB8Knw/kAmvPWjtrQauWysHClVfQh0UOAaABqZA=')\n    nonce_expect = b64decode(b'1AgjhU9eDvJRjFik73bjR2zZEATzL/jLu9yodYfWEgA=')\n    assert gkdfn(123456789, 1234567) == (key_expect, nonce_expect)\n"
  },
  {
    "path": "tests/telethon/test_pickle.py",
    "content": "import pickle\n\nfrom telethon.errors import RPCError, BadRequestError, FileIdInvalidError, NetworkMigrateError\n\n\ndef _assert_equality(error, unpickled_error):\n    assert error.code == unpickled_error.code\n    assert error.message == unpickled_error.message\n    assert type(error) == type(unpickled_error)\n    assert str(error) == str(unpickled_error)\n\n\ndef test_base_rpcerror_pickle():\n    error = RPCError(\"request\", \"message\", 123)\n    unpickled_error = pickle.loads(pickle.dumps(error))\n    _assert_equality(error, unpickled_error)\n\n\ndef test_rpcerror_pickle():\n    error = BadRequestError(\"request\", \"BAD_REQUEST\", 400)\n    unpickled_error = pickle.loads(pickle.dumps(error))\n    _assert_equality(error, unpickled_error)\n\n\ndef test_fancy_rpcerror_pickle():\n    error = FileIdInvalidError(\"request\")\n    unpickled_error = pickle.loads(pickle.dumps(error))\n    _assert_equality(error, unpickled_error)\n\n\ndef test_fancy_rpcerror_capture_pickle():\n    error = NetworkMigrateError(request=\"request\", capture=5)\n    unpickled_error = pickle.loads(pickle.dumps(error))\n    _assert_equality(error, unpickled_error)\n    assert error.new_dc == unpickled_error.new_dc\n"
  },
  {
    "path": "tests/telethon/test_utils.py",
    "content": "import io\nimport pathlib\n\nimport pytest\n\nfrom telethon import utils\nfrom telethon.tl.types import (\n    MessageMediaGame, Game, PhotoEmpty\n)\n\n\ndef test_game_input_media_memory_error():\n    large_long = 2**62\n    media = MessageMediaGame(Game(\n        id=large_long,  # <- key to trigger `MemoryError`\n        access_hash=large_long,\n        short_name='short_name',\n        title='title',\n        description='description',\n        photo=PhotoEmpty(large_long),\n    ))\n    input_media = utils.get_input_media(media)\n    bytes(input_media)  # <- shouldn't raise `MemoryError`\n\n\ndef test_private_get_extension():\n    # Positive cases\n    png_header = bytes.fromhex('89 50 4e 47 0d 0a 1a 0a  00 00 00 0d 49 48 44 52')\n    png_buffer = io.BytesIO(png_header)\n\n    class CustomFd:\n        def __init__(self, name):\n            self.name = name\n\n    assert utils._get_extension('foo.bar.baz') == '.baz'\n    assert utils._get_extension(pathlib.Path('foo.bar.baz')) == '.baz'\n    assert utils._get_extension(CustomFd('foo.bar.baz')) == '.baz'\n\n    # Negative cases\n    null_header = bytes.fromhex('00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00')\n    null_buffer = io.BytesIO(null_header)\n\n    empty_header = bytes()\n    empty_buffer = io.BytesIO(empty_header)\n\n    assert utils._get_extension('foo') == ''\n    assert utils._get_extension(pathlib.Path('foo')) == ''\n    assert utils._get_extension(null_header) == ''\n    assert utils._get_extension(null_buffer) == ''\n    assert utils._get_extension(null_buffer) == ''  # make sure it did seek back\n    assert utils._get_extension(empty_header) == ''\n    assert utils._get_extension(empty_buffer) == ''\n    assert utils._get_extension(empty_buffer) == ''  # make sure it did seek back\n    assert utils._get_extension(CustomFd('foo')) == ''\n\n\ndef test_rle_encode_trailing_zeros():\n    assert utils._rle_encode(b'\\x12\\x00\\x00\\x00\\x00') == b'\\x12\\x00\\x04'\n"
  },
  {
    "path": "tests/telethon/tl/__init__.py",
    "content": ""
  },
  {
    "path": "tests/telethon/tl/test_serialization.py",
    "content": "import pytest\n\nfrom telethon.tl import types, functions\n\n\ndef test_nested_invalid_serialization():\n    large_long = 2**62\n    request = functions.account.SetPrivacyRequest(\n        key=types.InputPrivacyKeyChatInvite(),\n        rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])]\n    )\n    with pytest.raises(TypeError):\n        bytes(request)\n"
  },
  {
    "path": "update-docs.sh",
    "content": "#!/bin/bash\n\nset -e\npython setup.py gen docs\nrm -rf /tmp/docs\nmv docs/ /tmp/docs\ngit checkout gh-pages\n# there's probably better ways but we know none has spaces\nrm -rf $(ls /tmp/docs)\nmv /tmp/docs/* .\ngit add constructors/ types/ methods/ index.html js/search.js css/ img/\ngit commit --amend -m \"Update documentation\"\ngit push --force\ngit checkout v1\n"
  }
]