diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index dc7a26c2..e69de29b 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,27 +0,0 @@ ---- -name: Bug Report -about: Create a report about a bug inside the library or issues with the documentation -title: '' -labels: '' -assignees: '' - ---- - -**Checklist** -* [ ] The error is in the library's code, and not in my own. -* [ ] I have searched for this issue before posting it and there isn't a duplicate. -* [ ] I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip` and triggered the bug in the latest version. - -**Code that causes the issue** -```python -from telethon.sync import TelegramClient -... - -``` - -**Traceback** -``` -Traceback (most recent call last): - File "code.py", line 1, in - -``` diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index dc38f2db..e69de29b 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,10 +0,0 @@ ---- -name: Feature Request -about: Suggest ideas, changes or other enhancements for the library -title: '' -labels: enhancement -assignees: '' - ---- - -Please describe your idea. Would you like another friendly method? Renaming them to something more appropriated? Changing the way something works? diff --git a/.github/ISSUE_TEMPLATE/question-about-usage.md b/.github/ISSUE_TEMPLATE/question-about-usage.md index e357fb81..e69de29b 100644 --- a/.github/ISSUE_TEMPLATE/question-about-usage.md +++ b/.github/ISSUE_TEMPLATE/question-about-usage.md @@ -1,20 +0,0 @@ ---- -name: Question about Usage -about: QUESTIONS DON'T BELONG HERE. Ask in StackOverflow or in @TelethonUpdates -title: '' -labels: RTFM -assignees: '' - ---- - -QUESTIONS ARE NEITHER BUGS NOR ENHANCEMENTS AND DON'T BELONG HERE. - -If you DO have a question, ask in: -* https://stackoverflow.com or -* https://t.me/TelethonUpdates (@TelethonUpdates channel in Telegram) - -If you do post a question, it will be labelled "RTFM" and closed as soon as possible without any answer. - -If you DON'T have a question, use the right template for bugs/issues with the library or to propose an improvement/enhancement to either the code or documentation. - -We are not being harsh. Only clear. The issues section is not for questions, and people keep asking things over and over, which is a waste of everyone's time. diff --git a/.gitignore b/.gitignore index a40abb8a..e69de29b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,108 +0,0 @@ -# Docs -_build/ -docs/ - -# Generated code -telethon/tl/functions/ -telethon/tl/types/ -telethon/tl/patched/ -telethon/tl/alltlobjects.py -telethon/errors/rpcerrorlist.py - -# User session -*.session -usermedia/ - -# Quick tests should live in this file -example.py - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -env/ -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*,cover -.hypothesis/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# IPython Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# dotenv -.env - -# virtualenv -.venv/ -venv/ -ENV/ - -# Spyder project settings -.spyderproject - -# Rope project settings -.ropeproject diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49f7e027..e69de29b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +0,0 @@ -- repo: git://github.com/pre-commit/pre-commit-hooks - sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469 - hooks: - - id: check-added-large-files - - id: check-case-conflict - - id: check-merge-conflict - - id: check-symlinks - - id: check-yaml - - id: double-quote-string-fixer - - id: end-of-file-fixer - - id: name-tests-test - - id: trailing-whitespace -- repo: git://github.com/pre-commit/mirrors-yapf - sha: v0.11.1 - hooks: - - id: yapf -- repo: git://github.com/FalconSocial/pre-commit-python-sorter - sha: 1.0.4 - hooks: - - id: python-import-sorter - args: - - --silent-overwrite diff --git a/LICENSE b/LICENSE index 7a430dd1..e69de29b 100755 --- a/LICENSE +++ b/LICENSE @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016-2019 LonamiWebs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in index 0eed5bd4..e69de29b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +0,0 @@ -include LICENSE -include README.rst - -recursive-include telethon * diff --git a/README.rst b/README.rst index f1eb902c..e69de29b 100755 --- a/README.rst +++ b/README.rst @@ -1,83 +0,0 @@ -Telethon -======== -.. epigraph:: - - ⭐️ Thanks **everyone** who has starred the project, it means a lot! - -|logo| **Telethon** is an asyncio_ **Python 3** -MTProto_ library to interact with Telegram_'s API -as a user or through a bot account (bot API alternative). - -.. important:: - - If you have code using Telethon before its 1.0 version, you must - read `Compatibility and Convenience`_ to learn how to migrate. - -What is this? -------------- - -Telegram is a popular messaging application. This library is meant -to make it easy for you to write Python programs that can interact -with Telegram. Think of it as a wrapper that has already done the -heavy job for you, so you can focus on developing an application. - - -Installing ----------- - -.. code-block:: sh - - pip3 install telethon - - -Creating a client ------------------ - -.. code-block:: python - - from telethon import TelegramClient, events, sync - - # These example values won't work. You must get your own api_id and - # api_hash from https://my.telegram.org, under API Development. - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - - client = TelegramClient('session_name', api_id, api_hash) - client.start() - - -Doing stuff ------------ - -.. code-block:: python - - print(client.get_me().stringify()) - - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') - - client.download_profile_photo('me') - messages = client.get_messages('username') - messages[0].download_media() - - @client.on(events.NewMessage(pattern='(?i)hi|hello')) - async def handler(event): - await event.respond('Hey!') - - -Next steps ----------- - -Do you like how Telethon looks? Check out `Read The Docs`_ for a more -in-depth explanation, with examples, troubleshooting issues, and more -useful information. - -.. _asyncio: https://docs.python.org/3/library/asyncio.html -.. _MTProto: https://core.telegram.org/mtproto -.. _Telegram: https://telegram.org -.. _Compatibility and Convenience: https://docs.telethon.dev/en/latest/misc/compatibility-and-convenience.html -.. _Read The Docs: https://docs.telethon.dev - -.. |logo| image:: logo.svg - :width: 24pt - :height: 24pt diff --git a/logo.svg b/logo.svg index 4131179b..e69de29b 100644 --- a/logo.svg +++ b/logo.svg @@ -1,6 +0,0 @@ - - - - - - diff --git a/optional-requirements.txt b/optional-requirements.txt index aeaf3994..e69de29b 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,4 +0,0 @@ -cryptg -pysocks -hachoir3 -pillow diff --git a/readthedocs/Makefile b/readthedocs/Makefile index fd6e0d0a..e69de29b 100644 --- a/readthedocs/Makefile +++ b/readthedocs/Makefile @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = Telethon -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/readthedocs/basic/installation.rst b/readthedocs/basic/installation.rst index b9c8b399..e69de29b 100644 --- a/readthedocs/basic/installation.rst +++ b/readthedocs/basic/installation.rst @@ -1,93 +0,0 @@ -.. _installation: - -============ -Installation -============ - -Telethon is a Python library, which means you need to download and install -Python from https://www.python.org/downloads/ if you haven't already. Once -you have Python installed, run: - -.. code-block:: sh - - pip3 install -U telethon --user - -To install or upgrade the library to the latest version. - - -Installing Development Versions -=============================== - -If you want the *latest* unreleased changes, -you can run the following command instead: - -.. code-block:: sh - - pip3 install -U https://github.com/LonamiWebs/Telethon/archive/master.zip --user - -.. note:: - - The development version may have bugs and is not recommended for production - use. However, when you are `reporting a library bug`__, you should try if the - bug still occurs in this version. - -.. __: https://github.com/LonamiWebs/Telethon/issues/ - - -Verification -============ - -To verify that the library is installed correctly, run the following command: - -.. code-block:: sh - - python3 -c "import telethon; print(telethon.__version__)" - -The version number of the library should show in the output. - - -Optional Dependencies -===================== - -If cryptg_ is installed, **the library will work a lot faster**, since -encryption and decryption will be made in C instead of Python. If your -code deals with a lot of updates or you are downloading/uploading a lot -of files, you will notice a considerable speed-up (from a hundred kilobytes -per second to several megabytes per second, if your connection allows it). -If it's not installed, pyaes_ will be used (which is pure Python, so it's -much slower). - -If pillow_ is installed, large images will be automatically resized when -sending photos to prevent Telegram from failing with "invalid image". -Official clients also do this. - -If aiohttp_ is installed, the library will be able to download -:tl:`WebDocument` media files (otherwise you will get an error). - -If hachoir_ is installed, it will be used to extract metadata from files -when sending documents. Telegram uses this information to show the song's -performer, artist, title, duration, and for videos too (including size). -Otherwise, they will default to empty values, and you can set the attributes -manually. - -.. note:: - - Some of the modules may require additional dependencies before being - installed through ``pip``. If you have an ``apt``-based system, consider - installing the most commonly missing dependencies: - - .. code-block:: sh - - apt update - apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev - pip install -U --user setuptools - pip install -U --user telethon cryptg pillow - - Thanks to `@bb010g`_ for writing down this nice list. - -.. _cryptg: https://github.com/cher-nov/cryptg -.. _pyaes: https://github.com/ricmoo/pyaes -.. _pillow: https://python-pillow.org -.. _aiohttp: https://docs.aiohttp.org -.. _hachoir: https://hachoir.readthedocs.io -.. _@bb010g: https://static.bb010g.com diff --git a/readthedocs/basic/next-steps.rst b/readthedocs/basic/next-steps.rst index 7cecdc9a..e69de29b 100644 --- a/readthedocs/basic/next-steps.rst +++ b/readthedocs/basic/next-steps.rst @@ -1,22 +0,0 @@ -========== -Next Steps -========== - -These basic first steps should have gotten you started with the library. - -By now, you should know how to call friendly methods and how to work with -the returned objects, how things work inside event handlers, etc. - -Next, we will see a quick reference summary of *all* the methods and -properties that you will need when using the library. If you follow -the links there, you will expand the documentation for the method -and property, with more examples on how to use them. - -Therefore, **you can find an example on every method** of the client -to learn how to use it, as well as a description of all the arguments. - -After that, we will go in-depth with some other important concepts -that are worth learning and understanding. - -From now on, you can keep pressing the "Next" button if you want, -or use the menu on the left, since some pages are quite lengthy. diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst index 40e99f6b..e69de29b 100644 --- a/readthedocs/basic/quick-start.rst +++ b/readthedocs/basic/quick-start.rst @@ -1,79 +0,0 @@ -=========== -Quick-Start -=========== - -Let's see a longer example to learn some of the methods that the library -has to offer. These are known as "friendly methods", and you should always -use these if possible. - -.. code-block:: python - - from telethon.sync import TelegramClient - - # Remember to use your own values from my.telegram.org! - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - - with TelegramClient('anon', api_id, api_hash) as client: - # Getting information about yourself - me = client.get_me() - - # "me" is an User object. You can pretty-print - # any Telegram object with the "stringify" method: - print(me.stringify()) - - # When you print something, you see a representation of it. - # You can access all attributes of Telegram objects with - # the dot operator. For example, to get the username: - username = me.username - print(username) - print(me.phone) - - # You can print all the dialogs/conversations that you are part of: - for dialog in client.iter_dialogs(): - print(dialog.name, 'has ID', dialog.id) - - # You can send messages to yourself... - client.send_message('me', 'Hello, myself!') - # ...to some chat ID - client.send_message(-100123456, 'Hello, group!') - # ...to your contacts - client.send_message('+34600123123', 'Hello, friend!') - # ...or even to any username - client.send_message('TelethonChat', 'Hello, Telethon!') - - # You can, of course, use markdown in your messages: - message = client.send_message( - 'me', - 'This message has **bold**, `code`, __italics__ and ' - 'a [nice website](https://lonamiwebs.github.io)!', - link_preview=False - ) - - # Sending a message returns the sent message object, which you can use - print(message.raw_text) - - # You can reply to messages directly if you have a message object - message.reply('Cool!') - - # Or send files, songs, documents, albums... - client.send_file('me', '/home/me/Pictures/holidays.jpg') - - # You can print the message history of any chat: - for message in client.iter_messages('me'): - print(message.id, message.text) - - # You can download media from messages, too! - # The method will return the path where the file was saved. - if message.photo: - path = message.download_media() - print('File saved to', path) - - -Here, we show how to sign in, get information about yourself, send -messages, files, getting chats, printing messages, and downloading -files. - -You should make sure that you understand what the code shown here -does, take note on how methods are called and used and so on before -proceeding. We will see all the available methods later on. diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst index 9614b623..e69de29b 100644 --- a/readthedocs/basic/signing-in.rst +++ b/readthedocs/basic/signing-in.rst @@ -1,179 +0,0 @@ -.. _signing-in: - -========== -Signing In -========== - -Before working with Telegram's API, you need to get your own API ID and hash: - -1. `Login to your Telegram account `_ with the - phone number of the developer account to use. - -2. Click under API Development tools. - -3. A *Create new application* window will appear. Fill in your application - details. There is no need to enter any *URL*, and only the first two - fields (*App title* and *Short name*) can currently be changed later. - -4. Click on *Create application* at the end. Remember that your - **API hash is secret** and Telegram won't let you revoke it. - Don't post it anywhere! - -.. note:: - - This API ID and hash is the one used by *your application*, not your - phone number. You can use this API ID and hash with *any* phone number - or even for bot accounts. - - -Editing the Code -================ - -This is a little introduction for those new to Python programming in general. - -We will write our code inside ``hello.py``, so you can use any text -editor that you like. To run the code, use ``python3 hello.py`` from -the terminal. - -.. important:: - - Don't call your script ``telethon.py``! Python will try to import - the client from there and it will fail with an error such as - "ImportError: cannot import name 'TelegramClient' ...". - - -Signing In -========== - -We can finally write some code to log into our account! - -.. code-block:: python - - from telethon.sync import TelegramClient - - # Use your own values from my.telegram.org - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - - # The first parameter is the .session file name (absolute paths allowed) - with TelegramClient('anon', api_id, api_hash) as client: - client.send_message('me', 'Hello, myself!') - - -In the first line, we import the class name so we can create an instance -of the client. Then, we define variables to store our API ID and hash -conveniently. - -At last, we create a new `TelegramClient ` -instance and call it ``client``. We can now use the client variable -for anything that we want, such as sending a message to ourselves. - -Using a ``with`` block is the preferred way to use the library. It will -automatically `start() ` the client, -logging or signing up if necessary. - -If the ``.session`` file already existed, it will not login -again, so be aware of this if you move or rename the file! - - -Signing In as a Bot Account -=========================== - -You can also use Telethon for your bots (normal bot accounts, not users). -You will still need an API ID and hash, but the process is very similar: - - -.. code-block:: python - - from telethon.sync import TelegramClient - - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - bot_token = '12345:0123456789abcdef0123456789abcdef - - # We have to manually call "start" if we want an explicit bot token - bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) - - # But then we can use the client instance as usual - with bot: - ... - - -To get a bot account, you need to talk -with `@BotFather `_. - - -Signing In behind a Proxy -========================= - -If you need to use a proxy to access Telegram, -you will need to `install PySocks`__ and then change: - -.. code-block:: python - - TelegramClient('anon', api_id, api_hash) - -with - -.. code-block:: python - - TelegramClient('anon', api_id, api_hash, proxy=(socks.SOCKS5, '127.0.0.1', 4444)) - -(of course, replacing the IP and port with the IP and port of the proxy). - -The ``proxy=`` argument should be a tuple, a list or a dict, -consisting of parameters described `in PySocks usage`__. - -.. __: https://github.com/Anorov/PySocks#installation -.. __: https://github.com/Anorov/PySocks#usage-1 - - -Using MTProto Proxies -===================== - -MTProto Proxies are Telegram's alternative to normal proxies, -and work a bit differently. The following protocols are available: - -* ``ConnectionTcpMTProxyAbridged`` -* ``ConnectionTcpMTProxyIntermediate`` -* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred) - -For now, you need to manually specify these special connection modes -if you want to use a MTProto Proxy. Your code would look like this: - -.. code-block:: python - - from telethon import TelegramClient, connection - # we need to change the connection ^^^^^^^^^^ - - client = TelegramClient( - 'anon', - api_id, - api_hash, - - # Use one of the available connection modes. - # Normally, this one works with most proxies. - connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, - - # Then, pass the proxy details as a tuple: - # (host name, port, proxy secret) - # - # If the proxy has no secret, the secret must be: - # '00000000000000000000000000000000' - proxy=('mtproxy.example.com', 2002, 'secret') - ) - -In future updates, we may make it easier to use MTProto Proxies -(such as avoiding the need to manually pass ``connection=``). - -In short, the same code above but without comments to make it clearer: - -.. code-block:: python - - from telethon import TelegramClient, connection - - client = TelegramClient( - 'anon', api_id, api_hash, - connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, - proxy=('mtproxy.example.com', 2002, 'secret') - ) diff --git a/readthedocs/basic/updates.rst b/readthedocs/basic/updates.rst index 6e22ae64..e69de29b 100644 --- a/readthedocs/basic/updates.rst +++ b/readthedocs/basic/updates.rst @@ -1,159 +0,0 @@ -======= -Updates -======= - -Updates are an important topic in a messaging platform like Telegram. -After all, you want to be notified when a new message arrives, when -a member joins, when someone starts typing, etc. -For that, you can use **events**. - -.. important:: - - It is strongly advised to enable logging when working with events, - since exceptions in event handlers are hidden by default. Please - add the following snippet to the very top of your file: - - .. code-block:: python - - import logging - logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s', - level=logging.WARNING) - - -Getting Started -=============== - -Let's start things with an example to automate replies: - -.. code-block:: python - - from telethon import TelegramClient, events - - client = TelegramClient('anon', api_id, api_hash) - - @client.on(events.NewMessage) - async def my_event_handler(event): - if 'hello' in event.raw_text: - await event.reply('hi!') - - client.start() - client.run_until_disconnected() - - -This code isn't much, but there might be some things unclear. -Let's break it down: - -.. code-block:: python - - from telethon import TelegramClient, events - - client = TelegramClient('anon', api_id, api_hash) - - -This is normal creation (of course, pass session name, API ID and hash). -Nothing we don't know already. - -.. code-block:: python - - @client.on(events.NewMessage) - - -This Python decorator will attach itself to the ``my_event_handler`` -definition, and basically means that *on* a `NewMessage -` *event*, -the callback function you're about to define will be called: - -.. code-block:: python - - async def my_event_handler(event): - if 'hello' in event.raw_text: - await event.reply('hi!') - - -If a `NewMessage -` event occurs, -and ``'hello'`` is in the text of the message, we `reply() -` to the event -with a ``'hi!'`` message. - -.. note:: - - Event handlers **must** be ``async def``. After all, - Telethon is an asynchronous library based on `asyncio`, - which is a safer and often faster approach to threads. - - You **must** ``await`` all method calls that use - network requests, which is most of them. - - -More Examples -============= - -Replying to messages with hello is fun, but, can we do more? - -.. code-block:: python - - @client.on(events.NewMessage(outgoing=True, pattern=r'\.save')) - async def handler(event): - if event.is_reply: - replied = await event.get_reply_message() - sender = replied.sender - await client.download_profile_photo(sender) - await event.respond('Saved your photo {}'.format(sender.username)) - -We could also get replies. This event filters outgoing messages -(only those that we send will trigger the method), then we filter -by the regex ``r'\.save'``, which will match messages starting -with ``".save"``. - -Inside the method, we check whether the event is replying to another message -or not. If it is, we get the reply message and the sender of that message, -and download their profile photo. - -Let's delete messages which contain "heck". We don't allow swearing here. - -.. code-block:: python - - @client.on(events.NewMessage(pattern=r'(?i).*heck')) - async def handler(event): - await event.delete() - - -With the ``r'(?i).*heck'`` regex, we match case-insensitive -"heck" anywhere in the message. Regex is very powerful and you -can learn more at https://regexone.com/. - -So far, we have only seen the `NewMessage -`, but there are many more -which will be covered later. This is only a small introduction to updates. - -Entities -======== - -When you need the user or chat where an event occurred, you **must** use -the following methods: - -.. code-block:: python - - async def handler(event): - # Good - chat = await event.get_chat() - sender = await event.get_sender() - chat_id = event.chat_id - sender_id = event.sender_id - - # BAD. Don't do this - chat = event.chat - sender = event.sender - chat_id = event.chat.id - sender_id = event.sender.id - -Events are like messages, but don't have all the information a message has! -When you manually get a message, it will have all the information it needs. -When you receive an update about a message, it **won't** have all the -information, so you have to **use the methods**, not the properties. - -Make sure you understand the code seen here before continuing! -As a rule of thumb, remember that new message events behave just -like message objects, so you can do with them everything you can -do with a message object. diff --git a/readthedocs/concepts/asyncio.rst b/readthedocs/concepts/asyncio.rst index 8500e55e..e69de29b 100644 --- a/readthedocs/concepts/asyncio.rst +++ b/readthedocs/concepts/asyncio.rst @@ -1,361 +0,0 @@ -.. _mastering-asyncio: - -================= -Mastering asyncio -================= - -.. contents:: - - -What's asyncio? -=============== - -`asyncio` is a Python 3's built-in library. This means it's already installed if -you have Python 3. Since Python 3.5, it is convenient to work with asynchronous -code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do. - -`asyncio` stands for *Asynchronous Input Output*. This is a very powerful -concept to use whenever you work IO. Interacting with the web or external -APIs such as Telegram's makes a lot of sense this way. - - -Why asyncio? -============ - -Asynchronous IO makes a lot of sense in a library like Telethon. -You send a request to the server (such as "get some message"), and -thanks to `asyncio`, your code won't block while a response arrives. - -The alternative would be to spawn a thread for each update so that -other code can run while the response arrives. That is *a lot* more -expensive. - -The code will also run faster, because instead of switching back and -forth between the OS and your script, your script can handle it all. -Avoiding switching saves quite a bit of time, in Python or any other -language that supports asynchronous IO. It will also be cheaper, -because tasks are smaller than threads, which are smaller than processes. - - -What are asyncio basics? -======================== - -.. code-block:: python - - # First we need the asyncio library - import asyncio - - # Then we need a loop to work with - loop = asyncio.get_event_loop() - - # We also need something to run - async def main(): - for char in 'Hello, world!\n': - print(char, end='', flush=True) - await asyncio.sleep(0.2) - - # Then, we need to run the loop with a task - loop.run_until_complete(main()) - - -What does telethon.sync do? -=========================== - -The moment you import any of these: - -.. code-block:: python - - from telethon import sync, ... - # or - from telethon.sync import ... - # or - import telethon.sync - -The ``sync`` module rewrites most ``async def`` -methods in Telethon to something similar to this: - -.. code-block:: python - - def new_method(): - result = original_method() - if loop.is_running(): - # the loop is already running, return the await-able to the user - return result - else: - # the loop is not running yet, so we can run it for the user - return loop.run_until_complete(result) - - -That means you can do this: - -.. code-block:: python - - print(client.get_me().username) - -Instead of this: - -.. code-block:: python - - import asyncio - loop = asyncio.get_event_loop() - me = loop.run_until_complete(client.get_me()) - print(me.username) - - -As you can see, it's a lot of boilerplate and noise having to type -``run_until_complete`` all the time, so you can let the magic module -to rewrite it for you. But notice the comment above: it won't run -the loop if it's already running, because it can't. That means this: - -.. code-block:: python - - async def main(): - # 3. the loop is running here - print( - client.get_me() # 4. this will return a coroutine! - .username # 5. this fails, coroutines don't have usernames - ) - - loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine - main() # 1. calling ``async def`` "returns" a coroutine - ) - - -Will fail. So if you're inside an ``async def``, then the loop is -running, and if the loop is running, you must ``await`` things yourself: - -.. code-block:: python - - async def main(): - print((await client.get_me()).username) - - loop.run_until_complete(main()) - - -What are async, await and coroutines? -===================================== - -The ``async`` keyword lets you define asynchronous functions, -also known as coroutines, and also iterate over asynchronous -loops or use ``async with``: - -.. code-block:: python - - import asyncio - - async def main(): - # ^ this declares the main() coroutine function - - async with client: - # ^ this is an asynchronous with block - - async for message in client.iter_messages(chat): - # ^ this is a for loop over an asynchronous generator - - print(message.sender.username) - - loop = asyncio.get_event_loop() - # ^ this assigns the default event loop from the main thread to a variable - - loop.run_until_complete(main()) - # ^ this runs the *entire* loop until the main() function finishes. - # While the main() function does not finish, the loop will be running. - # While the loop is running, you can't run it again. - - -The ``await`` keyword blocks the *current* task, and the loop can run -other tasks. Tasks can be thought of as "threads", since many can run -concurrently: - -.. code-block:: python - - import asyncio - - async def hello(delay): - await asyncio.sleep(delay) # await tells the loop this task is "busy" - print('hello') # eventually the loop resumes the code here - - async def world(delay): - # the loop decides this method should run first - await asyncio.sleep(delay) # await tells the loop this task is "busy" - print('world') # eventually the loop finishes all tasks - - loop = asyncio.get_event_loop() # get the default loop for the main thread - loop.create_task(world(2)) # create the world task, passing 2 as delay - loop.create_task(hello(delay=1)) # another task, but with delay 1 - try: - # run the event loop forever; ctrl+c to stop it - # we could also run the loop for three seconds: - # loop.run_until_complete(asyncio.sleep(3)) - loop.run_forever() - except KeyboardInterrupt: - pass - -The same example, but without the comment noise: - -.. code-block:: python - - import asyncio - - async def hello(delay): - await asyncio.sleep(delay) - print('hello') - - async def world(delay): - await asyncio.sleep(delay) - print('world') - - loop = asyncio.get_event_loop() - loop.create_task(world(2)) - loop.create_task(hello(1)) - loop.run_until_complete(asyncio.sleep(3)) - - -Can I use threads? -================== - -Yes, you can, but you must understand that the loops themselves are -not thread safe. and you must be sure to know what is happening. You -may want to create a loop in a new thread and make sure to pass it to -the client: - -.. code-block:: python - - import asyncio - import threading - - def go(): - loop = asyncio.new_event_loop() - client = TelegramClient(..., loop=loop) - ... - - threading.Thread(target=go).start() - - -Generally, **you don't need threads** unless you know what you're doing. -Just create another task, as shown above. If you're using the Telethon -with a library that uses threads, you must be careful to use `threading.Lock` -whenever you use the client, or enable the compatible mode. For that, see -:ref:`compatibility-and-convenience`. - -You may have seen this error: - -.. code-block:: text - - RuntimeError: There is no current event loop in thread 'Thread-1'. - -It just means you didn't create a loop for that thread, and if you don't -pass a loop when creating the client, it uses ``asyncio.get_event_loop()``, -which only works in the main thread. - - -client.run_until_disconnected() blocks! -======================================= - -All of what `client.run_until_disconnected() -` does is -run the `asyncio`'s event loop until the client is disconnected. That means -*the loop is running*. And if the loop is running, it will run all the tasks -in it. So if you want to run *other* code, create tasks for it: - -.. code-block:: python - - from datetime import datetime - - async def clock(): - while True: - print('The time:', datetime.now()) - await asyncio.sleep(1) - - loop.create_task(clock()) - ... - client.run_until_disconnected() - -This creates a task for a clock that prints the time every second. -You don't need to use `client.run_until_disconnected() -` either! -You just need to make the loop is running, somehow. `loop.run_forever() -` and `loop.run_until_complete() -` can also be used to run -the loop, and Telethon will be happy with any approach. - -Of course, there are better tools to run code hourly or daily, see below. - - -What else can asyncio do? -========================= - -Asynchronous IO is a really powerful tool, as we've seen. There are plenty -of other useful libraries that also use `asyncio` and that you can integrate -with Telethon. - -* `aiohttp `_ is like the infamous - `requests `_ but asynchronous. -* `quart `_ is an asynchronous alternative - to `Flask `_. -* `aiocron `_ lets you schedule things - to run things at a desired time, or run some tasks hourly, daily, etc. - -And of course, `asyncio `_ -itself! It has a lot of methods that let you do nice things. For example, -you can run requests in parallel: - -.. code-block:: python - - async def main(): - last, sent, download_path = await asyncio.gather( - client.get_messages('TelethonChat', 10), - client.send_message('TelethonOfftopic', 'Hey guys!'), - client.download_profile_photo('TelethonChat') - ) - - loop.run_until_complete(main()) - - -This code will get the 10 last messages from `@TelethonChat -`_, send one to `@TelethonOfftopic -`_, and also download the profile -photo of the main group. `asyncio` will run all these three tasks -at the same time. You can run all the tasks you want this way. - -A different way would be: - -.. code-block:: python - - loop.create_task(client.get_messages('TelethonChat', 10)) - loop.create_task(client.send_message('TelethonOfftopic', 'Hey guys!')) - loop.create_task(client.download_profile_photo('TelethonChat')) - -They will run in the background as long as the loop is running too. - -You can also `start an asyncio server -`_ -in the main script, and from another script, `connect to it -`_ -to achieve `Inter-Process Communication -`_. -You can get as creative as you want. You can program anything you want. -When you use a library, you're not limited to use only its methods. You can -combine all the libraries you want. People seem to forget this simple fact! - - -Why does client.start() work outside async? -=========================================== - -Because it's so common that it's really convenient to offer said -functionality by default. This means you can set up all your event -handlers and start the client without worrying about loops at all. - -Using the client in a ``with`` block, `start -`, `run_until_disconnected -`, and -`disconnect ` -all support this. - -Where can I read more? -====================== - -`Check out my blog post -`_ about `asyncio`, which -has some more examples and pictures to help you understand what happens -when the loop runs. diff --git a/readthedocs/concepts/botapi-vs-mtproto.rst b/readthedocs/concepts/botapi-vs-mtproto.rst index 75353a4a..e69de29b 100644 --- a/readthedocs/concepts/botapi-vs-mtproto.rst +++ b/readthedocs/concepts/botapi-vs-mtproto.rst @@ -1,333 +0,0 @@ -.. _botapi: - -======================= -HTTP Bot API vs MTProto -======================= - - -Telethon is more than just another viable alternative when developing bots -for Telegram. If you haven't decided which wrapper library for bots to use -yet, using Telethon from the beginning may save you some headaches later. - -.. contents:: - - -What is Bot API? -================ - -The `Telegram Bot API`_, also known as HTTP Bot API and from now on referred -to as simply "Bot API" is Telegram's official way for developers to control -their own Telegram bots. Quoting their main page: - - The Bot API is an HTTP-based interface created for developers keen on - building bots for Telegram. - - To learn how to create and set up a bot, please consult our - `Introduction to Bots`_ and `Bot FAQ`_. - -Bot API is simply an HTTP endpoint which translates your requests to it into -MTProto calls through tdlib_, their bot backend. - - -What is MTProto? -================ - -MTProto_ is Telegram's own protocol to communicate with their API when you -connect to their servers. - -Telethon is an alternative MTProto-based backend written entirely in Python -and much easier to setup and use. - -Both official applications and third-party clients (like your own -applications) logged in as either user or bots **can use MTProto** to -communicate directly with Telegram's API (which is not the HTTP bot API). - -When we talk about MTProto, we often mean "MTProto-based clients". - - -Advantages of MTProto over Bot API -================================== - -MTProto clients (like Telethon) connect directly to Telegram's servers, -which means there is no HTTP connection, no "polling" or "web hooks". This -means **less overhead**, since the protocol used between you and the server -is much more compact than HTTP requests with responses in wasteful JSON. - -Since there is a direct connection to Telegram's servers, even if their -Bot API endpoint is down, you can still have connection to Telegram directly. - -Using a MTProto client, you are also not limited to the public API that -they expose, and instead, **you have full control** of what your bot can do. -Telethon offers you all the power with often **much easier usage** than any -of the available Python Bot API wrappers. - -If your application ever needs user features because bots cannot do certain -things, you will be able to easily login as a user and even keep your bot -without having to learn a new library. - -If less overhead and full control didn't convince you to use Telethon yet, -check out the repository `HTTP Bot API vs MTProto comparison`_ with a more -exhaustive and up-to-date list of differences. - - -Migrating from Bot API to Telethon -================================== - -It doesn't matter if you wrote your bot with requests_ and you were -making API requests manually, or if you used a wrapper library like -python-telegram-bot_ or pyTelegramBotAPI_. It's never too late to -migrate to Telethon! - -If you were using an asynchronous library like aiohttp_ or a wrapper like -aiogram_ or dumbot_, it will be even easier, because Telethon is also an -asynchronous library. - -Next, we will see some examples from the most popular libraries. - - -Migrating from python-telegram-bot ----------------------------------- - -Let's take their `echobot2.py`_ example and shorten it a bit: - -.. code-block:: python - - from telegram.ext import Updater, CommandHandler, MessageHandler, Filters - - def start(update, context): - """Send a message when the command /start is issued.""" - update.message.reply_text('Hi!') - - def echo(update, context): - """Echo the user message.""" - update.message.reply_text(update.message.text) - - def main(): - """Start the bot.""" - updater = Updater("TOKEN") - dp = updater.dispatcher - dp.add_handler(CommandHandler("start", start)) - dp.add_handler(MessageHandler(Filters.text, echo)) - - updater.start_polling() - - updater.idle() - - if __name__ == '__main__': - main() - - -After using Telethon: - -.. code-block:: python - - from telethon import TelegramClient, events - - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def start(event): - """Send a message when the command /start is issued.""" - await event.respond('Hi!') - raise events.StopPropagation - - @bot.on(events.NewMessage) - async def echo(event): - """Echo the user message.""" - await event.respond(event.text) - - def main(): - """Start the bot.""" - bot.run_until_disconnected() - - if __name__ == '__main__': - main() - -Key differences: - -* The recommended way to do it imports less things. -* All handlers trigger by default, so we need ``events.StopPropagation``. -* Adding handlers, responding and running is a lot less verbose. -* Telethon needs ``async def`` and ``await``. -* The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``. - - -Migrating from pyTelegramBotAPI -------------------------------- - -Let's show another echobot from their README: - -.. code-block:: python - - import telebot - - bot = telebot.TeleBot("TOKEN") - - @bot.message_handler(commands=['start']) - def send_welcome(message): - bot.reply_to(message, "Howdy, how are you doing?") - - @bot.message_handler(func=lambda m: True) - def echo_all(message): - bot.reply_to(message, message.text) - - bot.polling() - -Now we rewrite it to use Telethon: - -.. code-block:: python - - from telethon import TelegramClient, events - - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def send_welcome(event): - await event.reply('Howdy, how are you doing?') - - @bot.on(events.NewMessage) - async def echo_all(event): - await event.reply(event.text) - - bot.run_until_disconnected() - -Key differences: - -* Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``. - Note that the ``event`` behaves just like their ``message``. -* Telethon also supports ``func=lambda m: True``, but it's not necessary. - - -Migrating from aiogram ----------------------- - -From their GitHub: - -.. code-block:: python - - from aiogram import Bot, Dispatcher, executor, types - - API_TOKEN = 'BOT TOKEN HERE' - - # Initialize bot and dispatcher - bot = Bot(token=API_TOKEN) - dp = Dispatcher(bot) - - @dp.message_handler(commands=['start']) - async def send_welcome(message: types.Message): - """ - This handler will be called when client send `/start` command. - """ - await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") - - @dp.message_handler(regexp='(^cat[s]?$|puss)') - async def cats(message: types.Message): - with open('data/cats.jpg', 'rb') as photo: - await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺', - reply_to_message_id=message.message_id) - - @dp.message_handler() - async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) - - if __name__ == '__main__': - executor.start_polling(dp, skip_updates=True) - - -After rewrite: - -.. code-block:: python - - from telethon import TelegramClient, events - - # Initialize bot and... just the bot! - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def send_welcome(event): - await event.reply('Howdy, how are you doing?') - - @bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)')) - async def cats(event): - await event.reply('Cats is here 😺', file='data/cats.jpg') - - @bot.on(events.NewMessage) - async def echo_all(event): - await event.reply(event.text) - - if __name__ == '__main__': - bot.run_until_disconnected() - - -Key differences: - -* Telethon offers convenience methods to avoid retyping - ``bot.send_photo(message.chat.id, ...)`` all the time, - and instead let you type ``event.reply``. -* Sending files is **a lot** easier. The methods for sending - photos, documents, audios, etc. are all the same! - -Migrating from dumbot ---------------------- - -Showcasing their subclassing example: - -.. code-block:: python - - from dumbot import Bot - - class Subbot(Bot): - async def init(self): - self.me = await self.getMe() - - async def on_update(self, update): - await self.sendMessage( - chat_id=update.message.chat.id, - text='i am {}'.format(self.me.username) - ) - - Subbot(token).run() - -After rewriting: - -.. code-block:: python - - from telethon import TelegramClient, events - - class Subbot(TelegramClient): - def __init__(self, *a, **kw): - await super().__init__(*a, **kw) - self.add_event_handler(self.on_update, events.NewMessage) - - async def connect(): - await super().connect() - self.me = await self.get_me() - - async def on_update(event): - await event.reply('i am {}'.format(self.me.username)) - - bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - bot.run_until_disconnected() - - -Key differences: - -* Telethon method names are ``snake_case``. -* dumbot does not offer friendly methods like ``update.reply``. -* Telethon does not have an implicit ``on_update`` handler, so - we need to manually register one. - - -.. _Telegram Bot API: https://core.telegram.org/bots/api -.. _Introduction to Bots: https://core.telegram.org/bots -.. _Bot FAQ: https://core.telegram.org/bots/faq -.. _tdlib: https://core.telegram.org/tdlib -.. _MTProto: https://core.telegram.org/mtproto -.. _HTTP Bot API vs MTProto comparison: https://github.com/telegram-mtproto/botapi-comparison -.. _requests: https://pypi.org/project/requests/ -.. _python-telegram-bot: https://python-telegram-bot.readthedocs.io -.. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI -.. _aiohttp: https://docs.aiohttp.org/en/stable -.. _aiogram: https://aiogram.readthedocs.io -.. _dumbot: https://github.com/Lonami/dumbot -.. _echobot2.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot2.py diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst index a92d36ba..e69de29b 100644 --- a/readthedocs/concepts/entities.rst +++ b/readthedocs/concepts/entities.rst @@ -1,308 +0,0 @@ -.. _entities: - -======== -Entities -======== - -The library widely uses the concept of "entities". An entity will refer -to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return -in response to certain methods, such as :tl:`GetUsersRequest`. - -.. note:: - - When something "entity-like" is required, it means that you need to - provide something that can be turned into an entity. These things include, - but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects, - or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even - phone numbers **from people you have in your contact list**. - - To "encounter" an ID, you would have to "find it" like you would in the - normal app. If the peer is in your dialogs, you would need to - `client.get_dialogs() `. - If the peer is someone in a group, you would similarly - `client.get_participants(group) `. - - Once you have encountered an ID, the library will (by default) have saved - their ``access_hash`` for you, which is needed to invoke most methods. - This is why sometimes you might encounter this error when working with - the library. You should ``except ValueError`` and run code that you know - should work to find the entity. - - -.. contents:: - - -What is an Entity? -================== - -A lot of methods and requests require *entities* to work. For example, -you send a message to an *entity*, get the username of an *entity*, and -so on. - -There are a lot of things that work as entities: usernames, phone numbers, -chat links, invite links, IDs, and the types themselves. That is, you can -use any of those when you see an "entity" is needed. - -.. note:: - - Remember that the phone number must be in your contact list before you - can use it. - -You should use, **from better to worse**: - -1. Input entities. For example, `event.input_chat - `, - `message.input_sender - `, - or caching an entity you will use a lot with - ``entity = await client.get_input_entity(...)``. - -2. Entities. For example, if you had to get someone's - username, you can just use ``user`` or ``channel``. - It will work. Only use this option if you already have the entity! - -3. IDs. This will always look the entity up from the - cache (the ``*.session`` file caches seen entities). - -4. Usernames, phone numbers and links. The cache will be - used too (unless you force a `client.get_entity() - `), - but may make a request if the username, phone or link - has not been found yet. - -In recent versions of the library, the following two are equivalent: - -.. code-block:: python - - async def handler(event): - await client.send_message(event.sender_id, 'Hi') - await client.send_message(event.input_sender, 'Hi') - - -If you need to be 99% sure that the code will work (sometimes it's -simply impossible for the library to find the input entity), or if -you will reuse the chat a lot, consider using the following instead: - -.. code-block:: python - - async def handler(event): - # This method may make a network request to find the input sender. - # Properties can't make network requests, so we need a method. - sender = await event.get_input_sender() - await client.send_message(sender, 'Hi') - await client.send_message(sender, 'Hi') - - -Getting Entities -================ - -Through the use of the :ref:`sessions`, the library will automatically -remember the ID and hash pair, along with some extra information, so -you're able to just do this: - -.. code-block:: python - - # Dialogs are the "conversations you have open". - # This method returns a list of Dialog, which - # has the .entity attribute and other information. - # - # This part is IMPORTANT, because it feels the entity cache. - dialogs = client.get_dialogs() - - # All of these work and do the same. - lonami = client.get_entity('lonami') - lonami = client.get_entity('t.me/lonami') - lonami = client.get_entity('https://telegram.dog/lonami') - - # Other kind of entities. - channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = client.get_entity('+34xxxxxxxxx') - friend = client.get_entity(friend_id) - - # Getting entities through their ID (User, Chat or Channel) - entity = client.get_entity(some_id) - - # You can be more explicit about the type for said ID by wrapping - # it inside a Peer instance. This is recommended but not necessary. - from telethon.tl.types import PeerUser, PeerChat, PeerChannel - - my_user = client.get_entity(PeerUser(some_id)) - my_chat = client.get_entity(PeerChat(some_id)) - my_channel = client.get_entity(PeerChannel(some_id)) - - -.. note:: - - You **don't** need to get the entity before using it! Just let the - library do its job. Use a phone from your contacts, username, ID or - input entity (preferred but not necessary), whatever you already have. - -All methods in the :ref:`telethon-client` call `.get_input_entity() -` prior -to sending the request to save you from the hassle of doing so manually. -That way, convenience calls such as `client.send_message('lonami', 'hi!') -` -become possible. - -Every entity the library encounters (in any response to any call) will by -default be cached in the ``.session`` file (an SQLite database), to avoid -performing unnecessary API calls. If the entity cannot be found, additonal -calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be -made to obtain the required information. - - -Entities vs. Input Entities -=========================== - -.. note:: - - This section is informative, but worth reading. The library - will transparently handle all of these details for you. - -On top of the normal types, the API also make use of what they call their -``Input*`` versions of objects. The input version of an entity (e.g. -:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum -information that's required from Telegram to be able to identify -who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They -are named like this because they are input parameters in the requests. - -Entities' ID are the same for all user and bot accounts, however, the access -hash is **different for each account**, so trying to reuse the access hash -from one account in another will **not** work. - -Sometimes, Telegram only needs to indicate the type of the entity along -with their ID. For this purpose, :tl:`Peer` versions of the entities also -exist, which just have the ID. You cannot get the hash out of them since -you should not be needing it. The library probably has cached it before. - -Peers are enough to identify an entity, but they are not enough to make -a request with them use them. You need to know their hash before you can -"use them", and to know the hash you need to "encounter" them, let it -be in your dialogs, participants, message forwards, etc. - -.. note:: - - You *can* use peers with the library. Behind the scenes, they are - replaced with the input variant. Peers "aren't enough" on their own - but the library will do some more work to use the right type. - -As we just mentioned, API calls don't need to know the whole information -about the entities, only their ID and hash. For this reason, another method, -`client.get_input_entity() ` -is available. This will always use the cache while possible, making zero API -calls most of the time. When a request is made, if you provided the full -entity, e.g. an :tl:`User`, the library will convert it to the required -:tl:`InputPeer` automatically for you. - -**You should always favour** -`client.get_input_entity() ` -**over** -`client.get_entity() ` -for this reason! Calling the latter will always make an API call to get -the most recent information about said entity, but invoking requests don't -need this information, just the :tl:`InputPeer`. Only use -`client.get_entity() ` -if you need to get actual information, like the username, name, title, etc. -of the entity. - -To further simplify the workflow, since the version ``0.16.2`` of the -library, the raw requests you make to the API are also able to call -`client.get_input_entity() ` -wherever needed, so you can even do things like: - -.. code-block:: python - - client(SendMessageRequest('username', 'hello')) - -The library will call the ``.resolve()`` method of the request, which will -resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if -you don't get this yet, but remember some of the details here are important. - - -Full Entities -============= - -In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its -variants for chats and channels), there is also the concept of :tl:`UserFull`. - -This full variant has additional information such as whether the user is -blocked, its notification settings, the bio or about of the user, etc. - -There is also :tl:`messages.ChatFull` which is the equivalent of full entities -for chats and channels, with also the about section of the channel. Note that -the ``users`` field only contains bots for the channel (so that clients can -suggest commands to use). - -You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` -and :tl:`GetFullChannel` respectively. - - -Accessing Entities -================== - -Although it's explicitly noted in the documentation that messages -*subclass* `ChatGetter ` -and `SenderGetter `, -some people still don't get inheritance. - -When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`" -it means that the class you're looking at, *also* can act as the class it -bases. In this case, `ChatGetter ` -knows how to get the *chat* where a thing belongs to. - -So, a `Message ` is a -`ChatGetter `. -That means you can do this: - -.. code-block:: python - - message.is_private - message.chat_id - message.get_chat() - # ...etc - -`SenderGetter ` is similar: - -.. code-block:: python - - message.user_id - message.get_input_user() - message.user - # ...etc - -Quite a few things implement them, so it makes sense to reuse the code. -For example, all events (except raw updates) implement `ChatGetter -` since all events occur -in some chat. - - -Summary -======= - -TL;DR; If you're here because of *"Could not find the input entity for"*, -you must ask yourself "how did I find this entity through official -applications"? Now do the same with the library. Use what applies: - -.. code-block:: python - - with client: - # Does it have an username? Use it! - entity = client.get_entity(username) - - # Do you have a conversation open with them? Get dialogs. - client.get_dialogs() - - # Are they participant of some group? Get them. - client.get_participants('TelethonChat') - - # Is the entity the original sender of a forwarded message? Get it. - client.get_messages('TelethonChat', 100) - - # NOW you can use the ID, anywhere! - entity = client.get_entity(123456) - client.send_message(123456, 'Hi!') - -Once the library has "seen" the entity, you can use their **integer** ID. -You can't use entities from IDs the library hasn't seen. You must make the -library see them *at least once* and disconnect properly. You know where -the entities are and you must tell the library. It won't guess for you. diff --git a/readthedocs/concepts/errors.rst b/readthedocs/concepts/errors.rst index 429c5697..e69de29b 100644 --- a/readthedocs/concepts/errors.rst +++ b/readthedocs/concepts/errors.rst @@ -1,77 +0,0 @@ -.. _rpc-errors: - -========== -RPC Errors -========== - -RPC stands for Remote Procedure Call, and when the library raises -a ``RPCError``, it's because you have invoked some of the API -methods incorrectly (wrong parameters, wrong permissions, or even -something went wrong on Telegram's server). All the errors are -available in :ref:`telethon-errors`, but some examples are: - -- ``FloodWaitError`` (420), the same request was repeated many times. - Must wait ``.seconds`` (you can access this attribute). For example: - - .. code-block:: python - - ... - from telethon import errors - - try: - print(client.get_messages(chat)[0].text) - except errors.FloodWaitError as e: - print('Have to sleep', e.seconds, 'seconds') - time.sleep(e.seconds) - -- ``SessionPasswordNeededError``, if you have setup two-steps - verification on Telegram. -- ``CdnFileTamperedError``, if the media you were trying to download - from a CDN has been altered. -- ``ChatAdminRequiredError``, you don't have permissions to perform - said operation on a chat or channel. Try avoiding filters, i.e. when - searching messages. - -The generic classes for different error codes are: - -- ``InvalidDCError`` (303), the request must be repeated on another DC. -- ``BadRequestError`` (400), the request contained errors. -- ``UnauthorizedError`` (401), the user is not authorized yet. -- ``ForbiddenError`` (403), privacy violation error. -- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's! - -If the error is not recognised, it will only be an ``RPCError``. - -You can refer to all errors from Python through the ``telethon.errors`` -module. If you don't know what attributes they have, try printing their -dir (like ``print(dir(e))``). - -Avoiding Limits -=============== - -Don't spam. You won't get ``FloodWaitError`` or your account banned or -deleted if you use the library *for legit use cases*. Make cool tools. -Don't spam! Nobody knows the exact limits for all requests since they -depend on a lot of factors, so don't bother asking. - -Still, if you do have a legit use case and still get those errors, the -library will automatically sleep when they are smaller than 60 seconds -by default. You can set different "auto-sleep" thresholds: - -.. code-block:: python - - client.flood_sleep_threshold = 0 # Don't auto-sleep - client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always - -You can also except it and act as you prefer: - -.. code-block:: python - - from telethon.errors import FloodWaitError - try: - ... - except FloodWaitError as e: - print('Flood waited for', e.seconds) - quit(1) - -VoIP numbers are very limited, and some countries are more limited too. diff --git a/readthedocs/concepts/full-api.rst b/readthedocs/concepts/full-api.rst index 2b1a6462..e69de29b 100644 --- a/readthedocs/concepts/full-api.rst +++ b/readthedocs/concepts/full-api.rst @@ -1,229 +0,0 @@ -.. _full-api: - -============ -The Full API -============ - -.. important:: - - While you have access to this, you should always use the friendly - methods listed on :ref:`client-ref` unless you have a better reason - not to, like a method not existing or you wanting more control. - - -The :ref:`telethon-client` doesn't offer a method for every single request -the Telegram API supports. However, it's very simple to *call* or *invoke* -any request. Whenever you need something, don't forget to `check the documentation`_ -and look for the `method you need`_. There you can go through a sorted list -of everything you can do. - - -.. note:: - - The reason to keep both https://tl.telethon.dev and this - documentation alive is that the former allows instant search results - as you type, and a "Copy import" button. If you like namespaces, you - can also do ``from telethon.tl import types, functions``. Both work. - - -.. important:: - - All the examples in this documentation assume that you have - ``from telethon import sync`` or ``import telethon.sync`` for the - sake of simplicity and that you understand what it does (see - :ref:`compatibility-and-convenience` for more). Simply add - either line at the beginning of your project and it will work. - - -You should also refer to the documentation to see what the objects -(constructors) Telegram returns look like. Every constructor inherits -from a common type, and that's the reason for this distinction. - -Say `client.send_message() -` didn't exist, -we could `use the search`_ to look for "message". There we would find -:tl:`SendMessageRequest`, which we can work with. - -Every request is a Python class, and has the parameters needed for you -to invoke it. You can also call ``help(request)`` for information on -what input parameters it takes. Remember to "Copy import to the -clipboard", or your script won't be aware of this class! Now we have: - -.. code-block:: python - - from telethon.tl.functions.messages import SendMessageRequest - -If you're going to use a lot of these, you may do: - -.. code-block:: python - - from telethon.tl import types, functions - # We now have access to 'functions.messages.SendMessageRequest' - -We see that this request must take at least two parameters, a ``peer`` -of type :tl:`InputPeer`, and a ``message`` which is just a Python -``str``\ ing. - -How can we retrieve this :tl:`InputPeer`? We have two options. We manually -construct one, for instance: - -.. code-block:: python - - from telethon.tl.types import InputPeerUser - - peer = InputPeerUser(user_id, user_hash) - -Or we call `client.get_input_entity() -`: - -.. code-block:: python - - import telethon.sync - peer = client.get_input_entity('someone') - - -When you're going to invoke an API method, most require you to pass an -:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using -`client.get_input_entity() ` -is more straightforward (and often immediate, if you've seen the user before, -know their ID, etc.). If you also **need** to have information about the whole -user, use `client.get_entity() ` -instead: - -.. code-block:: python - - entity = client.get_entity('someone') - -In the later case, when you use the entity, the library will cast it to -its "input" version for you. If you already have the complete user and -want to cache its input version so the library doesn't have to do this -every time its used, simply call `telethon.utils.get_input_peer`: - -.. code-block:: python - - from telethon import utils - peer = utils.get_input_peer(entity) - - -.. note:: - - Since ``v0.16.2`` this is further simplified. The ``Request`` itself - will call `client.get_input_entity - ` for you when - required, but it's good to remember what's happening. - -After this small parenthesis about `client.get_entity -` versus -`client.get_input_entity() `, -we have everything we need. To invoke our -request we do: - -.. code-block:: python - - result = client(SendMessageRequest(peer, 'Hello there!')) - -Message sent! Of course, this is only an example. There are over 250 -methods available as of layer 80, and you can use every single of them -as you wish. Remember to use the right types! To sum up: - -.. code-block:: python - - result = client(SendMessageRequest( - client.get_input_entity('username'), 'Hello there!' - )) - - -This can further be simplified to: - -.. code-block:: python - - result = client(SendMessageRequest('username', 'Hello there!')) - # Or even - result = client(SendMessageRequest(PeerChannel(id), 'Hello there!')) - -.. note:: - - Note that some requests have a "hash" parameter. This is **not** - your ``api_hash``! It likely isn't your self-user ``.access_hash`` either. - - It's a special hash used by Telegram to only send a difference of new data - that you don't already have with that request, so you can leave it to 0, - and it should work (which means no hash is known yet). - - For those requests having a "limit" parameter, you can often set it to - zero to signify "return default amount". This won't work for all of them - though, for instance, in "messages.search" it will actually return 0 items. - - -Requests in Parallel -==================== - -The library will automatically merge outgoing requests into a single -*container*. Telegram's API supports sending multiple requests in a -single container, which is faster because it has less overhead and -the server can run them without waiting for others. You can also -force using a container manually: - -.. code-block:: python - - async def main(): - - # Letting the library do it behind the scenes - await asyncio.wait([ - client.send_message('me', 'Hello'), - client.send_message('me', ','), - client.send_message('me', 'World'), - client.send_message('me', '.') - ]) - - # Manually invoking many requests at once - await client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ', '), - SendMessageRequest('me', 'World'), - SendMessageRequest('me', '.') - ]) - -Note that you cannot guarantee the order in which they are run. -Try running the above code more than one time. You will see the -order in which the messages arrive is different. - -If you use the raw API (the first option), you can use ``ordered`` -to tell the server that it should run the requests sequentially. -This will still be faster than going one by one, since the server -knows all requests directly: - -.. code-block:: python - - client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ', '), - SendMessageRequest('me', 'World'), - SendMessageRequest('me', '.') - ], ordered=True) - -If any of the requests fails with a Telegram error (not connection -errors or any other unexpected events), the library will raise -`telethon.errors.common.MultiError`. You can ``except`` this -and still access the successful results: - -.. code-block:: python - - from telethon.errors import MultiError - - try: - client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ''), - SendMessageRequest('me', 'World') - ], ordered=True) - except MultiError as e: - # The first and third requests worked. - first = e.results[0] - third = e.results[2] - # The second request failed. - second = e.exceptions[1] - -.. _check the documentation: https://tl.telethon.dev -.. _method you need: https://tl.telethon.dev/methods/index.html -.. _use the search: https://tl.telethon.dev/?q=message&redirect=no diff --git a/readthedocs/concepts/sessions.rst b/readthedocs/concepts/sessions.rst index ee528b48..e69de29b 100644 --- a/readthedocs/concepts/sessions.rst +++ b/readthedocs/concepts/sessions.rst @@ -1,158 +0,0 @@ -.. _sessions: - -============== -Session Files -============== - -.. contents:: - -They are an important part for the library to be efficient, such as caching -and handling your authorization key (or you would have to login every time!). - -What are Sessions? -================== - -The first parameter you pass to the constructor of the -:ref:`TelegramClient ` is -the ``session``, and defaults to be the session name (or full path). That is, -if you create a ``TelegramClient('anon')`` instance and connect, an -``anon.session`` file will be created in the working directory. - -Note that if you pass a string it will be a file in the current working -directory, although you can also pass absolute paths. - -The session file contains enough information for you to login without -re-sending the code, so if you have to enter the code more than once, -maybe you're changing the working directory, renaming or removing the -file, or using random names. - -These database files using ``sqlite3`` contain the required information to -talk to the Telegram servers, such as to which IP the client should connect, -port, authorization key so that messages can be encrypted, and so on. - -These files will by default also save all the input entities that you've seen, -so that you can get information about a user or channel by just their ID. -Telegram will **not** send their ``access_hash`` required to retrieve more -information about them, if it thinks you have already seem them. For this -reason, the library needs to store this information offline. - -The library will by default too save all the entities (chats and channels -with their name and username, and users with the phone too) in the session -file, so that you can quickly access them by username or phone number. - -If you're not going to work with updates, or don't need to cache the -``access_hash`` associated with the entities' ID, you can disable this -by setting ``client.session.save_entities = False``. - - -Different Session Storage -========================= - -If you don't want to use the default SQLite session storage, you can also use -one of the other implementations or implement your own storage. - -To use a custom session storage, simply pass the custom session instance to -:ref:`TelegramClient ` instead of -the session name. - -Telethon contains three implementations of the abstract ``Session`` class: - -.. currentmodule:: telethon.sessions - -* `MemorySession `: stores session data within memory. -* `SQLiteSession `: stores sessions within on-disk SQLite databases. Default. -* `StringSession `: stores session data within memory, - but can be saved as a string. - -You can import these ``from telethon.sessions``. For example, using the -`StringSession ` is done as follows: - -.. code-block:: python - - from telethon.sync import TelegramClient - from telethon.sessions import StringSession - - with TelegramClient(StringSession(string), api_id, api_hash) as client: - ... # use the client - - # Save the string session as a string; you should decide how - # you want to save this information (over a socket, remote - # database, print it and then paste the string in the code, - # etc.); the advantage is that you don't need to save it - # on the current disk as a separate file, and can be reused - # anywhere else once you log in. - string = client.session.save() - - # Note that it's also possible to save any other session type - # as a string by using ``StringSession.save(session_instance)``: - client = TelegramClient('sqlite-session', api_id, api_hash) - string = StringSession.save(client.session) - -There are other community-maintained implementations available: - -* `SQLAlchemy `_: - stores all sessions in a single database via SQLAlchemy. - -* `Redis `_: - stores all sessions in a single Redis data store. - - -Creating your Own Storage -========================= - -The easiest way to create your own storage implementation is to use -`MemorySession ` as the base and check out how -`SQLiteSession ` or one of the community-maintained -implementations work. You can find the relevant Python files under the -``sessions/`` directory in the Telethon's repository. - -After you have made your own implementation, you can add it to the -community-maintained session implementation list above with a pull request. - - -String Sessions -=============== - -`StringSession ` are a convenient way to embed your -login credentials directly into your code for extremely easy portability, -since all they take is a string to be able to login without asking for your -phone and code (or faster start if you're using a bot token). - -The easiest way to generate a string session is as follows: - -.. code-block:: python - - from telethon.sync import TelegramClient - from telethon.sessions import StringSession - - with TelegramClient(StringSession(), api_id, api_hash) as client: - print(client.session.save()) - - -Think of this as a way to export your authorization key (what's needed -to login into your account). This will print a string in the standard -output (likely your terminal). - -.. warning:: - - **Keep this string safe!** Anyone with this string can use it - to login into your account and do anything they want to to do. - - This is similar to leaking your ``*.session`` files online, - but it is easier to leak a string than it is to leak a file. - - -Once you have the string (which is a bit long), load it into your script -somehow. You can use a normal text file and ``open(...).read()`` it or -you can save it in a variable directly: - -.. code-block:: python - - string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...' - with TelegramClient(StringSession(string), api_id, api_hash) as client: - client.send_message('me', 'Hi') - - -These strings are really convenient for using in places like Heroku since -their ephemeral filesystem will delete external files once your application -is over. diff --git a/readthedocs/concepts/strings.rst b/readthedocs/concepts/strings.rst index c88d6940..e69de29b 100644 --- a/readthedocs/concepts/strings.rst +++ b/readthedocs/concepts/strings.rst @@ -1,100 +0,0 @@ -====================== -String-based Debugging -====================== - -Debugging is *really* important. Telegram's API is really big and there -is a lot of things that you should know. Such as, what attributes or fields -does a result have? Well, the easiest thing to do is printing it: - -.. code-block:: python - - user = client.get_entity('Lonami') - print(user) - -That will show a huge **string** similar to the following: - -.. code-block:: python - - User(id=10885151, is_self=False, contact=False, mutual_contact=False, deleted=False, bot=False, bot_chat_history=False, bot_nochats=False, verified=False, restricted=False, min=False, bot_inline_geo=False, access_hash=123456789012345678, first_name='Lonami', last_name=None, username='Lonami', phone=None, photo=UserProfilePhoto(photo_id=123456789012345678, photo_small=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678), photo_big=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678)), status=UserStatusOffline(was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)), bot_info_version=None, restriction_reason=None, bot_inline_placeholder=None, lang_code=None) - -That's a lot of text. But as you can see, all the properties are there. -So if you want the username you **don't use regex** or anything like -splitting ``str(user)`` to get what you want. You just access the -attribute you need: - -.. code-block:: python - - username = user.username - -Can we get better than the shown string, though? Yes! - -.. code-block:: python - - print(user.stringify()) - -Will show a much better: - -.. code-block:: python - - User( - id=10885151, - is_self=False, - contact=False, - mutual_contact=False, - deleted=False, - bot=False, - bot_chat_history=False, - bot_nochats=False, - verified=False, - restricted=False, - min=False, - bot_inline_geo=False, - access_hash=123456789012345678, - first_name='Lonami', - last_name=None, - username='Lonami', - phone=None, - photo=UserProfilePhoto( - photo_id=123456789012345678, - photo_small=FileLocation( - dc_id=4, - volume_id=123456789, - local_id=123456789, - secret=-123456789012345678 - ), - photo_big=FileLocation( - dc_id=4, - volume_id=123456789, - local_id=123456789, - secret=123456789012345678 - ) - ), - status=UserStatusOffline( - was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc) - ), - bot_info_version=None, - restriction_reason=None, - bot_inline_placeholder=None, - lang_code=None - ) - -Now it's easy to see how we could get, for example, -the ``was_online`` time. It's inside ``status``: - -.. code-block:: python - - online_at = user.status.was_online - -You don't need to print everything to see what all the possible values -can be. You can just search in http://tl.telethon.dev/. - -Remember that you can use Python's `isinstance -`_ -to check the type of something. For example: - -.. code-block:: python - - from telethon import types - - if isinstance(user.status, types.UserStatusOffline): - print(user.status.was_online) diff --git a/readthedocs/concepts/updates.rst b/readthedocs/concepts/updates.rst index a5b978de..e69de29b 100644 --- a/readthedocs/concepts/updates.rst +++ b/readthedocs/concepts/updates.rst @@ -1,229 +0,0 @@ -================ -Updates in Depth -================ - -Properties vs. Methods -====================== - -The event shown above acts just like a `custom.Message -`, which means you -can access all the properties it has, like ``.sender``. - -**However** events are different to other methods in the client, like -`client.get_messages `. -Events *may not* send information about the sender or chat, which means it -can be ``None``, but all the methods defined in the client always have this -information so it doesn't need to be re-fetched. For this reason, you have -``get_`` methods, which will make a network call if necessary. - -In short, you should do this: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - # event.input_chat may be None, use event.get_input_chat() - chat = await event.get_input_chat() - sender = await event.get_sender() - buttons = await event.get_buttons() - - async def main(): - async for message in client.iter_messages('me', 10): - # Methods from the client always have these properties ready - chat = message.input_chat - sender = message.sender - buttons = message.buttons - -Notice, properties (`message.sender -`) don't need an ``await``, but -methods (`message.get_sender -`) **do** need an ``await``, -and you should use methods in events for these properties that may need network. - -Events Without the client -========================= - -The code of your application starts getting big, so you decide to -separate the handlers into different files. But how can you access -the client from these files? You don't need to! Just `events.register -` them: - -.. code-block:: python - - # handlers/welcome.py - from telethon import events - - @events.register(events.NewMessage('(?i)hello')) - async def handler(event): - client = event.client - await event.respond('Hey!') - await client.send_message('me', 'I said hello to someone') - - -Registering events is a way of saying "this method is an event handler". -You can use `telethon.events.is_handler` to check if any method is a handler. -You can think of them as a different approach to Flask's blueprints. - -It's important to note that this does **not** add the handler to any client! -You never specified the client on which the handler should be used. You only -declared that it is a handler, and its type. - -To actually use the handler, you need to `client.add_event_handler -` to the -client (or clients) where they should be added to: - -.. code-block:: python - - # main.py - from telethon import TelegramClient - import handlers.welcome - - with TelegramClient(...) as client: - client.add_event_handler(handlers.welcome.handler) - client.run_until_disconnected() - - -This also means that you can register an event handler once and -then add it to many clients without re-declaring the event. - - -Events Without Decorators -========================= - -If for any reason you don't want to use `telethon.events.register`, -you can explicitly pass the event handler to use to the mentioned -`client.add_event_handler -`: - -.. code-block:: python - - from telethon import TelegramClient, events - - async def handler(event): - ... - - with TelegramClient(...) as client: - client.add_event_handler(handler, events.NewMessage) - client.run_until_disconnected() - - -Similarly, you also have `client.remove_event_handler -` -and `client.list_event_handlers -`. - -The ``event`` argument is optional in all three methods and defaults to -`events.Raw ` for adding, and ``None`` when -removing (so all callbacks would be removed). - -.. note:: - - The ``event`` type is ignored in `client.add_event_handler - ` - if you have used `telethon.events.register` on the ``callback`` - before, since that's the point of using such method at all. - - -Stopping Propagation of Updates -=============================== - -There might be cases when an event handler is supposed to be used solitary and -it makes no sense to process any other handlers in the chain. For this case, -it is possible to raise a `telethon.events.StopPropagation` exception which -will cause the propagation of the update through your handlers to stop: - -.. code-block:: python - - from telethon.events import StopPropagation - - @client.on(events.NewMessage) - async def _(event): - # ... some conditions - await event.delete() - - # Other handlers won't have an event to work with - raise StopPropagation - - @client.on(events.NewMessage) - async def _(event): - # Will never be reached, because it is the second handler - # in the chain. - pass - - -Remember to check :ref:`telethon-events` if you're looking for -the methods reference. - -Understanding asyncio -===================== - - -With `asyncio`, the library has several tasks running in the background. -One task is used for sending requests, another task is used to receive them, -and a third one is used to handle updates. - -To handle updates, you must keep your script running. You can do this in -several ways. For instance, if you are *not* running `asyncio`'s event -loop, you should use `client.run_until_disconnected -`: - -.. code-block:: python - - import asyncio - from telethon import TelegramClient - - client = TelegramClient(...) - ... - client.run_until_disconnected() - - -Behind the scenes, this method is ``await``'ing on the `client.disconnected -` property, -so the code above and the following are equivalent: - -.. code-block:: python - - import asyncio - from telethon import TelegramClient - - client = TelegramClient(...) - - async def main(): - await client.disconnected - - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) - - -You could also run `client.disconnected -` -until it completed. - -But if you don't want to ``await``, then you should know what you want -to be doing instead! What matters is that you shouldn't let your script -die. If you don't care about updates, you don't need any of this. - -Notice that unlike `client.disconnected -`, -`client.run_until_disconnected -` will -handle ``KeyboardInterrupt`` with you. This method is special and can -also be ran while the loop is running, so you can do this: - -.. code-block:: python - - async def main(): - await client.run_until_disconnected() - - loop.run_until_complete(main()) - -Sequential Updates -================== - -If you need to process updates sequentially (i.e. not in parallel), -you should set ``sequential_updates=True`` when creating the client: - -.. code-block:: python - - with TelegramClient(..., sequential_updates=True) as client: - ... diff --git a/readthedocs/conf.py b/readthedocs/conf.py index f4841dd5..e69de29b 100644 --- a/readthedocs/conf.py +++ b/readthedocs/conf.py @@ -1,193 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Telethon documentation build configuration file, created by -# sphinx-quickstart on Fri Nov 17 15:36:11 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import re -import os -import sys - -sys.path.insert(0, os.path.abspath(os.curdir)) -sys.path.insert(0, os.path.abspath(os.pardir)) - -root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir)) - -tl_ref_url = 'https://tl.telethon.dev' - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', - 'custom_roles' -] - -intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None) -} - -# Change the default role so we can avoid prefixing everything with :obj: -default_role = "py:obj" - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Telethon' -copyright = '2017 - 2019, Lonami' -author = 'Lonami' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -with open(os.path.join(root, 'telethon', 'version.py'), 'r') as f: - version = re.search(r"^__version__\s+=\s+'(.*)'$", - f.read(), flags=re.MULTILINE).group(1) - -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'friendly' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - 'collapse_navigation': True, - 'display_version': True, - 'navigation_depth': 3, -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'globaltoc.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Telethondoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'Telethon.tex', 'Telethon Documentation', - author, 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'telethon', 'Telethon Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'Telethon', 'Telethon Documentation', - author, 'Telethon', 'One line description of project.', - 'Miscellaneous'), -] - diff --git a/readthedocs/custom_roles.py b/readthedocs/custom_roles.py index bf025fb8..e69de29b 100644 --- a/readthedocs/custom_roles.py +++ b/readthedocs/custom_roles.py @@ -1,67 +0,0 @@ -from docutils import nodes, utils -from docutils.parsers.rst.roles import set_classes - - -def make_link_node(rawtext, app, name, options): - """ - Create a link to the TL reference. - - :param rawtext: Text being replaced with link node. - :param app: Sphinx application context - :param name: Name of the object to link to - :param options: Options dictionary passed to role func. - """ - try: - base = app.config.tl_ref_url - if not base: - raise AttributeError - except AttributeError as e: - raise ValueError('tl_ref_url config value is not set') from e - - if base[-1] != '/': - base += '/' - - set_classes(options) - node = nodes.reference(rawtext, utils.unescape(name), - refuri='{}?q={}'.format(base, name), - **options) - return node - - -# noinspection PyUnusedLocal -def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None): - """ - Link to the TL reference. - - Returns 2 part tuple containing list of nodes to insert into the - document and a list of system messages. Both are allowed to be empty. - - :param name: The role name used in the document. - :param rawtext: The entire markup snippet, with role. - :param text: The text marked with the role. - :param lineno: The line number where rawtext appears in the input. - :param inliner: The inliner instance that called us. - :param options: Directive options for customization. - :param content: The directive content for customization. - """ - if options is None: - options = {} - - # TODO Report error on type not found? - # Usage: - # msg = inliner.reporter.error(..., line=lineno) - # return [inliner.problematic(rawtext, rawtext, msg)], [msg] - app = inliner.document.settings.env.app - node = make_link_node(rawtext, app, text, options) - return [node], [] - - -def setup(app): - """ - Install the plugin. - - :param app: Sphinx application context. - """ - app.add_role('tl', tl_role) - app.add_config_value('tl_ref_url', None, 'env') - return diff --git a/readthedocs/developing/coding-style.rst b/readthedocs/developing/coding-style.rst index c629034c..e69de29b 100644 --- a/readthedocs/developing/coding-style.rst +++ b/readthedocs/developing/coding-style.rst @@ -1,22 +0,0 @@ -============ -Coding Style -============ - - -Basically, make it **readable**, while keeping the style similar to the -code of whatever file you're working on. - -Also note that not everyone has 4K screens for their primary monitors, -so please try to stick to the 80-columns limit. This makes it easy to -``git diff`` changes from a terminal before committing changes. If the -line has to be long, please don't exceed 120 characters. - -For the commit messages, please make them *explanatory*. Not only -they're helpful to troubleshoot when certain issues could have been -introduced, but they're also used to construct the change log once a new -version is ready. - -If you don't know enough Python, I strongly recommend reading `Dive Into -Python 3 `__, available online for -free. For instance, remember to do ``if x is None`` or -``if x is not None`` instead ``if x == None``! diff --git a/readthedocs/developing/philosophy.rst b/readthedocs/developing/philosophy.rst index f779be2b..e69de29b 100644 --- a/readthedocs/developing/philosophy.rst +++ b/readthedocs/developing/philosophy.rst @@ -1,25 +0,0 @@ -========== -Philosophy -========== - - -The intention of the library is to have an existing MTProto library -existing with hardly any dependencies (indeed, wherever Python is -available, you can run this library). - -Being written in Python means that performance will be nowhere close to -other implementations written in, for instance, Java, C++, Rust, or -pretty much any other compiled language. However, the library turns out -to actually be pretty decent for common operations such as sending -messages, receiving updates, or other scripting. Uploading files may be -notably slower, but if you would like to contribute, pull requests are -appreciated! - -If ``libssl`` is available on your system, the library will make use of -it to speed up some critical parts such as encrypting and decrypting the -messages. Files will notably be sent and downloaded faster. - -The main focus is to keep everything clean and simple, for everyone to -understand how working with MTProto and Telegram works. Don't be afraid -to read the source, the code won't bite you! It may prove useful when -using the library on your own use cases. diff --git a/readthedocs/developing/project-structure.rst b/readthedocs/developing/project-structure.rst index 960234ac..e69de29b 100644 --- a/readthedocs/developing/project-structure.rst +++ b/readthedocs/developing/project-structure.rst @@ -1,47 +0,0 @@ -================= -Project Structure -================= - - -Main interface -============== - -The library itself is under the ``telethon/`` directory. The -``__init__.py`` file there exposes the main ``TelegramClient``, a class -that servers as a nice interface with the most commonly used methods on -Telegram such as sending messages, retrieving the message history, -handling updates, etc. - -The ``TelegramClient`` inherits from several mixing ``Method`` classes, -since there are so many methods that having them in a single file would -make maintenance painful (it was three thousand lines before this separation -happened!). It's a "god object", but there is only a way to interact with -Telegram really. - -The ``TelegramBaseClient`` is an ABC which will support all of these mixins -so they can work together nicely. It doesn't even know how to invoke things -because they need to be resolved with user information first (to work with -input entities comfortably). - -The client makes use of the ``network/mtprotosender.py``. The -``MTProtoSender`` is responsible for connecting, reconnecting, -packing, unpacking, sending and receiving items from the network. -Basically, the low-level communication with Telegram, and handling -MTProto-related functions and types such as ``BadSalt``. - -The sender makes use of a ``Connection`` class which knows the format in -which outgoing messages should be sent (how to encode their length and -their body, if they're further encrypted). - -Auto-generated code -=================== - -The files under ``telethon_generator/`` are used to generate the code -that gets placed under ``telethon/tl/``. The parsers take in files in -a specific format (such as ``.tl`` for objects and ``.json`` for errors) -and spit out the generated classes which represent, as Python classes, -the request and types defined in the ``.tl`` file. It also constructs -an index so that they can be imported easily. - -Custom documentation can also be generated to easily navigate through -the vast amount of items offered by the API. diff --git a/readthedocs/developing/telegram-api-in-other-languages.rst b/readthedocs/developing/telegram-api-in-other-languages.rst index d77ebaa9..e69de29b 100644 --- a/readthedocs/developing/telegram-api-in-other-languages.rst +++ b/readthedocs/developing/telegram-api-in-other-languages.rst @@ -1,73 +0,0 @@ -=============================== -Telegram API in Other Languages -=============================== - - -Telethon was made for **Python**, and as far as I know, there is no -*exact* port to other languages. However, there *are* other -implementations made by awesome people (one needs to be awesome to -understand the official Telegram documentation) on several languages -(even more Python too), listed below: - -C -* - -Possibly the most well-known unofficial open source implementation out -there by `@vysheng `__, -`tgl `__, and its console client -`telegram-cli `__. Latest development -has been moved to `BitBucket `__. - -C++ -=== - -The newest (and official) library, written from scratch, is called -`tdlib `__ and is what the Telegram X -uses. You can find more information in the official documentation, -published `here `__. - -JavaScript -========== - -`@zerobias `__ is working on -`telegram-mtproto `__, -a work-in-progress JavaScript library installable via -`npm `__. - -Kotlin -====== - -`Kotlogram `__ is a Telegram -implementation written in Kotlin (one of the -`official `__ -languages for -`Android `__) by -`@badoualy `__, currently as a beta– -yet working. - -PHP -=== - -A PHP implementation is also available thanks to -`@danog `__ and his -`MadelineProto `__ project, with -a very nice `online -documentation `__ too. - -Python -====== - -A fairly new (as of the end of 2017) Telegram library written from the -ground up in Python by -`@delivrance `__ and his -`Pyrogram `__ library. -There isn't really a reason to pick it over Telethon and it'd be kinda -sad to see you go, but it would be nice to know what you miss from each -other library in either one so both can improve. - -Rust -==== - -Yet another work-in-progress implementation, this time for Rust thanks -to `@JuanPotato `__ under the fancy -name of `Vail `__. diff --git a/readthedocs/developing/test-servers.rst b/readthedocs/developing/test-servers.rst index 98f93755..e69de29b 100644 --- a/readthedocs/developing/test-servers.rst +++ b/readthedocs/developing/test-servers.rst @@ -1,37 +0,0 @@ -============ -Test Servers -============ - - -To run Telethon on a test server, use the following code: - -.. code-block:: python - - client = TelegramClient(None, api_id, api_hash) - client.session.set_dc(dc_id, '149.154.167.40', 80) - -You can check your ``'test ip'`` on https://my.telegram.org. - -You should set ``None`` session so to ensure you're generating a new -authorization key for it (it would fail if you used a session where you -had previously connected to another data center). - -Note that port 443 might not work, so you can try with 80 instead. - -Once you're connected, you'll likely be asked to either sign in or sign up. -Remember `anyone can access the phone you -choose `__, -so don't store sensitive data here. - -Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and -``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would -be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five -times, in this case, ``22222`` so we can hardcode that: - -.. code-block:: python - - client = TelegramClient(None, api_id, api_hash) - client.session.set_dc(2, '149.154.167.40', 80) - client.start( - phone='9996621234', code_callback=lambda: '22222' - ) diff --git a/readthedocs/developing/tips-for-porting-the-project.rst b/readthedocs/developing/tips-for-porting-the-project.rst index 69348f9d..e69de29b 100644 --- a/readthedocs/developing/tips-for-porting-the-project.rst +++ b/readthedocs/developing/tips-for-porting-the-project.rst @@ -1,17 +0,0 @@ -============================ -Tips for Porting the Project -============================ - - -If you're going to use the code on this repository to guide you, please -be kind and don't forget to mention it helped you! - -You should start by reading the source code on the `first -release `__ of -the project, and start creating a ``MTProtoSender``. Once this is made, -you should write by hand the code to authenticate on the Telegram's -server, which are some steps required to get the key required to talk to -them. Save it somewhere! Then, simply mimic, or reinvent other parts of -the code, and it will be ready to go within a few days. - -Good luck! diff --git a/readthedocs/developing/understanding-the-type-language.rst b/readthedocs/developing/understanding-the-type-language.rst index 8e5259a7..e69de29b 100644 --- a/readthedocs/developing/understanding-the-type-language.rst +++ b/readthedocs/developing/understanding-the-type-language.rst @@ -1,33 +0,0 @@ -=============================== -Understanding the Type Language -=============================== - - -`Telegram's Type Language `__ -(also known as TL, found on ``.tl`` files) is a concise way to define -what other programming languages commonly call classes or structs. - -Every definition is written as follows for a Telegram object is defined -as follows: - - ``name#id argument_name:argument_type = CommonType`` - -This means that in a single line you know what the ``TLObject`` name is. -You know it's unique ID, and you know what arguments it has. It really -isn't that hard to write a generator for generating code to any -platform! - -The generated code should also be able to *encode* the ``TLObject`` (let -this be a request or a type) into bytes, so they can be sent over the -network. This isn't a big deal either, because you know how the -``TLObject``\ 's are made, and how the types should be serialized. - -You can either write your own code generator, or use the one this -library provides, but please be kind and keep some special mention to -this project for helping you out. - -This is only a introduction. The ``TL`` language is not *that* easy. But -it's not that hard either. You're free to sniff the -``telethon_generator/`` files and learn how to parse other more complex -lines, such as ``flags`` (to indicate things that may or may not be -written at all) and ``vector``\ 's. diff --git a/readthedocs/examples/chats-and-channels.rst b/readthedocs/examples/chats-and-channels.rst index fa485c59..e69de29b 100644 --- a/readthedocs/examples/chats-and-channels.rst +++ b/readthedocs/examples/chats-and-channels.rst @@ -1,258 +0,0 @@ -=============================== -Working with Chats and Channels -=============================== - - -.. note:: - - These examples assume you have read :ref:`full-api`. - -.. contents:: - - -Joining a chat or channel -========================= - -Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a -special form of :tl:`Chat`, which can also be super-groups if -their ``megagroup`` member is ``True``. - - -Joining a public channel -======================== - -Once you have the :ref:`entity ` of the channel you want to join -to, you can make use of the :tl:`JoinChannelRequest` to join such channel: - -.. code-block:: python - - from telethon.tl.functions.channels import JoinChannelRequest - client(JoinChannelRequest(channel)) - - # In the same way, you can also leave such channel - from telethon.tl.functions.channels import LeaveChannelRequest - client(LeaveChannelRequest(input_channel)) - - -For more on channels, check the `channels namespace`__. - - -__ https://tl.telethon.dev/methods/channels/index.html - - -Joining a private chat or channel -================================= - -If all you have is a link like this one: -``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have -enough information to join! The part after the -``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this -example, is the ``hash`` of the chat or channel. Now you can use -:tl:`ImportChatInviteRequest` as follows: - -.. code-block:: python - - from telethon.tl.functions.messages import ImportChatInviteRequest - updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) - - -Adding someone else to such chat or channel -=========================================== - -If you don't want to add yourself, maybe because you're already in, -you can always add someone else with the :tl:`AddChatUserRequest`, which -use is very straightforward, or :tl:`InviteToChannelRequest` for channels: - -.. code-block:: python - - # For normal chats - from telethon.tl.functions.messages import AddChatUserRequest - - # Note that ``user_to_add`` is NOT the name of the parameter. - # It's the user you want to add (``user_id=user_to_add``). - client(AddChatUserRequest( - chat_id, - user_to_add, - fwd_limit=10 # Allow the user to see the 10 last messages - )) - - # For channels (which includes megagroups) - from telethon.tl.functions.channels import InviteToChannelRequest - - client(InviteToChannelRequest( - channel, - [users_to_add] - )) - - -Checking a link without joining -=============================== - -If you don't need to join but rather check whether it's a group or a -channel, you can use the :tl:`CheckChatInviteRequest`, which takes in -the hash of said channel or group. - - -Admin Permissions -================= - -Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest`: - -.. code-block:: python - - from telethon.tl.functions.channels import EditAdminRequest - from telethon.tl.types import ChatAdminRights - - # You need both the channel and who to grant permissions - # They can either be channel/user or input channel/input user. - # - # ChatAdminRights is a list of granted permissions. - # Set to True those you want to give. - rights = ChatAdminRights( - post_messages=None, - add_admins=None, - invite_users=None, - change_info=True, - ban_users=None, - delete_messages=True, - pin_messages=True, - invite_link=None, - edit_messages=None - ) - # Equivalent to: - # rights = ChatAdminRights( - # change_info=True, - # delete_messages=True, - # pin_messages=True - # ) - - # Once you have a ChatAdminRights, invoke it - client(EditAdminRequest(channel, user, rights)) - - # User will now be able to change group info, delete other people's - # messages and pin messages. - # - # In a normal chat, you should do this instead: - from telethon.tl.functions.messages import EditChatAdminRequest - - client(EditChatAdminRequest(chat_id, user, is_admin=True)) - - - -.. note:: - - Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all - parameters to ``True`` to give a user full permissions, as not all - permissions are related to both broadcast channels/megagroups. - - E.g. trying to set ``post_messages=True`` in a megagroup will raise an - error. It is recommended to always use keyword arguments, and to set only - the permissions the user needs. If you don't need to change a permission, - it can be omitted (full list `here`__). - - -Restricting Users -================= - -Similar to how you give or revoke admin permissions, you can edit the -banned rights of a user through :tl:`EditBannedRequest` and its parameter -:tl:`ChatBannedRights`: - -.. code-block:: python - - from telethon.tl.functions.channels import EditBannedRequest - from telethon.tl.types import ChatBannedRights - - from datetime import datetime, timedelta - - # Restricting a user for 7 days, only allowing view/send messages. - # - # Note that it's "reversed". You must set to ``True`` the permissions - # you want to REMOVE, and leave as ``None`` those you want to KEEP. - rights = ChatBannedRights( - until_date=timedelta(days=7), - view_messages=None, - send_messages=None, - send_media=True, - send_stickers=True, - send_gifs=True, - send_games=True, - send_inline=True, - embed_links=True - ) - - # The above is equivalent to - rights = ChatBannedRights( - until_date=datetime.now() + timedelta(days=7), - send_media=True, - send_stickers=True, - send_gifs=True, - send_games=True, - send_inline=True, - embed_links=True - ) - - client(EditBannedRequest(channel, user, rights)) - - -You can use a `datetime.datetime` object for ``until_date=``, -a `datetime.timedelta` or even a Unix timestamp. Note that if you ban -someone for less than 30 seconds or for more than 366 days, Telegram -will consider the ban to actually last forever. This is officially -documented under https://core.telegram.org/bots/api#restrictchatmember. - - -Kicking a member -================ - -Telegram doesn't actually have a request to kick a user from a group. -Instead, you need to restrict them so they can't see messages. Any date -is enough: - -.. code-block:: python - - from telethon.tl.functions.channels import EditBannedRequest - from telethon.tl.types import ChatBannedRights - - client(EditBannedRequest( - channel, user, ChatBannedRights( - until_date=None, - view_messages=True - ) - )) - - -__ https://github.com/Kyle2142 -__ https://github.com/LonamiWebs/Telethon/issues/490 -__ https://tl.telethon.dev/constructors/channel_admin_rights.html - - -Increasing View Count in a Channel -================================== - -It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and -while I don't understand why so many people ask this, the solution is to -use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: - -.. code-block:: python - - - # Obtain `channel' through dialogs or through client.get_entity() or anyhow. - # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. - - client(GetMessagesViewsRequest( - peer=channel, - id=msg_ids, - increment=True - )) - - -Note that you can only do this **once or twice a day** per account, -running this in a loop will obviously not increase the views forever -unless you wait a day between each iteration. If you run it any sooner -than that, the views simply won't be increased. - -__ https://github.com/LonamiWebs/Telethon/issues/233 -__ https://github.com/LonamiWebs/Telethon/issues/305 -__ https://github.com/LonamiWebs/Telethon/issues/409 -__ https://github.com/LonamiWebs/Telethon/issues/447 diff --git a/readthedocs/examples/projects-using-telethon.rst b/readthedocs/examples/projects-using-telethon.rst index bc286350..e69de29b 100644 --- a/readthedocs/examples/projects-using-telethon.rst +++ b/readthedocs/examples/projects-using-telethon.rst @@ -1,85 +0,0 @@ -.. _telethon_projects: - -======================= -Projects using Telethon -======================= - -This page lists some **interesting and useful** real world -examples showcasing what can be built with the library. - -.. note:: - - Do you have an interesting project that uses the library or know of any - that's not listed here? Feel free to leave a comment at - `issue 744 `_ - so it can be included in the next revision of the documentation! - - You can also advertise your bot and its features, in the issue, although - it should be a big project which can be useful for others before being - included here, so please don't feel offended if it can't be here! - - -.. _projects-telegram-export: - -telethon_examples/ -================== - -`telethon_examples `_ / -`LonamiWebs' site `_ - -This documentation is not the only place where you can find useful code -snippets using the library. The main repository also has a folder with -some cool examples (even a Tkinter GUI!) which you can download, edit -and run to learn and play with them. - -@TelethonSnippets -================= - -`@TelethonSnippets `_ - -You can find useful short snippets for Telethon here. - -telegram-export -=============== - -`telegram-export `_ / -`expectocode's GitHub `_ - -A tool to download Telegram data (users, chats, messages, and media) -into a database (and display the saved data). - -.. _projects-mautrix-telegram: - -mautrix-telegram -================ - -`mautrix-telegram `_ / -`maunium's site `_ - -A Matrix-Telegram hybrid puppeting/relaybot bridge. - -.. _projects-telegramtui: - -TelegramTUI -=========== - -`TelegramTUI `_ / -`bad-day's GitHub `_ - -A Telegram client on your terminal. - -tgcloud -======= - -`tgcloud `_ / -`tgcloud's site `_ - -Opensource Telegram based cloud storage. - -tgmount -======= - -`tgmount `_ / -`nktknshn's GitHub `_ - -Mount Telegram dialogs and channels as a Virtual File System. diff --git a/readthedocs/examples/users.rst b/readthedocs/examples/users.rst index 130bd7ae..e69de29b 100644 --- a/readthedocs/examples/users.rst +++ b/readthedocs/examples/users.rst @@ -1,74 +0,0 @@ -===== -Users -===== - - -.. note:: - - These examples assume you have read :ref:`full-api`. - -.. contents:: - - -Retrieving full information -=========================== - -If you need to retrieve the bio, biography or about information for a user -you should use :tl:`GetFullUser`: - - -.. code-block:: python - - from telethon.tl.functions.users import GetFullUserRequest - - full = client(GetFullUserRequest(user)) - # or even - full = client(GetFullUserRequest('username')) - - bio = full.about - - -See :tl:`UserFull` to know what other fields you can access. - - -Updating your name and/or bio -============================= - -The first name, last name and bio (about) can all be changed with the same -request. Omitted fields won't change after invoking :tl:`UpdateProfile`: - -.. code-block:: python - - from telethon.tl.functions.account import UpdateProfileRequest - - client(UpdateProfileRequest( - about='This is a test from Telethon' - )) - - -Updating your username -====================== - -You need to use :tl:`account.UpdateUsername`: - -.. code-block:: python - - from telethon.tl.functions.account import UpdateUsernameRequest - - client(UpdateUsernameRequest('new_username')) - - -Updating your profile photo -=========================== - -The easiest way is to upload a new file and use that as the profile photo -through :tl:`UploadProfilePhoto`: - - -.. code-block:: python - - from telethon.tl.functions.photos import UploadProfilePhotoRequest - - client(UploadProfilePhotoRequest( - client.upload_file('/path/to/some/file') - ))) diff --git a/readthedocs/examples/word-of-warning.rst b/readthedocs/examples/word-of-warning.rst index 5501325f..e69de29b 100644 --- a/readthedocs/examples/word-of-warning.rst +++ b/readthedocs/examples/word-of-warning.rst @@ -1,16 +0,0 @@ -================= -A Word of Warning -================= - -Full API is **not** how you are intended to use the library. You **should** -always prefer the :ref:`client-ref`. However, not everything is implemented -as a friendly method, so full API is your last resort. - -If you select a method in :ref:`client-ref`, you will most likely find an -example for that method. This is how you are intended to use the library. - -Full API **will** break between different minor versions of the library, -since Telegram changes very often. The friendly methods will be kept -compatible between major versions. - -If you need to see real-world examples, please refer to :ref:`telethon_projects`. diff --git a/readthedocs/examples/working-with-messages.rst b/readthedocs/examples/working-with-messages.rst index e08e4cf0..e69de29b 100644 --- a/readthedocs/examples/working-with-messages.rst +++ b/readthedocs/examples/working-with-messages.rst @@ -1,45 +0,0 @@ -===================== -Working with messages -===================== - - -.. note:: - - These examples assume you have read :ref:`full-api`. - -.. contents:: - - -Sending stickers -================ - -Stickers are nothing else than ``files``, and when you successfully retrieve -the stickers for a certain sticker set, all you will have are ``handles`` to -these files. Remember, the files Telegram holds on their servers can be -referenced through this pair of ID/hash (unique per user), and you need to -use this handle when sending a "document" message. This working example will -send yourself the very first sticker you have: - -.. code-block:: python - - # Get all the sticker sets this user has - from telethon.tl.functions.messages import GetAllStickersRequest - sticker_sets = client(GetAllStickersRequest(0)) - - # Choose a sticker set - from telethon.tl.functions.messages import GetStickerSetRequest - from telethon.tl.types import InputStickerSetID - sticker_set = sticker_sets.sets[0] - - # Get the stickers for this sticker set - stickers = client(GetStickerSetRequest( - stickerset=InputStickerSetID( - id=sticker_set.id, access_hash=sticker_set.access_hash - ) - )) - - # Stickers are nothing more than files, so send that - client.send_file('me', stickers.documents[0]) - - -.. _issues: https://github.com/LonamiWebs/Telethon/issues/215 diff --git a/readthedocs/index.rst b/readthedocs/index.rst index 9f28deda..e69de29b 100644 --- a/readthedocs/index.rst +++ b/readthedocs/index.rst @@ -1,119 +0,0 @@ -======================== -Telethon's Documentation -======================== - -.. code-block:: python - - from telethon.sync import TelegramClient, events - - with TelegramClient('name', api_id, api_hash) as client: - client.send_message('me', 'Hello, myself!') - print(client.download_profile_photo('me')) - - @client.on(events.NewMessage(pattern='(?i).*Hello')) - async def handler(event): - await event.reply('Hey!') - - client.run_until_disconnected() - - -* Are you new here? Jump straight into :ref:`installation`! -* Looking for the method reference? See :ref:`client-ref`. -* Did you upgrade the library? Please read :ref:`changelog`. -* Used Telethon before v1.0? See :ref:`compatibility-and-convenience`. -* Coming from Bot API or want to create new bots? See :ref:`botapi`. -* Need the full API reference? https://tl.telethon.dev/. - - -What is this? -------------- - -Telegram is a popular messaging application. This library is meant -to make it easy for you to write Python programs that can interact -with Telegram. Think of it as a wrapper that has already done the -heavy job for you, so you can focus on developing an application. - - -How should I use the documentation? ------------------------------------ - -If you are getting started with the library, you should follow the -documentation in order by pressing the "Next" button at the bottom-right -of every page. - -You can also use the menu on the left to quickly skip over sections. - -.. toctree:: - :hidden: - :caption: First Steps - - basic/installation - basic/signing-in - basic/quick-start - basic/updates - basic/next-steps - -.. toctree:: - :hidden: - :caption: Quick References - - quick-references/faq - quick-references/client-reference - quick-references/events-reference - quick-references/objects-reference - -.. toctree:: - :hidden: - :caption: Concepts - - concepts/strings - concepts/entities - concepts/updates - concepts/sessions - concepts/full-api - concepts/errors - concepts/botapi-vs-mtproto - concepts/asyncio - -.. toctree:: - :hidden: - :caption: Full API Examples - - examples/word-of-warning - examples/chats-and-channels - examples/users - examples/working-with-messages - examples/projects-using-telethon - -.. toctree:: - :hidden: - :caption: Developing - - developing/philosophy.rst - developing/test-servers.rst - developing/project-structure.rst - developing/coding-style.rst - developing/understanding-the-type-language.rst - developing/tips-for-porting-the-project.rst - developing/telegram-api-in-other-languages.rst - -.. toctree:: - :hidden: - :caption: Miscellaneous - - misc/changelog - misc/wall-of-shame.rst - misc/compatibility-and-convenience - -.. toctree:: - :hidden: - :caption: Telethon Modules - - modules/client - modules/events - modules/custom - modules/utils - modules/errors - modules/sessions - modules/network - modules/helpers diff --git a/readthedocs/make.bat b/readthedocs/make.bat index f51f7234..e69de29b 100644 --- a/readthedocs/make.bat +++ b/readthedocs/make.bat @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=Telethon - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/readthedocs/misc/changelog.rst b/readthedocs/misc/changelog.rst index 44fc2b4b..e69de29b 100644 --- a/readthedocs/misc/changelog.rst +++ b/readthedocs/misc/changelog.rst @@ -1,3331 +0,0 @@ -.. _changelog: - - -=========================== -Changelog (Version History) -=========================== - - -This page lists all the available versions of the library, -in chronological order. You should read this when upgrading -the library to know where your code can break, and where -it can take advantage of new goodies! - -.. contents:: List of All Versions - -Documentation Overhaul (v1.8) -============================= - -*Published at 2019/05/30* - -+------------------------+ -| Scheme layer used: 100 | -+------------------------+ - -The documentation has been completely reworked from the ground up, -with awesome new quick references such as :ref:`client-ref` to help -you quickly find what you need! - -Raw methods also warn you when a friendly variant is available, so -that you don't accidentally make your life harder than it has to be. - -In addition, all methods in the client now are fully annotated with type -hints! More work needs to be done, but this should already help a lot when -using Telethon from any IDEs. - -You may have noticed that the patch versions between ``v1.7.2`` to ``v1.7.7`` -have not been documented. This is because patch versions should only contain -bug fixes, no new features or breaking changes. This hasn't been the case in -the past, but from now on, the library will try to adhere more strictly to -the `Semantic Versioning `_ principles. - -If you ever want to look at those bug fixes, please use the appropriated -``git`` command, such as ``git shortlog v1.7.1...v1.7.4``, but in general, -they probably just fixed your issue. - -With that out of the way, let's look at the full change set: - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer changed, so take note if you use the raw API, as it's usual. -* The way photos are downloaded changed during the layer update of the - previous version, and fixing that bug as a breaking change in itself. - `client.download_media() ` - now offers a different way to deal with thumbnails. - - -Additions -~~~~~~~~~ - -* New `Message.file ` property! - Now you can trivially access `message.file.id ` - to get the file ID of some media, or even ``print(message.file.name)``. -* Archiving dialogs with `Dialog.archive() ` - or `client.edit_folder() ` - is now possible. -* New cleaned-up method to stream downloads with `client.iter_download() - `, which offers - a lot of flexibility, such as arbitrary offsets for efficient seeking. -* `Dialog.delete() ` has existed - for a while, and now `client.delete_dialog() - ` exists too so you - can easily leave chats or delete dialogs without fetching all dialogs. -* Some people or chats have a lot of profile photos. You can now iterate - over all of them with the new `client.iter_profile_photos() - ` method. -* You can now annoy everyone with the new `Message.pin(notify=True) - `! The client has its own - variant too, called `client.pin_message() - `. - - -Bug fixes -~~~~~~~~~ - -* Correctly catch and raise all RPC errors. -* Downloading stripped photos wouldn't work correctly. -* Under some systems, ``libssl`` would fail to load earlier than - expected, causing the library to fail when being imported. -* `conv.get_response() ` - after ID 0 wasn't allowed when it should. -* `InlineBuilder ` only worked - with local files, but files from anywhere are supported. -* Accessing the text property from a raw-API call to fetch :tl:`Message` would fail - (any any other property that needed the client). -* Database is now upgraded if the version was lower, not different. - From now on, this should help with upgrades and downgrades slightly. -* Fixed saving ``pts`` and session-related stuff. -* Disconnection should not raise any errors. -* Invite links of the form ``tg://join?invite=`` now work. -* `client.iter_participants(search=...) ` - now works on private chats again. -* Iterating over messages in reverse with a date as offset wouldn't work. -* The conversation would behave weirdly when a timeout occurred. - - -Enhancements -~~~~~~~~~~~~ - -* ``telethon`` now re-export all the goodies that you commonly need when - using the library, so e.g. ``from telethon import Button`` will now work. -* ``telethon.sync`` now re-exports everything from ``telethon``, so that - you can trivially import from just one place everything that you need. -* More attempts at reducing CPU usage after automatically fetching missing - entities on events. This isn't a big deal, even if it sounds like one. -* Hexadecimal invite links are now supported. You didn't need them, but - they will now work. - -Internal Changes -~~~~~~~~~~~~~~~~ - -* Deterministic code generation. This is good for ``diff``. -* On Python 3.7 and above, we properly close the connection. -* A lot of micro-optimization. -* Fixes to bugs introduced while making this release. -* Custom commands on ``setup.py`` are nicer to use. - - - -Fix-up for Photo Downloads (v1.7.1) -=================================== - -*Published at 2019/04/24* - -Telegram changed the way thumbnails (which includes photos) are downloaded, -so you can no longer use a :tl:`PhotoSize` alone to download a particular -thumbnail size (this is a **breaking change**). - -Instead, you will have to specify the new ``thumb`` parameter in -`client.download_media() ` -to download a particular thumbnail size. This addition enables you to easily -download thumbnails from documents, something you couldn't do easily before. - - -Easier Events (v1.7) -==================== - -*Published at 2019/04/22* - -+-----------------------+ -| Scheme layer used: 98 | -+-----------------------+ - -If you have been using Telethon for a while, you probably know how annoying -the "Could not find the input entity for…" error can be. In this new version, -the library will try harder to find the input entity for you! - -That is, instead of doing: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(await event.get_input_sender()) - # ...... needs await, it's a method ^^^^^ ^^ - -You can now do: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(event.input_sender) - # ...... no await, it's a property! ^ - # It's also 12 characters shorter :) - -And even the following will hopefully work: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(event.sender_id) - -A lot of people use IDs thinking this is the right way of doing it. Ideally, -you would always use ``input_*``, not ``sender`` or ``sender_id`` (and the -same applies to chats). But, with this change, IDs will work just the same as -``input_*`` inside events. - -**This feature still needs some more testing**, so please do open an issue -if you find strange behaviour. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer changed, and a lot of things did too. If you are using - raw API, you should be careful with this. In addition, some attributes - weren't of type ``datetime`` when they should be, which has been fixed. -* Due to the layer change, you can no longer download photos with just - their :tl:`PhotoSize`. Version 1.7.1 introduces a new way to download - thumbnails to work around this issue. -* `client.disconnect() - ` - is now asynchronous again. This means you need to ``await`` it. You - don't need to worry about this if you were using ``with client`` or - `client.run_until_disconnected - `. - This should prevent the "pending task was destroyed" errors. - -Additions -~~~~~~~~~ - -* New in-memory cache for input entities. This should mean a lot less - of disk look-ups. -* New `client.action ` method - to easily indicate that you are doing some chat action: - - .. code-block:: python - - async with client.action(chat, 'typing'): - await asyncio.sleep(2) # type for 2 seconds - await client.send_message(chat, 'Hello world! I type slow ^^') - - You can also easily use this for sending files, playing games, etc. - - -New bugs -~~~~~~~~ - -* Downloading photos is broken. This is fixed in v1.7.1. - -Bug fixes -~~~~~~~~~ - -* Fix sending photos from streams/bytes. -* Fix unhandled error when sending requests that were too big. -* Fix edits that arrive too early on conversations. -* Fix `client.edit_message() - ` - when trying to edit a file. -* Fix method calls on the objects returned by `client.iter_dialogs() - `. -* Attempt at fixing `client.iter_dialogs() - ` missing many dialogs. -* ``offset_date`` in `client.iter_messages() - ` was being - ignored in some cases. This has been worked around. -* Fix `callback_query.edit() - `. -* Fix `CallbackQuery(func=...) ` - was being ignored. -* Fix `UserUpdate ` not working for - "typing" (and uploading file, etc.) status. -* Fix library was not expecting ``IOError`` from PySocks. -* Fix library was raising a generic ``ConnectionError`` - and not the one that actually occurred. -* Fix the ``blacklist_chats`` parameter in `MessageRead - ` not working as intended. -* Fix `client.download_media(contact) - `. -* Fix mime type when sending ``mp3`` files. -* Fix forcibly getting the sender or chat from events would - not always return all their information. -* Fix sending albums with `client.send_file() - ` was not returning - the sent messages. -* Fix forwarding albums with `client.forward_messages() - `. -* Some fixes regarding filtering updates from chats. -* Attempt at preventing duplicated updates. -* Prevent double auto-reconnect. - - -Enhancements -~~~~~~~~~~~~ - -* Some improvements related to proxy connections. -* Several updates and improvements to the documentation, - such as optional dependencies now being properly listed. -* You can now forward messages from different chats directly with - `client.forward_messages `. - - -Tidying up Internals (v1.6) -=========================== - -*Published at 2019/02/27* - -+-----------------------+ -| Scheme layer used: 95 | -+-----------------------+ - -First things first, sorry for updating the layer in the previous patch -version. That should only be done between major versions ideally, but -due to how Telegram works, it's done between minor versions. However raw -API has and will always be considered "unsafe", this meaning that you -should always use the convenience client methods instead. These methods -don't cover the full API yet, so pull requests are welcome. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer update, of course. This didn't really need a mention here. -* You can no longer pass a ``batch_size`` when iterating over messages. - No other method exposed this parameter, and it was only meant for testing - purposes. Instead, it's now a private constant. -* ``client.iter_*`` methods no longer have a ``_total`` parameter which - was supposed to be private anyway. Instead, they return a new generator - object which has a ``.total`` attribute: - - .. code-block:: python - - it = client.iter_messages(chat) - for i, message in enumerate(it, start=1): - percentage = i / it.total - print('{:.2%} {}'.format(percentage, message.text)) - -Additions -~~~~~~~~~ - -* You can now pass ``phone`` and ``phone_code_hash`` in `client.sign_up - `, although you probably don't - need that. -* Thanks to the overhaul of all ``client.iter_*`` methods, you can now do: - - .. code-block:: python - - for message in reversed(client.iter_messages('me')): - print(message.text) - -Bug fixes -~~~~~~~~~ - -* Fix `telethon.utils.resolve_bot_file_id`, which wasn't working after - the layer update (so you couldn't send some files by bot file IDs). -* Fix sending albums as bot file IDs (due to image detection improvements). -* Fix `takeout() ` failing - when they need to download media from other DCs. -* Fix repeatedly calling `conversation.get_response() - ` when many - messages arrived at once (i.e. when several of them were forwarded). -* Fixed connecting with `ConnectionTcpObfuscated - `. -* Fix `client.get_peer_id('me') - `. -* Fix warning of "missing sqlite3" when in reality it just had wrong tables. -* Fix a strange error when using too many IDs in `client.delete_messages() - `. -* Fix `client.send_file ` - with the result of `client.upload_file - `. -* When answering inline results, their order was not being preserved. -* Fix `events.ChatAction ` - detecting user leaves as if they were kicked. - -Enhancements -~~~~~~~~~~~~ - -* Cleared up some parts of the documentation. -* Improved some auto-casts to make life easier. -* Improved image detection. Now you can easily send ``bytes`` - and streams of images as photos, unless you force document. -* Sending images as photos that are too large will now be resized - before uploading, reducing the time it takes to upload them and - also avoiding errors when the image was too large (as long as - ``pillow`` is installed). The images will remain unchanged if you - send it as a document. -* Treat ``errors.RpcMcgetFailError`` as a temporary server error - to automatically retry shortly. This works around most issues. - -Internal changes -~~~~~~~~~~~~~~~~ - -* New common way to deal with retries (``retry_range``). -* Cleaned up the takeout client. -* Completely overhauled asynchronous generators. - -Layer Update (v1.5.5) -===================== - -*Published at 2019/01/14* - -+-----------------------+ -| Scheme layer used: 93 | -+-----------------------+ - -There isn't an entry for v1.5.4 because it contained only one hot-fix -regarding loggers. This update is slightly bigger so it deserves mention. - -Additions -~~~~~~~~~ - -* New ``supports_streaming`` parameter in `client.send_file - `. - -Bug fixes -~~~~~~~~~ - -* Dealing with mimetypes should cause less issues in systems like Windows. -* Potentially fix alternative session storages that had issues with dates. - -Enhancements -~~~~~~~~~~~~ - -* Saner timeout defaults for conversations. -* ``Path``-like files are now supported for thumbnails. -* Added new hot-keys to the online documentation at - https://tl.telethon.dev/ such as ``/`` to search. - Press ``?`` to view them all. - - -Bug Fixes (v1.5.3) -================== - -*Published at 2019/01/14* - -Several bug fixes and some quality of life enhancements. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* `message.edit ` now respects - the previous message buttons or link preview being hidden. If you want to - toggle them you need to explicitly set them. This is generally the desired - behaviour, but may cause some bots to have buttons when they shouldn't. - -Additions -~~~~~~~~~ - -* You can now "hide_via" when clicking on results from `client.inline_query - ` to @bing and @gif. -* You can now further configure the logger Telethon uses to suit your needs. - -Bug fixes -~~~~~~~~~ - -* Fixes for ReadTheDocs to correctly build the documentation. -* Fix :tl:`UserEmpty` not being expected when getting the input variant. -* The message object returned when sending a message with buttons wouldn't - always contain the :tl:`ReplyMarkup`. -* Setting email when configuring 2FA wasn't properly supported. -* ``utils.resolve_bot_file_id`` now works again for photos. - -Enhancements -~~~~~~~~~~~~ - -* Chat and channel participants can now be used as peers. -* Reworked README and examples at - https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples - - -Takeout Sessions (v1.5.2) -========================= - -*Published at 2019/01/05* - -You can now easily start takeout sessions (also known as data export sessions) -through `client.takeout() `. -Some of the requests will have lower flood limits when done through the -takeout session. - -Bug fixes -~~~~~~~~~ - -* The new `AdminLogEvent ` - had a bug that made it unusable. -* `client.iter_dialogs() ` - will now locally check for the offset date, since Telegram ignores it. -* Answering inline queries with media no works properly. You can now use - the library to create inline bots and send stickers through them! - - -object.to_json() (v1.5.1) -========================= - -*Published at 2019/01/03* - -The library already had a way to easily convert the objects the API returned -into dictionaries through ``object.to_dict()``, but some of the fields are -dates or ``bytes`` which JSON can't serialize directly. - -For convenience, a new ``object.to_json()`` has been added which will by -default format both of those problematic types into something sensible. - -Additions -~~~~~~~~~ - -* New `client.iter_admin_log() - ` method. - -Bug fixes -~~~~~~~~~ - -* `client.is_connected() - ` - would be wrong when the initial connection failed. -* Fixed ``UnicodeDecodeError`` when accessing the text of messages - with malformed offsets in their entities. -* Fixed `client.get_input_entity() - ` for integer IDs - that the client has not seen before. - -Enhancements -~~~~~~~~~~~~ - -* You can now configure the reply markup when using `Button - ` as a bot. -* More properties for `Message - ` to make accessing media convenient. -* Downloading to ``file=bytes`` will now return a ``bytes`` object - with the downloaded media. - - -Polls with the Latest Layer (v1.5) -================================== - -*Published at 2018/12/25* - -+-----------------------+ -| Scheme layer used: 91 | -+-----------------------+ - -This version doesn't really bring many new features, but rather focuses on -updating the code base to support the latest available Telegram layer, 91. -This layer brings polls, and you can create and manage them through Telethon! - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer change from 82 to 91 changed a lot of things in the raw API, - so be aware that if you rely on raw API calls, you may need to update - your code, in particular **if you work with files**. They have a new - ``file_reference`` parameter that you must provide. - -Additions -~~~~~~~~~ - -* New `client.is_bot() ` method. - -Bug fixes -~~~~~~~~~ - -* Markdown and HTML parsing now behave correctly with leading whitespace. -* HTTP connection should now work correctly again. -* Using ``caption=None`` would raise an error instead of setting no caption. -* ``KeyError`` is now handled properly when forwarding messages. -* `button.click() ` - now works as expected for :tl:`KeyboardButtonGame`. - -Enhancements -~~~~~~~~~~~~ - -* Some improvements to the search in the full API and generated examples. -* Using entities with ``access_hash = 0`` will now work in more cases. - -Internal changes -~~~~~~~~~~~~~~~~ - -* Some changes to the documentation and code generation. -* 2FA code was updated to work under the latest layer. - - -Error Descriptions in CSV files (v1.4.3) -======================================== - -*Published at 2018/12/04* - -While this may seem like a minor thing, it's a big usability improvement. - -Anyone who wants to update the documentation for known errors, or whether -some methods can be used as a bot, user or both, can now be easily edited. -Everyone is encouraged to help document this better! - -Bug fixes -~~~~~~~~~ - -* ``TimeoutError`` was not handled during automatic reconnects. -* Getting messages by ID using :tl:`InputMessageReplyTo` could fail. -* Fixed `message.get_reply_message - ` - as a bot when a user replied to a different bot. -* Accessing some document properties in a `Message - ` would fail. - -Enhancements -~~~~~~~~~~~~ - -* Accessing `events.ChatAction ` - properties such as input users may now work in more cases. - -Internal changes -~~~~~~~~~~~~~~~~ - -* Error descriptions and information about methods is now loaded - from a CSV file instead of being part of several messy JSON files. - - -Bug Fixes (v1.4.2) -================== - -*Published at 2018/11/24* - -This version also includes the v1.4.1 hot-fix, which was a single -quick fix and didn't really deserve an entry in the changelog. - -Bug fixes -~~~~~~~~~ - -* Authorization key wouldn't be saved correctly, requiring re-login. -* Conversations with custom events failed to be cancelled. -* Fixed ``telethon.sync`` when using other threads. -* Fix markdown/HTML parser from failing with leading/trailing whitespace. -* Fix accessing ``chat_action_event.input_user`` property. -* Potentially improved handling unexpected disconnections. - - -Enhancements -~~~~~~~~~~~~ - -* Better default behaviour for `client.send_read_acknowledge - `. -* Clarified some points in the documentation. -* Clearer errors for ``utils.get_peer*``. - - -Connection Overhaul (v1.4) -========================== - -*Published at 2018/11/03* - -Yet again, a lot of work has been put into reworking the low level connection -classes. This means ``asyncio.open_connection`` is now used correctly and the -errors it can produce are handled properly. The separation between packing, -encrypting and network is now abstracted away properly, so reasoning about -the code is easier, making it more maintainable. - -As a user, you shouldn't worry about this, other than being aware that quite -a few changes were made in the insides of the library and you should report -any issues that you encounter with this version if any. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The threaded version of the library will no longer be maintained, primarily - because it never was properly maintained anyway. If you have old code, stick - with old versions of the library, such as ``0.19.1.6``. -* Timeouts no longer accept ``timedelta``. Simply use seconds. -* The ``callback`` parameter from `telethon.tl.custom.button.Button.inline()` - was removed, since it had always been a bad idea. Adding the callback there - meant a lot of extra work for every message sent, and only registering it - after the first message was sent! Instead, use - `telethon.events.callbackquery.CallbackQuery`. - - -Additions -~~~~~~~~~ - -* New `dialog.delete() ` method. -* New `conversation.cancel() - ` method. -* New ``retry_delay`` delay for the client to be used on auto-reconnection. - - -Bug fixes -~~~~~~~~~ - -* Fixed `Conversation.wait_event() - `. -* Fixed replying with photos/documents on inline results. -* `client.is_user_authorized() - ` now works - correctly after `client.log_out() - `. -* `dialog.is_group ` now works for - :tl:`ChatForbidden`. -* Not using ``async with`` when needed is now a proper error. -* `events.CallbackQuery ` - with string regex was not working properly. -* `client.get_entity('me') ` - now works again. -* Empty codes when signing in are no longer valid. -* Fixed file cache for in-memory sessions. - - -Enhancements -~~~~~~~~~~~~ - -* Support ``next_offset`` in `inline_query.answer() - `. -* Support ```` mentions in HTML parse mode. -* New auto-casts for :tl:`InputDocument` and :tl:`InputChatPhoto`. -* Conversations are now exclusive per-chat by default. -* The request that caused a RPC error is now shown in the error message. -* New full API examples in the generated documentation. -* Fixed some broken links in the documentation. -* `client.disconnect() - ` - is now synchronous, but you can still ``await`` it for consistency - or compatibility. - - -Event Templates (v1.3) -====================== - -*Published at 2018/09/22* - - -If you have worked with Flask templates, you will love this update, -since it gives you the same features but even more conveniently: - -.. code-block:: python - - # handlers/welcome.py - from telethon import events - - @events.register(events.NewMessage('(?i)hello')) - async def handler(event): - client = event.client - await event.respond('Hi!') - await client.send_message('me', 'Sent hello to someone') - - -This will `register ` the ``handler`` callback -to handle new message events. Note that you didn't add this to any client -yet, and this is the key point: you don't need a client to define handlers! -You can add it later: - -.. code-block:: python - - # main.py - from telethon import TelegramClient - import handlers.welcome - - with TelegramClient(...) as client: - # This line adds the handler we defined before for new messages - client.add_event_handler(handlers.welcome.handler) - client.run_until_disconnected() - - -This should help you to split your big code base into a more modular design. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* ``.sender`` is the ``.chat`` when the message is sent in a broadcast - channel. This makes sense, because the sender of the message was the - channel itself, but you now must take into consideration that it may - be either a :tl:`User` or :tl:`Channel` instead of being ``None``. - - -Additions -~~~~~~~~~ - -* New ``MultiError`` class when invoking many requests at once - through ``client([requests])``. -* New custom ``func=`` on all events. These will receive the entire - event, and a good usage example is ``func=lambda e: e.is_private``. -* New ``.web_preview`` field on messages. The ``.photo`` and ``.document`` - will also return the media in the web preview if any, for convenience. -* Callback queries now have a ``.chat`` in most circumstances. - - -Bug fixes -~~~~~~~~~ - -* Running code with `python3 -O` would remove critical code from asserts. -* Fix some rare ghost disconnections after reconnecting. -* Fix strange behavior for `send_message(chat, Message, reply_to=foo) - `. -* The ``loop=`` argument was being pretty much ignored. -* Fix ``MemorySession`` file caching. -* The logic for getting entities from their username is now correct. -* Fixes for sending stickers from ``.webp`` files in Windows, again. -* Fix disconnection without being logged in. -* Retrieving media from messages would fail. -* Getting some messages by ID on private chats. - - -Enhancements -~~~~~~~~~~~~ - -* `iter_participants ` - will now use its ``search=`` as a symbol set when ``aggressive=True``, - so you can do ``client.get_participants(group, aggressive=True, - search='абвгдеёжзийклмнопрст')``. -* The ``StringSession`` supports custom encoding. -* Callbacks for `telethon.client.auth.AuthMethods.start` can be ``async``. - - -Internal changes -~~~~~~~~~~~~~~~~ - -* Cherry-picked a commit to use ``asyncio.open_connection`` in the lowest - level of the library. Do open issues if this causes trouble, but it should - otherwise improve performance and reliability. -* Building and resolving events overhaul. - - -Conversations, String Sessions and More (v1.2) -============================================== - -*Published at 2018/08/14* - - -This is a big release! Quite a few things have been added to the library, -such as the new `Conversation `. -This makes it trivial to get tokens from `@BotFather `_: - -.. code-block:: python - - from telethon.tl import types - - with client.conversation('BotFather') as conv: - conv.send_message('/mybots') - message = conv.get_response() - message.click(0) - message = conv.get_edit() - message.click(0) - message = conv.get_edit() - for _, token in message.get_entities_text(types.MessageEntityCode): - print(token) - - -In addition to that, you can now easily load and export session files -without creating any on-disk file thanks to the ``StringSession``: - -.. code-block:: python - - from telethon.sessions import StringSession - string = StringSession.save(client.session) - -Check out :ref:`sessions` for more details. - -For those who aren't able to install ``cryptg``, the support for ``libssl`` -has been added back. While interfacing ``libssl`` is not as fast, the speed -when downloading and sending files should really be noticeably faster. - -While those are the biggest things, there are still more things to be -excited about. - - -Additions -~~~~~~~~~ - -- The mentioned method to start a new `client.conversation - `. -- Implemented global search through `client.iter_messages - ` - with ``None`` entity. -- New `client.inline_query ` - method to perform inline queries. -- Bot-API-style ``file_id`` can now be used to send files and download media. - You can also access `telethon.utils.resolve_bot_file_id` and - `telethon.utils.pack_bot_file_id` to resolve and create these - file IDs yourself. Note that each user has its own ID for each file - so you can't use a bot's ``file_id`` with your user, except stickers. -- New `telethon.utils.get_peer`, useful when you expect a :tl:`Peer`. - -Bug fixes -~~~~~~~~~ - -- UTC timezone for `telethon.events.userupdate.UserUpdate`. -- Bug with certain input parameters when iterating messages. -- RPC errors without parent requests caused a crash, and better logging. -- ``incoming = outgoing = True`` was not working properly. -- Getting a message's ID was not working. -- File attributes not being inferred for ``open()``'ed files. -- Use ``MemorySession`` if ``sqlite3`` is not installed by default. -- Self-user would not be saved to the session file after signing in. -- `client.catch_up() ` - seems to be functional again. - - -Enhancements -~~~~~~~~~~~~ - -- Updated documentation. -- Invite links will now use cache, so using them as entities is cheaper. -- You can reuse message buttons to send new messages with those buttons. -- ``.to_dict()`` will now work even on invalid ``TLObject``'s. - - -Better Custom Message (v1.1.1) -============================== - -*Published at 2018/07/23* - -The `custom.Message ` class has been -rewritten in a cleaner way and overall feels less hacky in the library. -This should perform better than the previous way in which it was patched. - -The release is primarily intended to test this big change, but also fixes -**Python 3.5.2 compatibility** which was broken due to a trailing comma. - - -Bug fixes -~~~~~~~~~ - -- Using ``functools.partial`` on event handlers broke updates - if they had uncaught exceptions. -- A bug under some session files where the sender would export - authorization for the same data center, which is unsupported. -- Some logical bugs in the custom message class. - - -Bot Friendly (v1.1) -=================== - -*Published at 2018/07/21* - -Two new event handlers to ease creating normal bots with the library, -namely `events.InlineQuery ` -and `events.CallbackQuery ` -for handling ``@InlineBot queries`` or reacting to a button click. For -this second option, there is an even better way: - -.. code-block:: python - - from telethon.tl.custom import Button - - async def callback(event): - await event.edit('Thank you!') - - bot.send_message(chat, 'Hello!', - buttons=Button.inline('Click me', callback)) - - -You can directly pass the callback when creating the button. - -This is fine for small bots but it will add the callback every time -you send a message, so you probably should do this instead once you -are done testing: - -.. code-block:: python - - markup = bot.build_reply_markup(Button.inline('Click me', callback)) - bot.send_message(chat, 'Hello!', buttons=markup) - - -And yes, you can create more complex button layouts with lists: - -.. code-block:: python - - from telethon import events - - global phone = '' - - @bot.on(events.CallbackQuery) - async def handler(event): - global phone - if event.data == b'<': - phone = phone[:-1] - else: - phone += event.data.decode('utf-8') - - await event.answer('Phone is now {}'.format(phone)) - - markup = bot.build_reply_markup([ - [Button.inline('1'), Button.inline('2'), Button.inline('3')], - [Button.inline('4'), Button.inline('5'), Button.inline('6')], - [Button.inline('7'), Button.inline('8'), Button.inline('9')], - [Button.inline('+'), Button.inline('0'), Button.inline('<')], - ]) - bot.send_message(chat, 'Enter a phone', buttons=markup) - - -(Yes, there are better ways to do this). Now for the rest of things: - - -Additions -~~~~~~~~~ - -- New `custom.Button ` class - to help you create inline (or normal) reply keyboards. You - must sign in as a bot to use the ``buttons=`` parameters. -- New events usable if you sign in as a bot: `events.InlineQuery - ` and `events.CallbackQuery - `. -- New ``silent`` parameter when sending messages, usable in broadcast channels. -- Documentation now has an entire section dedicate to how to use - the client's friendly methods at *(removed broken link)*. - -Bug fixes -~~~~~~~~~ - -- Empty ``except`` are no longer used which means - sending a keyboard interrupt should now work properly. -- The ``pts`` of incoming updates could be ``None``. -- UTC timezone information is properly set for read ``datetime``. -- Some infinite recursion bugs in the custom message class. -- :tl:`Updates` was being dispatched to raw handlers when it shouldn't. -- Using proxies and HTTPS connection mode may now work properly. -- Less flood waits when downloading media from different data centers, - and the library will now detect them even before sending requests. - -Enhancements -~~~~~~~~~~~~ - -- Interactive sign in now supports signing in with a bot token. -- ``timedelta`` is now supported where a date is expected, which - means you can e.g. ban someone for ``timedelta(minutes=5)``. -- Events are only built once and reused many times, which should - save quite a few CPU cycles if you have a lot of the same type. -- You can now click inline buttons directly if you know their data. - -Internal changes -~~~~~~~~~~~~~~~~ - -- When downloading media, the right sender is directly - used without previously triggering migrate errors. -- Code reusing for getting the chat and the sender, - which easily enables this feature for new types. - - -New HTTP(S) Connection Mode (v1.0.4) -==================================== - -*Published at 2018/07/09* - -This release implements the HTTP connection mode to the library, which -means certain proxies that only allow HTTP connections should now work -properly. You can use it doing the following, like any other mode: - -.. code-block:: python - - from telethon import TelegramClient, sync - from telethon.network import ConnectionHttp - - client = TelegramClient(..., connection=ConnectionHttp) - with client: - client.send_message('me', 'Hi!') - - -Additions -~~~~~~~~~ - -- ``add_mark=`` is now back on ``utils.get_input_peer`` and also on - `client.get_input_entity() `. -- New `client.get_peer_id ` - convenience for ``utils.get_peer_id(await client.get_input_entity(peer))``. - - -Bug fixes -~~~~~~~~~ - -- If several `TLMessage` in a `MessageContainer` exceeds 1MB, it will no - longer be automatically turned into one. This basically means that e.g. - uploading 10 file parts at once will work properly again. -- Documentation fixes and some missing ``await``. -- Revert named argument for `client.forward_messages - ` - -Enhancements -~~~~~~~~~~~~ - -- New auto-casts to :tl:`InputNotifyPeer` and ``chat_id``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Outgoing `TLMessage` are now pre-packed so if there's an error when - serializing the raw requests, the library will no longer swallow it. - This also means re-sending packets doesn't need to re-pack their bytes. - - - -Iterate Messages in Reverse (v1.0.3) -==================================== - -*Published at 2018/07/04* - -+-----------------------+ -| Scheme layer used: 82 | -+-----------------------+ - -Mostly bug fixes, but now there is a new parameter on `client.iter_messages -` to support reversing -the order in which messages are returned. - -Additions -~~~~~~~~~ - -- The mentioned ``reverse`` parameter when iterating over messages. -- A new ``sequential_updates`` parameter when creating the client - for updates to be processed sequentially. This is useful when you - need to make sure that all updates are processed in order, such - as a script that only forwards incoming messages somewhere else. - -Bug fixes -~~~~~~~~~ - -- Count was always ``None`` for `message.button_count - `. -- Some fixes when disconnecting upon dropping the client. -- Support for Python 3.4 in the sync version, and fix media download. -- Some issues with events when accessing the input chat or their media. -- Hachoir wouldn't automatically close the file after reading its metadata. -- Signing in required a named ``code=`` parameter, but usage - without a name was really widespread so it has been reverted. - - -Bug Fixes (v1.0.2) -================== - -*Published at 2018/06/28* - -Updated some asserts and parallel downloads, as well as some fixes for sync. - - -Bug Fixes (v1.0.1) -================== - -*Published at 2018/06/27* - -And as usual, every major release has a few bugs that make the library -unusable! This quick update should fix those, namely: - -Bug fixes -~~~~~~~~~ - -- `client.start() ` was completely - broken due to a last-time change requiring named arguments everywhere. -- Since the rewrite, if your system clock was wrong, the connection would - get stuck in an infinite "bad message" loop of responses from Telegram. -- Accessing the buttons of a custom message wouldn't work in channels, - which lead to fix a completely different bug regarding starting bots. -- Disconnecting could complain if the magic ``telethon.sync`` was imported. -- Successful automatic reconnections now ask Telegram to send updates to us - once again as soon as the library is ready to listen for them. - - -Synchronous magic (v1.0) -======================== - -*Published at 2018/06/27* - -.. important:: - - If you come from Telethon pre-1.0 you **really** want to read - :ref:`compatibility-and-convenience` to port your scripts to - the new version. - -The library has been around for well over a year. A lot of improvements have -been made, a lot of user complaints have been fixed, and a lot of user desires -have been implemented. It's time to consider the public API as stable, and -remove some of the old methods that were around until now for compatibility -reasons. But there's one more surprise! - -There is a new magic ``telethon.sync`` module to let you use **all** the -methods in the :ref:`TelegramClient ` (and the types returned -from its functions) in a synchronous way, while using `asyncio` behind -the scenes! This means you're now able to do both of the following: - -.. code-block:: python - - import asyncio - - async def main(): - await client.send_message('me', 'Hello!') - - asyncio.get_event_loop().run_until_complete(main()) - - # ...can be rewritten as: - - from telethon import sync - client.send_message('me', 'Hello!') - -Both ways can coexist (you need to ``await`` if the loop is running). - -You can also use the magic ``sync`` module in your own classes, and call -``sync.syncify(cls)`` to convert all their ``async def`` into magic variants. - - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- ``message.get_fwd_sender`` is now in `message.forward - `. -- ``client.idle`` is now `client.run_until_disconnected() - ` -- ``client.add_update_handler`` is now `client.add_event_handler - ` -- ``client.remove_update_handler`` is now `client.remove_event_handler - ` -- ``client.list_update_handlers`` is now `client.list_event_handlers - ` -- ``client.get_message_history`` is now `client.get_messages - ` -- ``client.send_voice_note`` is now `client.send_file - ` with ``is_voice=True``. -- ``client.invoke()`` is now ``client(...)``. -- ``report_errors`` has been removed since it's currently not used, - and ``flood_sleep_threshold`` is now part of the client. -- The ``update_workers`` and ``spawn_read_thread`` arguments are gone. - Simply remove them from your code when you create the client. -- Methods with a lot of arguments can no longer be used without specifying - their argument. Instead you need to use named arguments. This improves - readability and not needing to learn the order of the arguments, which - can also change. - - -Additions -~~~~~~~~~ - -- `client.send_file ` now - accepts external ``http://`` and ``https://`` URLs. -- You can use the :ref:`TelegramClient ` inside of ``with`` - blocks, which will `client.start() ` - and `disconnect() ` - the client for you: - - .. code-block:: python - - from telethon import TelegramClient, sync - - with TelegramClient(name, api_id, api_hash) as client: - client.send_message('me', 'Hello!') - - Convenience at its maximum! You can even chain the `.start() - ` method since - it returns the instance of the client: - - .. code-block:: python - - with TelegramClient(name, api_id, api_hash).start(bot_token=token) as bot: - bot.send_message(chat, 'Hello!') - - -Bug fixes -~~~~~~~~~ - -- There were some ``@property async def`` left, and some ``await property``. -- "User joined" event was being treated as "User was invited". -- SQLite's cursor should not be closed properly after usage. -- ``await`` the updates task upon disconnection. -- Some bug in Python 3.5.2's `asyncio` causing 100% CPU load if you - forgot to call `client.disconnect() - `. - The method is called for you on object destruction, but you still should - disconnect manually or use a ``with`` block. -- Some fixes regarding disconnecting on client deletion and properly - saving the authorization key. -- Passing a class to `message.get_entities_text - ` now works properly. -- Iterating messages from a specific user in private messages now works. - -Enhancements -~~~~~~~~~~~~ - -- Both `client.start() ` and - `client.run_until_disconnected() - ` can - be ran in both a synchronous way (without starting the loop manually) - or from an ``async def`` where they need to have an ``await``. - - -Core Rewrite in asyncio (v1.0-rc1) -================================== - -*Published at 2018/06/24* - -+-----------------------+ -| Scheme layer used: 81 | -+-----------------------+ - -This version is a major overhaul of the library internals. The core has -been rewritten, cleaned up and refactored to fix some oddities that have -been growing inside the library. - -This means that the code is easier to understand and reason about, -including the code flow such as conditions, exceptions, where to -reconnect, how the library should behave, and separating different -retry types such as disconnections or call fails, but it also means -that **some things will necessarily break** in this version. - -All requests that touch the network are now methods and need to -have their ``await`` (or be ran until their completion). - -Also, the library finally has the simple logo it deserved: a carefully -hand-written ``.svg`` file representing a T following Python's colours. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- If you relied on internals like the ``MtProtoSender`` and the - ``TelegramBareClient``, both are gone. They are now `MTProtoSender - ` and `TelegramBaseClient - ` and they behave - differently. -- Underscores have been renamed from filenames. This means - ``telethon.errors.rpc_error_list`` won't work, but you should - have been using `telethon.errors` all this time instead. -- `client.connect ` - no longer returns ``True`` on success. Instead, you should ``except`` the - possible ``ConnectionError`` and act accordingly. This makes it easier to - not ignore the error. -- You can no longer set ``retries=n`` when calling a request manually. The - limit works differently now, and it's done on a per-client basis. -- Accessing `.sender `, - `.chat ` and similar may *not* work - in events anymore, since previously they could access the network. The new - rule is that properties are not allowed to make API calls. You should use - `.get_sender() `, - `.get_chat() ` instead while - using events. You can safely access properties if you get messages through - `client.get_messages() ` - or other methods in the client. -- The above point means ``reply_message`` is now `.get_reply_message() - `, and ``fwd_from_entity`` - is now `get_fwd_sender() `. - Also ``forward`` was gone in the previous version, and you should be using - ``fwd_from`` instead. - - -Additions -~~~~~~~~~ - -- Telegram's Terms Of Service are now accepted when creating a new account. - This can possibly help avoid bans. This has no effect for accounts that - were created before. -- The `method reference `_ now shows - which methods can be used if you sign in with a ``bot_token``. -- There's a new `client.disconnected - ` future - which you can wait on. When a disconnection occurs, you will now, instead - letting it happen in the background. -- More configurable retries parameters, such as auto-reconnection, retries - when connecting, and retries when sending a request. -- You can filter `events.NewMessage ` - by sender ID, and also whether they are forwards or not. -- New ``ignore_migrated`` parameter for `client.iter_dialogs - `. - -Bug fixes -~~~~~~~~~ - -- Several fixes to `telethon.events.newmessage.NewMessage`. -- Removed named ``length`` argument in ``to_bytes`` for PyPy. -- Raw events failed due to not having ``._set_client``. -- `message.get_entities_text - ` properly - supports filtering, even if there are no message entities. -- `message.click ` works better. -- The server started sending :tl:`DraftMessageEmpty` which the library - didn't handle correctly when getting dialogs. -- The "correct" chat is now always returned from returned messages. -- ``to_id`` was not validated when retrieving messages by their IDs. -- ``'__'`` is no longer considered valid in usernames. -- The ``fd`` is removed from the reader upon closing the socket. This - should be noticeable in Windows. -- :tl:`MessageEmpty` is now handled when searching messages. -- Fixed a rare infinite loop bug in `client.iter_dialogs - ` for some people. -- Fixed ``TypeError`` when there is no `.sender - `. - -Enhancements -~~~~~~~~~~~~ - -- You can now delete over 100 messages at once with `client.delete_messages - `. -- Signing in now accounts for ``AuthRestartError`` itself, and also handles - ``PasswordHashInvalidError``. -- ``__all__`` is now defined, so ``from telethon import *`` imports sane - defaults (client, events and utils). This is however discouraged and should - be used only in quick scripts. -- ``pathlib.Path`` is now supported for downloading and uploading media. -- Messages you send to yourself are now considered outgoing, unless they - are forwarded. -- The documentation has been updated with a brand new `asyncio` crash - course to encourage you use it. You can still use the threaded version - if you want though. -- ``.name`` property is now properly supported when sending and downloading - files. -- Custom ``parse_mode``, which can now be set per-client, support - :tl:`MessageEntityMentionName` so you can return those now. -- The session file is saved less often, which could result in a noticeable - speed-up when working with a lot of incoming updates. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- The flow for sending a request is as follows: the ``TelegramClient`` creates - a ``MTProtoSender`` with a ``Connection``, and the sender starts send and - receive loops. Sending a request means enqueueing it in the sender, which - will eventually pack and encrypt it with its ``ConnectionState`` instead - of using the entire ``Session`` instance. When the data is packed, it will - be sent over the ``Connection`` and ultimately over the ``TcpClient``. - -- Reconnection occurs at the ``MTProtoSender`` level, and receiving responses - follows a similar process, but now ``asyncio.Future`` is used for the results - which are no longer part of all ``TLObject``, instead are part of the - ``TLMessage`` which simplifies things. - -- Objects can no longer be ``content_related`` and instead subclass - ``TLRequest``, making the separation of concerns easier. - -- The ``TelegramClient`` has been split into several mixin classes to avoid - having a 3,000-lines-long file with all the methods. - -- More special cases in the ``MTProtoSender`` have been cleaned up, and also - some attributes from the ``Session`` which didn't really belong there since - they weren't being saved. - -- The ``telethon_generator/`` can now convert ``.tl`` files into ``.json``, - mostly as a proof of concept, but it might be useful for other people. - - -Custom Message class (v0.19.1) -============================== - -*Published at 2018/06/03* - -+-----------------------+ -| Scheme layer used: 80 | -+-----------------------+ - - -This update brings a new `telethon.tl.custom.message.Message` object! - -All the methods in the `telethon.telegram_client.TelegramClient` that -used to return a :tl:`Message` will now return this object instead, which -means you can do things like the following: - -.. code-block:: python - - msg = client.send_message(chat, 'Hello!') - msg.edit('Hello there!') - msg.reply('Good day!') - print(msg.sender) - -Refer to its documentation to see all you can do, again, click -`telethon.tl.custom.message.Message` to go to its page. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The `telethon.network.connection.common.Connection` class is now an ABC, - and the old ``ConnectionMode`` is now gone. Use a specific connection (like - `telethon.network.connection.tcpabridged.ConnectionTcpAbridged`) instead. - -Additions -~~~~~~~~~ - -- You can get messages by their ID with - `telethon.telegram_client.TelegramClient.get_messages`'s ``ids`` parameter: - - .. code-block:: python - - message = client.get_messages(chats, ids=123) # Single message - message_list = client.get_messages(chats, ids=[777, 778]) # Multiple - -- More convenience properties for `telethon.tl.custom.dialog.Dialog`. -- New default `telethon.telegram_client.TelegramClient.parse_mode`. -- You can edit the media of messages that already have some media. -- New dark theme in the online ``tl`` reference, check it out at - https://tl.telethon.dev/. - -Bug fixes -~~~~~~~~~ - -- Some IDs start with ``1000`` and these would be wrongly treated as channels. -- Some short usernames like ``@vote`` were being ignored. -- `telethon.telegram_client.TelegramClient.iter_messages`'s ``from_user`` - was failing if no filter had been set. -- `telethon.telegram_client.TelegramClient.iter_messages`'s ``min_id/max_id`` - was being ignored by Telegram. This is now worked around. -- `telethon.telegram_client.TelegramClient.catch_up` would fail with empty - states. -- `telethon.events.newmessage.NewMessage` supports ``incoming=False`` - to indicate ``outgoing=True``. - -Enhancements -~~~~~~~~~~~~ - -- You can now send multiple requests at once while preserving the order: - - .. code-block:: python - - from telethon.tl.functions.messages import SendMessageRequest - client([SendMessageRequest(chat, 'Hello 1!'), - SendMessageRequest(chat, 'Hello 2!')], ordered=True) - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``without rowid`` is not used in SQLite anymore. -- Unboxed serialization would fail. -- Different default limit for ``iter_messages`` and ``get_messages``. -- Some clean-up in the ``telethon_generator/`` package. - - -Catching up on Updates (v0.19) -============================== - -*Published at 2018/05/07* - -+-----------------------+ -| Scheme layer used: 76 | -+-----------------------+ - -This update prepares the library for catching up with updates with the new -`telethon.telegram_client.TelegramClient.catch_up` method. This feature needs -more testing, but for now it will let you "catch up" on some old updates that -occurred while the library was offline, and brings some new features and bug -fixes. - - -Additions -~~~~~~~~~ - -- Add ``search``, ``filter`` and ``from_user`` parameters to - `telethon.telegram_client.TelegramClient.iter_messages`. -- `telethon.telegram_client.TelegramClient.download_file` now - supports a ``None`` path to return the file in memory and - return its ``bytes``. -- Events now have a ``.original_update`` field. - -Bug fixes -~~~~~~~~~ - -- Fixed a race condition when receiving items from the network. -- A disconnection is made when "retries reached 0". This hasn't been - tested but it might fix the bug. -- ``reply_to`` would not override :tl:`Message` object's reply value. -- Add missing caption when sending :tl:`Message` with media. - -Enhancements -~~~~~~~~~~~~ - -- Retry automatically on ``RpcCallFailError``. This error happened a lot - when iterating over many messages, and retrying often fixes it. -- Faster `telethon.telegram_client.TelegramClient.iter_messages` by - sleeping only as much as needed. -- `telethon.telegram_client.TelegramClient.edit_message` now supports - omitting the entity if you pass a :tl:`Message`. -- `telethon.events.raw.Raw` can now be filtered by type. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The library now distinguishes between MTProto and API schemas. -- :tl:`State` is now persisted to the session file. -- Connection won't retry forever. -- Fixed some errors and cleaned up the generation of code. -- Fixed typos and enhanced some documentation in general. -- Add auto-cast for :tl:`InputMessage` and :tl:`InputLocation`. - - -Pickle-able objects (v0.18.3) -============================= - -*Published at 2018/04/15* - - -Now you can use Python's ``pickle`` module to serialize ``RPCError`` and -any other ``TLObject`` thanks to **@vegeta1k95**! A fix that was fairly -simple, but still might be useful for many people. - -As a side note, the documentation at https://tl.telethon.dev -now lists known ``RPCError`` for all requests, so you know what to expect. -This required a major rewrite, but it was well worth it! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- `telethon.telegram_client.TelegramClient.forward_messages` now returns - a single item instead of a list if the input was also a single item. - -Additions -~~~~~~~~~ - -- New `telethon.events.messageread.MessageRead` event, to find out when - and who read which messages as soon as it happens. -- Now you can access ``.chat_id`` on all events and ``.sender_id`` on some. - -Bug fixes -~~~~~~~~~ - -- Possibly fix some bug regarding lost ``GzipPacked`` requests. -- The library now uses the "real" layer 75, hopefully. -- Fixed ``.entities`` name collision on updates by making it private. -- ``AUTH_KEY_DUPLICATED`` is handled automatically on connection. -- Markdown parser's offset uses ``match.start()`` to allow custom regex. -- Some filter types (as a type) were not supported by - `telethon.telegram_client.TelegramClient.iter_participants`. -- `telethon.telegram_client.TelegramClient.remove_event_handler` works. -- `telethon.telegram_client.TelegramClient.start` works on all terminals. -- :tl:`InputPeerSelf` case was missing from - `telethon.telegram_client.TelegramClient.get_input_entity`. - -Enhancements -~~~~~~~~~~~~ - -- The ``parse_mode`` for messages now accepts a callable. -- `telethon.telegram_client.TelegramClient.download_media` accepts web previews. -- `telethon.tl.custom.dialog.Dialog` instances can now be casted into - :tl:`InputPeer`. -- Better logging when reading packages "breaks". -- Better and more powerful ``setup.py gen`` command. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The library won't call ``.get_dialogs()`` on entity not found. Instead, - it will ``raise ValueError()`` so you can properly ``except`` it. -- Several new examples and updated documentation. -- ``py:obj`` is the default Sphinx's role which simplifies ``.rst`` files. -- ``setup.py`` now makes use of ``python_requires``. -- Events now live in separate files. -- Other minor changes. - - -Several bug fixes (v0.18.2) -=========================== - -*Published at 2018/03/27* - -Just a few bug fixes before they become too many. - -Additions -~~~~~~~~~ - -- Getting an entity by its positive ID should be enough, regardless of their - type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although - wrapping them inside a ``Peer`` is still recommended, it's not necessary. -- New ``client.edit_2fa`` function to change your Two Factor Authentication - settings. -- ``.stringify()`` and string representation for custom ``Dialog/Draft``. - -Bug fixes -~~~~~~~~~ - -- Some bug regarding ``.get_input_peer``. -- ``events.ChatAction`` wasn't picking up all the pins. -- ``force_document=True`` was being ignored for albums. -- Now you're able to send ``Photo`` and ``Document`` as files. -- Wrong access to a member on chat forbidden error for ``.get_participants``. - An empty list is returned instead. -- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if - someone has "me" or "self" as their name they won't be retrieved. - - -Iterator methods (v0.18.1) -========================== - -*Published at 2018/03/17* - -All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_`` -counterpart, so you can do operations while retrieving items from them. -For instance, you can ``client.iter_dialogs()`` and ``break`` once you -find what you're looking for instead fetching them all at once. - -Another big thing, you can get entities by just their positive ID. This -may cause some collisions (although it's very unlikely), and you can (should) -still be explicit about the type you want. However, it's a lot more convenient -and less confusing. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- The library only offers the default ``SQLiteSession`` again. - See :ref:`sessions` for more on how to use a different storage from now on. - -Additions -~~~~~~~~~ - -- Events now override ``__str__`` and implement ``.stringify()``, just like - every other ``TLObject`` does. -- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and - :meth:`delete` for the message that triggered it. -- :meth:`client.iter_participants` (and its :meth:`client.get_participants` - counterpart) now expose the ``filter`` argument, and the returned users - also expose the ``.participant`` they are. -- You can now use :meth:`client.remove_event_handler` and - :meth:`client.list_event_handlers` similar how you could with normal updates. -- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif`` - to access only specific types of documents. -- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a - new :meth:`Draft.send` to send it. - -Bug fixes -~~~~~~~~~ - -- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments. -- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``. -- Bot API style IDs not working on :meth:`client.get_input_entity`. -- :meth:`client.download_media` didn't support ``PhotoSize``. - -Enhancements -~~~~~~~~~~~~ - -- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some - events (mostly those that occur in a channel). -- You can send albums larger than 10 items (they will be sliced for you), - as well as mixing normal files with photos. -- ``TLObject`` now have Python type hints. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Several documentation corrections. -- :meth:`client.get_dialogs` is only called once again when an entity is - not found to avoid flood waits. - - -Sessions overhaul (v0.18) -========================= - -*Published at 2018/03/04* - -+-----------------------+ -| Scheme layer used: 75 | -+-----------------------+ - -The ``Session``'s have been revisited thanks to the work of **@tulir** and -they now use an `ABC `__ so you -can easily implement your own! - -The default will still be a ``SQLiteSession``, but you might want to use -the new ``AlchemySessionContainer`` if you need. Refer to the section of -the documentation on :ref:`sessions` for more. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``events.MessageChanged`` doesn't exist anymore. Use the new - ``events.MessageEdited`` and ``events.MessageDeleted`` instead. - -Additions -~~~~~~~~~ - -- The mentioned addition of new session types. -- You can omit the event type on ``client.add_event_handler`` to use ``Raw``. -- You can ``raise StopPropagation`` of events if you added several of them. -- ``.get_participants()`` can now get up to 90,000 members from groups with - 100,000 if when ``aggressive=True``, "bypassing" Telegram's limit. -- You now can access ``NewMessage.Event.pattern_match``. -- Multiple captions are now supported when sending albums. -- ``client.send_message()`` has an optional ``file=`` parameter, so - you can do ``events.reply(file='/path/to/photo.jpg')`` and similar. -- Added ``.input_`` versions to ``events.ChatAction``. -- You can now access the public ``.client`` property on ``events``. -- New ``client.forward_messages``, with its own wrapper on ``events``, - called ``event.forward_to(...)``. - - -Bug fixes -~~~~~~~~~ - -- Silly bug regarding ``client.get_me(input_peer=True)``. -- ``client.send_voice_note()`` was missing some parameters. -- ``client.send_file()`` plays better with streams now. -- Incoming messages from bots weren't working with whitelists. -- Markdown's URL regex was not accepting newlines. -- Better attempt at joining background update threads. -- Use the right peer type when a marked integer ID is provided. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- Resolving ``events.Raw`` is now a no-op. -- Logging calls in the ``TcpClient`` to spot errors. -- ``events`` resolution is postponed until you are successfully connected, - so you can attach them before starting the client. -- When an entity is not found, it is searched in *all* dialogs. This might - not always be desirable but it's more comfortable for legitimate uses. -- Some non-persisting properties from the ``Session`` have been moved out. - - -Further easing library usage (v0.17.4) -====================================== - -*Published at 2018/02/24* - -Some new things and patches that already deserved their own release. - - -Additions -~~~~~~~~~ - -- New ``pattern`` argument to ``NewMessage`` to easily filter messages. -- New ``.get_participants()`` convenience method to get members from chats. -- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. -- You can now ``.get_entity()`` through exact name match instead username. -- Raise ``ProxyConnectionError`` instead looping forever so you can - ``except`` it on your own code and behave accordingly. - -Bug fixes -~~~~~~~~~ - -- ``.parse_username`` would fail with ``www.`` or a trailing slash. -- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. -- You can now send ``b'byte strings'`` directly as files again. -- ``.send_file()`` was not respecting the original captions when passing - another message (or media) as the file. -- Downloading media from a different data center would always log a warning - for the first time. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. -- You can use ``.get_me(input_peer=True)`` if all you need is your self ID. -- New addition to the interactive client example to show peer information. -- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so - you can always safely rely on ``.sender`` to get the right ID. - - -New small convenience functions (v0.17.3) -========================================= - -*Published at 2018/02/18* - -More bug fixes and a few others addition to make events easier to use. - -Additions -~~~~~~~~~ - -- Use ``hachoir`` to extract video and audio metadata before upload. -- New ``.add_event_handler``, ``.add_update_handler`` now deprecated. - -Bug fixes -~~~~~~~~~ - -- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` - (now it will ask you for it if you don't provide it, as docstring hinted). -- ``.edit_message()`` was ignoring the formatting (e.g. markdown). -- Added missing case to the ``NewMessage`` event for normal groups. -- Accessing the ``.text`` of the ``NewMessage`` event was failing due - to a bug with the markdown unparser. - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, - which you can find on https://github.com/Lonami/cryptg. - - - -New small convenience functions (v0.17.2) -========================================= - -*Published at 2018/02/15* - -Primarily bug fixing and a few welcomed additions. - -Additions -~~~~~~~~~ - -- New convenience ``.edit_message()`` method on the ``TelegramClient``. -- New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event. -- Default to markdown parsing when sending and editing messages. -- Support for inline mentions when sending and editing messages. They work - like inline urls (e.g. ``[text](@username)``) and also support the Bot-API - style (see `here `__). - -Bug fixes -~~~~~~~~~ - -- Periodically send ``GetStateRequest`` automatically to keep the server - sending updates even if you're not invoking any request yourself. -- HTML parsing was failing due to not handling surrogates properly. -- ``.sign_up`` was not accepting ``int`` codes. -- Whitelisting more than one chat on ``events`` wasn't working. -- Video files are sent as a video by default unless ``force_document``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- More ``logging`` calls to help spot some bugs in the future. -- Some more logic to retrieve input entities on events. -- Clarified a few parts of the documentation. - - -Updates as Events (v0.17.1) -=========================== - -*Published at 2018/02/09* - -Of course there was more work to be done regarding updates, and it's here! -The library comes with a new ``events`` module (which you will often import -as ``from telethon import TelegramClient, events``). This are pretty much -all the additions that come with this version change, but they are a nice -addition. Refer to *(removed broken link)* to get started with events. - - -Trust the Server with Updates (v0.17) -===================================== - -*Published at 2018/02/03* - -The library trusts the server with updates again. The library will *not* -check for duplicates anymore, and when the server kicks us, it will run -``GetStateRequest`` so the server starts sending updates again (something -it wouldn't do unless you invoked something, it seems). But this update -also brings a few more changes! - -Additions -~~~~~~~~~ - -- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. -- Added some missing cases on ``.get_input_entity()`` and peer functions. -- ``obj.to_dict()`` now has a ``'_'`` key with the type used. -- ``.start()`` can also sign up now. -- More parameters for ``.get_message_history()``. -- Updated list of RPC errors. -- HTML parsing thanks to **@tulir**! It can be used similar to markdown: - ``client.send_message(..., parse_mode='html')``. - - -Enhancements -~~~~~~~~~~~~ - -- ``client.send_file()`` now accepts ``Message``'s and - ``MessageMedia``'s as the ``file`` parameter. -- Some documentation updates and fixed to clarify certain things. -- New exact match feature on https://tl.telethon.dev. -- Return as early as possible from ``.get_input_entity()`` and similar, - to avoid penalizing you for doing this right. - -Bug fixes -~~~~~~~~~ - -- ``.download_media()`` wouldn't accept a ``Document`` as parameter. -- The SQLite is now closed properly on disconnection. -- IPv6 addresses shouldn't use square braces. -- Fix regarding ``.log_out()``. -- The time offset wasn't being used (so having wrong system time would - cause the library not to work at all). - - -New ``.resolve()`` method (v0.16.2) -=================================== - -*Published at 2018/01/19* - -The ``TLObject``'s (instances returned by the API and ``Request``'s) have -now acquired a new ``.resolve()`` method. While this should be used by the -library alone (when invoking a request), it means that you can now use -``Peer`` types or even usernames where a ``InputPeer`` is required. The -object now has access to the ``client``, so that it can fetch the right -type if needed, or access the session database. Furthermore, you can -reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer`` -was needed), since ``.resolve()`` is called when invoking. Before, it was -only done on object construction. - -Additions -~~~~~~~~~ - -- Album support. Just pass a list, tuple or any iterable to ``.send_file()``. - - -Enhancements -~~~~~~~~~~~~ - -- ``.start()`` asks for your phone only if required. -- Better file cache. All files under 10MB, once uploaded, should never be - needed to be re-uploaded again, as the sent media is cached to the session. - - -Bug fixes -~~~~~~~~~ - -- ``setup.py`` now calls ``gen_tl`` when installing the library if needed. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- The mentioned ``.resolve()`` to perform "autocast", more powerful. -- Upload and download methods are no longer part of ``TelegramBareClient``. -- Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``. - Only override ``.on_response()`` if necessary (small amount of cases). -- Reduced "autocast" overhead as much as possible. - You shouldn't be penalized if you've provided the right type. - - -MtProto 2.0 (v0.16.1) -===================== - -*Published at 2018/01/11* - -+-----------------------+ -| Scheme layer used: 74 | -+-----------------------+ - -The library is now using MtProto 2.0! This shouldn't really affect you -as an end user, but at least it means the library will be ready by the -time MtProto 1.0 is deprecated. - -Additions -~~~~~~~~~ - -- New ``.start()`` method, to make the library avoid boilerplate code. -- ``.send_file`` accepts a new optional ``thumbnail`` parameter, and - returns the ``Message`` with the sent file. - - -Bug fixes -~~~~~~~~~ - -- The library uses again only a single connection. Less updates are - be dropped now, and the performance is even better than using temporary - connections. -- ``without rowid`` will only be used on the ``*.session`` if supported. -- Phone code hash is associated with phone, so you can change your mind - when calling ``.sign_in()``. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- File cache now relies on the hash of the file uploaded instead its path, - and is now persistent in the ``*.session`` file. Report any bugs on this! -- Clearer error when invoking without being connected. -- Markdown parser doesn't work on bytes anymore (which makes it cleaner). - - -Sessions as sqlite databases (v0.16) -==================================== - -*Published at 2017/12/28* - -In the beginning, session files used to be pickle. This proved to be bad -as soon as one wanted to add more fields. For this reason, they were -migrated to use JSON instead. But this proved to be bad as soon as one -wanted to save things like entities (usernames, their ID and hash), so -now it properly uses -`sqlite3 `__, -which has been well tested, to save the session files! Calling -``.get_input_entity`` using a ``username`` no longer will need to fetch -it first, so it's really 0 calls again. Calling ``.get_entity`` will -always fetch the most up to date version. - -Furthermore, nearly everything has been documented, thus preparing the -library for `Read the Docs `__ (although there -are a few things missing I'd like to polish first), and the -`logging `__ are now -better placed. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``.get_dialogs()`` now returns a **single list** instead a tuple - consisting of a **custom class** that should make everything easier - to work with. -- ``.get_message_history()`` also returns a **single list** instead a - tuple, with the ``Message`` instances modified to make them more - convenient. - -Both lists have a ``.total`` attribute so you can still know how many -dialogs/messages are in total. - -Additions -~~~~~~~~~ - -- The mentioned use of ``sqlite3`` for the session file. -- ``.get_entity()`` now supports lists too, and it will make as little - API calls as possible if you feed it ``InputPeer`` types. Usernames - will always be resolved, since they may have changed. -- ``.set_proxy()`` method, to avoid having to create a new - ``TelegramClient``. -- More ``date`` types supported to represent a date parameter. - -Bug fixes -~~~~~~~~~ - -- Empty strings weren't working when they were a flag parameter (e.g., - setting no last name). -- Fix invalid assertion regarding flag parameters as well. -- Avoid joining the background thread on disconnect, as it would be - ``None`` due to a race condition. -- Correctly handle ``None`` dates when downloading media. -- ``.download_profile_photo`` was failing for some channels. -- ``.download_media`` wasn't handling ``Photo``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``date`` was being serialized as local date, but that was wrong. -- ``date`` was being represented as a ``float`` instead of an ``int``. -- ``.tl`` parser wasn't stripping inline comments. -- Removed some redundant checks on ``update_state.py``. -- Use a `synchronized - queue `__ instead a - hand crafted version. -- Use signed integers consistently (e.g. ``salt``). -- Always read the corresponding ``TLObject`` from API responses, except - for some special cases still. -- A few more ``except`` low level to correctly wrap errors. -- More accurate exception types. -- ``invokeWithLayer(initConnection(X))`` now wraps every first request - after ``.connect()``. - -As always, report if you have issues with some of the changes! - -IPv6 support (v0.15.5) -====================== - -*Published at 2017/11/16* - -+-----------------------+ -| Scheme layer used: 73 | -+-----------------------+ - -It's here, it has come! The library now **supports IPv6**! Just pass -``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could -*not* test this feature because my machine doesn't have IPv6 setup. If -you know IPv6 works in your machine but the library doesn't, please -refer to `#425 `_. - -Additions -~~~~~~~~~ - -- IPv6 support. -- New method to extract the text surrounded by ``MessageEntity``\ 's, - in the ``extensions.markdown`` module. - -Enhancements -~~~~~~~~~~~~ - -- Markdown parsing is Done Right. -- Reconnection on failed invoke. Should avoid "number of retries - reached 0" (#270). -- Some missing autocast to ``Input*`` types. -- The library uses the ``NullHandler`` for ``logging`` as it should - have always done. -- ``TcpClient.is_connected()`` is now more reliable. - -.. bug-fixes-1: - -Bug fixes -~~~~~~~~~ - -- Getting an entity using their phone wasn't actually working. -- Full entities aren't saved unless they have an ``access_hash``, to - avoid some ``None`` errors. -- ``.get_message_history`` was failing when retrieving items that had - messages forwarded from a channel. - -General enhancements (v0.15.4) -============================== - -*Published at 2017/11/04* - -+-----------------------+ -| Scheme layer used: 72 | -+-----------------------+ - -This update brings a few general enhancements that are enough to deserve -a new release, with a new feature: beta **markdown-like parsing** for -``.send_message()``! - -.. additions-1: - -Additions -~~~~~~~~~ - -- ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It - works in a similar fashion to the official clients (defaults to - double underscore/asterisk, like ``**this**``). Please report any - issues with emojies or enhancements for the parser! -- New ``.idle()`` method so your main thread can do useful job (listen - for updates). -- Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for - ``TLMessage`` and ``MessageContainer``. - -.. bug-fixes-2: - -Bug fixes -~~~~~~~~~ - -- The list of known peers could end "corrupted" and have users with - ``access_hash=None``, resulting in ``struct`` error for it not being - an integer. You shouldn't encounter this issue anymore. -- The warning for "added update handler but no workers set" wasn't - actually working. -- ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``. -- There used to be an exception when logging exceptions (whoops) on - update handlers. -- "Downloading contacts" would produce strange output if they had - semicolons (``;``) in their name. -- Fix some cyclic imports and installing dependencies from the ``git`` - repository. -- Code generation was using f-strings, which are only supported on - Python ≥3.6. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The ``auth_key`` generation has been moved from ``.connect()`` to - ``.invoke()``. There were some issues were ``.connect()`` failed and - the ``auth_key`` was ``None`` so this will ensure to have a valid - ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised. -- Support for higher limits on ``.get_history()`` and - ``.get_dialogs()``. -- Much faster integer factorization when generating the required - ``auth_key``. Thanks @delivrance for making me notice this, and for - the pull request. - -Bug fixes with updates (v0.15.3) -================================ - -*Published at 2017/10/20* - -Hopefully a very ungrateful bug has been removed. When you used to -invoke some request through update handlers, it could potentially enter -an infinite loop. This has been mitigated and it's now safe to invoke -things again! A lot of updates were being dropped (all those gzipped), -and this has been fixed too. - -More bug fixes include a `correct -parsing `__ -of certain TLObjects thanks to @stek29, and -`some `__ -`wrong -calls `__ -that would cause the library to crash thanks to @andr-04, and the -``ReadThread`` not re-starting if you were already authorized. - -Internally, the ``.to_bytes()`` function has been replaced with -``__bytes__`` so now you can do ``bytes(tlobject)``. - -Bug fixes and new small features (v0.15.2) -========================================== - -*Published at 2017/10/14* - -This release primarly focuses on a few bug fixes and enhancements. -Although more stuff may have broken along the way. - -Enhancements -~~~~~~~~~~~~ - -- You will be warned if you call ``.add_update_handler`` with no - ``update_workers``. -- New customizable threshold value on the session to determine when to - automatically sleep on flood waits. See - ``client.session.flood_sleep_threshold``. -- New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa. -- Join all threads when calling ``.disconnect()``, to assert no - dangling thread is left alive. -- Larger chunk when downloading files should result in faster - downloads. -- You can use a callable key for the ``EntityDatabase``, so it can be - any filter you need. - -.. bug-fixes-3: - -Bug fixes -~~~~~~~~~ - -- ``.get_input_entity`` was failing for IDs and other cases, also - making more requests than it should. -- Use ``basename`` instead ``abspath`` when sending a file. You can now - also override the attributes. -- ``EntityDatabase.__delitem__`` wasn't working. -- ``.send_message()`` was failing with channels. -- ``.get_dialogs(limit=None)`` should now return all the dialogs - correctly. -- Temporary fix for abusive duplicated updates. - -.. enhancements-1: - -.. internal-changes-1: - -Internal changes -~~~~~~~~~~~~~~~~ - -- MsgsAck is now sent in a container rather than its own request. -- ``.get_input_photo`` is now used in the generated code. -- ``.process_entities`` was being called from more places than only - ``__call__``. -- ``MtProtoSender`` now relies more on the generated code to read - responses. - -Custom Entity Database (v0.15.1) -================================ - -*Published at 2017/10/05* - -The main feature of this release is that Telethon now has a custom -database for all the entities you encounter, instead depending on -``@lru_cache`` on the ``.get_entity()`` method. - -The ``EntityDatabase`` will, by default, **cache** all the users, chats -and channels you find in memory for as long as the program is running. -The session will, by default, save all key-value pairs of the entity -identifiers and their hashes (since Telegram may send an ID that it -thinks you already know about, we need to save this information). - -You can **prevent** the ``EntityDatabase`` from saving users by setting -``client.session.entities.enabled = False``, and prevent the ``Session`` -from saving input entities at all by setting -``client.session.save_entities = False``. You can also clear the cache -for a certain user through -``client.session.entities.clear_cache(entity=None)``, which will clear -all if no entity is given. - - -Additions -~~~~~~~~~ - -- New method to ``.delete_messages()``. -- New ``ChannelPrivateError`` class. - -Enhancements -~~~~~~~~~~~~ - -- ``.sign_in`` accepts phones as integers. -- Changing the IP to which you connect to is as simple as - ``client.session.server_address = 'ip'``, since now the - server address is always queried from the session. - -Bug fixes -~~~~~~~~~ - -- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the - right amount of dialogs. -- ``GeneralProxyError`` should be passed to the main thread - again, so that you can handle it. - -Updates Overhaul Update (v0.15) -=============================== - -*Published at 2017/10/01* - -After hundreds of lines changed on a major refactor, *it's finally -here*. It's the **Updates Overhaul Update**; let's get right into it! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``.create_new_connection()`` is gone for good. No need to deal with - this manually since new connections are now handled on demand by the - library itself. - -Enhancements -~~~~~~~~~~~~ - -- You can **invoke** requests from **update handlers**. And **any other - thread**. A new temporary will be made, so that you can be sending - even several requests at the same time! -- **Several worker threads** for your updates! By default, ``None`` - will spawn. I recommend you to work with ``update_workers=4`` to get - started, these will be polling constantly for updates. -- You can also change the number of workers at any given time. -- The library can now run **in a single thread** again, if you don't - need to spawn any at all. Simply set ``spawn_read_thread=False`` when - creating the ``TelegramClient``! -- You can specify ``limit=None`` on ``.get_dialogs()`` to get **all** - of them[1]. -- **Updates are expanded**, so you don't need to check if the update - has ``.updates`` or an inner ``.update`` anymore. -- All ``InputPeer`` entities are **saved in the session** file, but you - can disable this by setting ``save_entities=False``. -- New ``.get_input_entity`` method, which makes use of the above - feature. You **should use this** when a request needs a - ``InputPeer``, rather than the whole entity (although both work). -- Assert that either all or None dependent-flag parameters are set - before sending the request. -- Phone numbers can have dashes, spaces, or parenthesis. They'll be - removed before making the request. -- You can override the phone and its hash on ``.sign_in()``, if you're - creating a new ``TelegramClient`` on two different places. - -Bug fixes -~~~~~~~~~ - -- ``.log_out()`` was consuming all retries. It should work just fine - now. -- The session would fail to load if the ``auth_key`` had been removed - manually. -- ``Updates.check_error`` was popping wrong side, although it's been - completely removed. -- ``ServerError``\ 's will be **ignored**, and the request will - immediately be retried. -- Cross-thread safety when saving the session file. -- Some things changed on a matter of when to reconnect, so please - report any bugs! - -.. internal-changes-2: - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``TelegramClient`` is now only an abstraction over the - ``TelegramBareClient``, which can only do basic things, such as - invoking requests, working with files, etc. If you don't need any of - the abstractions the ``TelegramClient``, you can now use the - ``TelegramBareClient`` in a much more comfortable way. -- ``MtProtoSender`` is not thread-safe, but it doesn't need to be since - a new connection will be spawned when needed. -- New connections used to be cached and then reused. Now only their - sessions are saved, as temporary connections are spawned only when - needed. -- Added more RPC errors to the list. - -**[1]:** Broken due to a condition which should had been the opposite -(sigh), fixed 4 commits ahead on -https://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c. - --------------- - -**That's pretty much it**, although there's more work to be done to make -the overall experience of working with updates *even better*. Stay -tuned! - -Serialization bug fixes (v0.14.2) -================================= - -*Published at 2017/09/29* - -Bug fixes -~~~~~~~~~ - -- **Important**, related to the serialization. Every object or request - that had to serialize a ``True/False`` type was always being serialized - as ``false``! -- Another bug that didn't allow you to leave as ``None`` flag parameters - that needed a list has been fixed. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Other internal changes include a somewhat more readable ``.to_bytes()`` - function and pre-computing the flag instead using bit shifting. The - ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``, - and ``.subclass_of_id`` is also uppercase now. - -Farewell, BinaryWriter (v0.14.1) -================================ - -*Published at 2017/09/28* - -Version ``v0.14`` had started working on the new ``.to_bytes()`` method -to dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when -serializing TLObjects, and this release finally removes it. The speed up -when serializing things to bytes should now be over twice as fast -wherever it's needed. - -Bug fixes -~~~~~~~~~ - -- This version is again compatible with Python 3.x versions **below 3.5** - (there was a method call that was Python 3.5 and above). - -Internal changes -~~~~~~~~~~~~~~~~ - -- Using proper classes (including the generated code) for generating - authorization keys and to write out ``TLMessage``\ 's. - - -Several requests at once and upload compression (v0.14) -======================================================= - -*Published at 2017/09/27* - -New major release, since I've decided that these two features are big -enough: - -Additions -~~~~~~~~~ - -- Requests larger than 512 bytes will be **compressed through - gzip**, and if the result is smaller, this will be uploaded instead. -- You can now send **multiple requests at once**, they're simply - ``*var_args`` on the ``.invoke()``. Note that the server doesn't - guarantee the order in which they'll be executed! - -Internally, another important change. The ``.on_send`` function on the -``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From -my tests, this has always been over twice as fast serializing objects, -although more replacements need to be done, so please report any issues. - -Enhancements -~~~~~~~~~~~~ -- Implemented ``.get_input_media`` helper methods. Now you can even use - another message as input media! - - -Bug fixes -~~~~~~~~~ - -- Downloading media from CDNs wasn't working (wrong - access to a parameter). -- Correct type hinting. -- Added a tiny sleep when trying to perform automatic reconnection. -- Error reporting is done in the background, and has a shorter timeout. -- ``setup.py`` used to fail with wrongly generated code. - -Quick fix-up (v0.13.6) -====================== - -*Published at 2017/09/23* - -Before getting any further, here's a quick fix-up with things that -should have been on ``v0.13.5`` but were missed. Specifically, the -**timeout when receiving** a request will now work properly. - -Some other additions are a tiny fix when **handling updates**, which was -ignoring some of them, nicer ``__str__`` and ``.stringify()`` methods -for the ``TLObject``\ 's, and not stopping the ``ReadThread`` if you try -invoking something there (now it simply returns ``None``). - -Attempts at more stability (v0.13.5) -==================================== - -*Published at 2017/09/23* - -Yet another update to fix some bugs and increase the stability of the -library, or, at least, that was the attempt! - -This release should really **improve the experience with the background -thread** that the library starts to read things from the network as soon -as it can, but I can't spot every use case, so please report any bug -(and as always, minimal reproducible use cases will help a lot). - -.. bug-fixes-4: - -Bug fixes -~~~~~~~~~ - -- ``setup.py`` was failing on Python < 3.5 due to some imports. -- Duplicated updates should now be ignored. -- ``.send_message`` would crash in some cases, due to having a typo - using the wrong object. -- ``"socket is None"`` when calling ``.connect()`` should not happen - anymore. -- ``BrokenPipeError`` was still being raised due to an incorrect order - on the ``try/except`` block. - -.. enhancements-2: - -Enhancements -~~~~~~~~~~~~ - -- **Type hinting** for all the generated ``Request``\ 's and - ``TLObjects``! IDEs like PyCharm will benefit from this. -- ``ProxyConnectionError`` should properly be passed to the main thread - for you to handle. -- The background thread will only be started after you're authorized on - Telegram (i.e. logged in), and several other attempts at polishing - the experience with this thread. -- The ``Connection`` instance is only created once now, and reused - later. -- Calling ``.connect()`` should have a better behavior now (like - actually *trying* to connect even if we seemingly were connected - already). -- ``.reconnect()`` behavior has been changed to also be more consistent - by making the assumption that we'll only reconnect if the server has - disconnected us, and is now private. - -.. other-changes-1: - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``TLObject.__repr__`` doesn't show the original TL definition - anymore, it was a lot of clutter. If you have any complaints open an - issue and we can discuss it. -- Internally, the ``'+'`` from the phone number is now stripped, since - it shouldn't be included. -- Spotted a new place where ``BrokenAuthKeyError`` would be raised, and - it now is raised there. - -More bug fixes and enhancements (v0.13.4) -========================================= - -*Published at 2017/09/18* - -.. new-stuff-1: - -Additions -~~~~~~~~~ - -- ``TelegramClient`` now exposes a ``.is_connected()`` method. -- Initial authorization on a new data center will retry up to 5 times - by default. -- Errors that couldn't be handled on the background thread will be - raised on the next call to ``.invoke()`` or ``updates.poll()``. - -.. bugs-fixed-1: - -Bug fixes -~~~~~~~~~~ - -- Now you should be able to sign in even if you have - ``process_updates=True`` and no previous session. -- Some errors and methods are documented a bit clearer. -- ``.send_message()`` could randomly fail, as the returned type was not - expected. -- ``TimeoutError`` is now ignored, since the request will be retried up - to 5 times by default. -- "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when - first connecting to a new data center. -- ``BufferError`` is handled more gracefully, in the same way as - ``InvalidCheckSumError``\ 's. -- Attempt at fixing some "NoneType has no attribute…" errors (with the - ``.sender``). - -Internal changes -~~~~~~~~~~~~~~~~ - -- Calling ``GetConfigRequest`` is now made less often. -- The ``initial_query`` parameter from ``.connect()`` is gone, as it's - not needed anymore. -- Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since - it's a constant). -- The message from ``BufferError`` is now more useful. - -Bug fixes and enhancements (v0.13.3) -==================================== - -*Published at 2017/09/14* - -.. bugs-fixed-2: - -Bug fixes -~~~~~~~~~ - -- **Reconnection** used to fail because it tried invoking things from - the ``ReadThread``. -- Inferring **random ids** for ``ForwardMessagesRequest`` wasn't - working. -- Downloading media from **CDNs** failed due to having forgotten to - remove a single line. -- ``TcpClient.close()`` now has a **``threading.Lock``**, so - ``NoneType has no close()`` should not happen. -- New **workaround** for ``msg seqno too low/high``. Also, both - ``Session.id/seq`` are not saved anymore. - -.. enhancements-3: - -Enhancements -~~~~~~~~~~~~ - -- **Request will be retried** up to 5 times by default rather than - failing on the first attempt. -- ``InvalidChecksumError``\ 's are now **ignored** by the library. -- ``TelegramClient.get_entity()`` is now **public**, and uses the - ``@lru_cache()`` decorator. -- New method to **``.send_voice_note()``**\ 's. -- Methods to send message and media now support a **``reply_to`` - parameter**. -- ``.send_message()`` now returns the **full message** which was just - sent. - -New way to work with updates (v0.13.2) -====================================== - -*Published at 2017/09/08* - -This update brings a new way to work with updates, and it's begging for -your **feedback**, or better names or ways to do what you can do now. - -Please refer to the `wiki/Usage -Modes `__ for -an in-depth description on how to work with updates now. Notice that you -cannot invoke requests from within handlers anymore, only the -``v.0.13.1`` patch allowed you to do so. - -Bug fixes -~~~~~~~~~ - -- Periodic pings are back. -- The username regex mentioned on ``UsernameInvalidError`` was invalid, - but it has now been fixed. -- Sending a message to a phone number was failing because the type used - for a request had changed on layer 71. -- CDN downloads weren't working properly, and now a few patches have been - applied to ensure more reliability, although I couldn't personally test - this, so again, report any feedback. - -Invoke other requests from within update callbacks (v0.13.1) -============================================================ - -*Published at 2017/09/04* - -.. warning:: - - This update brings some big changes to the update system, - so please read it if you work with them! - -A silly "bug" which hadn't been spotted has now been fixed. Now you can -invoke other requests from within your update callbacks. However **this -is not advised**. You should post these updates to some other thread, -and let that thread do the job instead. Invoking a request from within a -callback will mean that, while this request is being invoked, no other -things will be read. - -Internally, the generated code now resides under a *lot* less files, -simply for the sake of avoiding so many unnecessary files. The generated -code is not meant to be read by anyone, simply to do its job. - -Unused attributes have been removed from the ``TLObject`` class too, and -``.sign_up()`` returns the user that just logged in in a similar way to -``.sign_in()`` now. - -Connection modes (v0.13) -======================== - -*Published at 2017/09/04* - -+-----------------------+ -| Scheme layer used: 71 | -+-----------------------+ - -The purpose of this release is to denote a big change, now you can -connect to Telegram through different `**connection -modes** `__. -Also, a **second thread** will *always* be started when you connect a -``TelegramClient``, despite whether you'll be handling updates or -ignoring them, whose sole purpose is to constantly read from the -network. - -The reason for this change is as simple as *"reading and writing -shouldn't be related"*. Even when you're simply ignoring updates, this -way, once you send a request you will only need to read the result for -the request. Whatever Telegram sent before has already been read and -outside the buffer. - -.. additions-2: - -Additions -~~~~~~~~~ - -- The mentioned different connection modes, and a new thread. -- You can modify the ``Session`` attributes through the - ``TelegramClient`` constructor (using ``**kwargs``). -- ``RPCError``\ 's now belong to some request you've made, which makes - more sense. -- ``get_input_*`` now handles ``None`` (default) parameters more - gracefully (it used to crash). - -.. enhancements-4: - -Enhancements -~~~~~~~~~~~~ - -- The low-level socket doesn't use a handcrafted timeout anymore, which - should benefit by avoiding the arbitrary ``sleep(0.1)`` that there - used to be. -- ``TelegramClient.sign_in`` will call ``.send_code_request`` if no - ``code`` was provided. - -Deprecation -~~~~~~~~~~~ - -- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change - this or you will be using ``phone`` as ``code``, and it will fail! - The definition looks like - ``def sign_up(self, code, first_name, last_name='')``. -- The old ``JsonSession`` finally replaces the original ``Session`` - (which used pickle). If you were overriding any of these, you should - only worry about overriding ``Session`` now. - -Added verification for CDN file (v0.12.2) -========================================= - -*Published at 2017/08/28* - -Since the Content Distributed Network (CDN) is not handled by Telegram -itself, the owners may tamper these files. Telegram sends their sha256 -sum for clients to implement this additional verification step, which -now the library has. If any CDN has altered the file you're trying to -download, ``CdnFileTamperedError`` will be raised to let you know. - -Besides this. ``TLObject.stringify()`` was showing bytes as lists (now -fixed) and RPC errors are reported by default: - - In an attempt to help everyone who works with the Telegram API, - Telethon will by default report all Remote Procedure Call errors to - `PWRTelegram `__, a public database anyone can - query, made by `Daniil `__. All the information - sent is a GET request with the error code, error message and method used. - - -.. note:: - - If you still would like to opt out, simply set - ``client.session.report_errors = False`` to disable this feature. - However Daniil would really thank you if you helped him (and everyone) - by keeping it on! - -CDN support (v0.12.1) -===================== - -*Published at 2017/08/24* - -The biggest news for this update are that downloading media from CDN's -(you'll often encounter this when working with popular channels) now -**works**. - -Bug fixes -~~~~~~~~~ - -- The method used to download documents crashed because - two lines were swapped. -- Determining the right path when downloading any file was - very weird, now it's been enhanced. -- The ``.sign_in()`` method didn't support integer values for the code! - Now it does again. - -Some important internal changes are that the old way to deal with RSA -public keys now uses a different module instead the old strange -hand-crafted version. - -Hope the new, super simple ``README.rst`` encourages people to use -Telethon and make it better with either suggestions, or pull request. -Pull requests are *super* appreciated, but showing some support by -leaving a star also feels nice ⭐️. - -Newbie friendly update (v0.12) -============================== - -*Published at 2017/08/22* - -+-----------------------+ -| Scheme layer used: 70 | -+-----------------------+ - -This update is overall an attempt to make Telethon a bit more user -friendly, along with some other stability enhancements, although it -brings quite a few changes. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- The ``TelegramClient`` methods ``.send_photo_file()``, - ``.send_document_file()`` and ``.send_media_file()`` are now a - **single method** called ``.send_file()``. It's also important to - note that the **order** of the parameters has been **swapped**: first - to *who* you want to send it, then the file itself. - -- The same applies to ``.download_msg_media()``, which has been renamed - to ``.download_media()``. The method now supports a ``Message`` - itself too, rather than only ``Message.media``. The specialized - ``.download_photo()``, ``.download_document()`` and - ``.download_contact()`` still exist, but are private. - -Additions -~~~~~~~~~ - -- Updated to **layer 70**! -- Both downloading and uploading now support **stream-like objects**. -- A lot **faster initial connection** if ``sympy`` is installed (can be - installed through ``pip``). -- ``libssl`` will also be used if available on your system (likely on - Linux based systems). This speed boost should also apply to uploading - and downloading files. -- You can use a **phone number** or an **username** for methods like - ``.send_message()``, ``.send_file()``, and all the other quick-access - methods provided by the ``TelegramClient``. - -.. bug-fixes-5: - -Bug fixes -~~~~~~~~~ - -- Crashing when migrating to a new layer and receiving old updates - should not happen now. -- ``InputPeerChannel`` is now casted to ``InputChannel`` automtically - too. -- ``.get_new_msg_id()`` should now be thread-safe. No promises. -- Logging out on macOS caused a crash, which should be gone now. -- More checks to ensure that the connection is flagged correctly as - either connected or not. - -.. note:: - - Downloading files from CDN's will **not work** yet (something new - that comes with layer 70). - --------------- - -That's it, any new idea or suggestion about how to make the project even -more friendly is highly appreciated. - -.. note:: - - Did you know that you can pretty print any result Telegram returns - (called ``TLObject``\ 's) by using their ``.stringify()`` function? - Great for debugging! - -get_input_* now works with vectors (v0.11.5) -============================================= - -*Published at 2017/07/11* - -Quick fix-up of a bug which hadn't been encountered until now. Auto-cast -by using ``get_input_*`` now works. - -get_input_* everywhere (v0.11.4) -================================= - -*Published at 2017/07/10* - -For some reason, Telegram doesn't have enough with the -`InputPeer `__. -There also exist -`InputChannel `__ -and -`InputUser `__! -You don't have to worry about those anymore, it's handled internally -now. - -Besides this, every Telegram object now features a new default -``.__str__`` look, and also a `.stringify() -method `__ -to pretty format them, if you ever need to inspect them. - -The library now uses `the DEBUG -level `__ -everywhere, so no more warnings or information messages if you had -logging enabled. - -The ``no_webpage`` parameter from ``.send_message`` `has been -renamed `__ -to ``link_preview`` for clarity, so now it does the opposite (but has a -clearer intention). - -Quick .send_message() fix (v0.11.3) -=================================== - -*Published at 2017/07/05* - -A very quick follow-up release to fix a tiny bug with -``.send_message()``, no new features. - -Callable TelegramClient (v0.11.2) -================================= - -*Published at 2017/07/04* - -+-----------------------+ -| Scheme layer used: 68 | -+-----------------------+ - -There is a new preferred way to **invoke requests**, which you're -encouraged to use: - -.. code:: python - - # New! - result = client(SomeRequest()) - - # Old. - result = client.invoke(SomeRequest()) - -Existing code will continue working, since the old ``.invoke()`` has not -been deprecated. - -When you ``.create_new_connection()``, it will also handle -``FileMigrateError``\ 's for you, so you don't need to worry about those -anymore. - -.. bugs-fixed-3: - -Bugs fixes -~~~~~~~~~~ - -- Fixed some errors when installing Telethon via ``pip`` (for those - using either source distributions or a Python version ≤ 3.5). -- ``ConnectionResetError`` didn't flag sockets as closed, but now it - does. - -On a more technical side, ``msg_id``\ 's are now more accurate. - -Improvements to the updates (v0.11.1) -===================================== - -*Published at 2017/06/24* - -Receiving new updates shouldn't miss any anymore, also, periodic pings -are back again so it should work on the long run. - -On a different order of things, ``.connect()`` also features a timeout. -Notice that the ``timeout=`` is **not** passed as a **parameter** -anymore, and is instead specified when creating the ``TelegramClient``. - -Bug fixes -~~~~~~~~~ - -- Fixed some name class when a request had a ``.msg_id`` parameter. -- The correct amount of random bytes is now used in DH request -- Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. -- Avoid connecting if already connected. - -Support for parallel connections (v0.11) -======================================== - -*Published at 2017/06/16* - -*This update brings a lot of changes, so it would be nice if you could* -**read the whole change log**! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- Every Telegram error has now its **own class**, so it's easier to - fine-tune your ``except``\ 's. -- Markdown parsing is **not part** of Telethon itself anymore, although - there are plans to support it again through a some external module. -- The ``.list_sessions()`` has been moved to the ``Session`` class - instead. -- The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` - anymore. - -Additions -~~~~~~~~~ - -- A new, more **lightweight class** has been added. The - ``TelegramBareClient`` is now the base of the normal - ``TelegramClient``, and has the most basic features. -- New method to ``.create_new_connection()``, which can be ran **in - parallel** with the original connection. This will return the - previously mentioned ``TelegramBareClient`` already connected. -- Any file object can now be used to download a file (for instance, a - ``BytesIO()`` instead a file name). -- Vales like ``random_id`` are now **automatically inferred**, so you - can save yourself from the hassle of writing - ``generate_random_long()`` everywhere. Same applies to - ``.get_input_peer()``, unless you really need the extra performance - provided by skipping one ``if`` if called manually. -- Every type now features a new ``.to_dict()`` method. - -.. bug-fixes-6: - -Bug fixes -~~~~~~~~~ - -- Received errors are acknowledged to the server, so they don't happen - over and over. -- Downloading media on different data centers is now up to **x2 - faster**, since there used to be an ``InvalidDCError`` for each file - part tried to be downloaded. -- Lost messages are now properly skipped. -- New way to handle the **result of requests**. The old ``ValueError`` - "*The previously sent request must be resent. However, no request was - previously sent (possibly called from a different thread).*" *should* - not happen anymore. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Some fixes to the ``JsonSession``. -- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while - ``.reconnect()`` was being called on the ``UpdatesThread``. -- Some improvements on the ``TcpClient``, such as not switching between - blocking and non-blocking sockets. -- The code now uses ASCII characters only. -- Some enhancements to ``.find_user_or_chat()`` and - ``.get_input_peer()``. - -JSON session file (v0.10.1) -=========================== - -*Published at 2017/06/07* - -This version is primarily for people to **migrate** their ``.session`` -files, which are *pickled*, to the new *JSON* format. Although slightly -slower, and a bit more vulnerable since it's plain text, it's a lot more -resistant to upgrades. - -.. warning:: - - You **must** upgrade to this version before any higher one if you've - used Telethon ≤ v0.10. If you happen to upgrade to an higher version, - that's okay, but you will have to manually delete the ``*.session`` file, - and logout from that session from an official client. - -Additions -~~~~~~~~~ - -- New ``.get_me()`` function to get the **current** user. -- ``.is_user_authorized()`` is now more reliable. -- New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` - on the online documentation. -- **More error codes** added to the ``errors`` file. - -Enhancements -~~~~~~~~~~~~ - -- Everything on the documentation is now, theoretically, **sorted - alphabetically**. -- No second thread is spawned unless one or more update handlers are added. - -Full support for different DCs and ++stable (v0.10) -=================================================== - -*Published at 2017/06/03* - -Working with **different data centers** finally *works*! On a different -order of things, **reconnection** is now performed automatically every -time Telegram decides to kick us off their servers, so now Telethon can -really run **forever and ever**! In theory. - -Enhancements -~~~~~~~~~~~~ - -- **Documentation** improvements, such as showing the return type. -- The ``msg_id too low/high`` error should happen **less often**, if - any. -- Sleeping on the main thread is **not done anymore**. You will have to - ``except FloodWaitError``\ 's. -- You can now specify your *own application version*, device model, - system version and language code. -- Code is now more *pythonic* (such as making some members private), - and other internal improvements (which affect the **updates - thread**), such as using ``logger`` instead a bare ``print()`` too. - -This brings Telethon a whole step closer to ``v1.0``, though more things -should preferably be changed. - -Stability improvements (v0.9.1) -=============================== - -*Published at 2017/05/23* - -Telethon used to crash a lot when logging in for the very first time. -The reason for this was that the reconnection (or dead connections) were -not handled properly. Now they are, so you should be able to login -directly, without needing to delete the ``*.session`` file anymore. -Notice that downloading from a different DC is still a WIP. - -Enhancements -~~~~~~~~~~~~ - -- Updates thread is only started after a successful login. -- Files meant to be ran by the user now use **shebangs** and - proper permissions. -- In-code documentation now shows the returning type. -- **Relative import** is now used everywhere, so you can rename - ``telethon`` to anything else. -- **Dead connections** are now **detected** instead entering an infinite loop. -- **Sockets** can now be **closed** (and re-opened) properly. -- Telegram decided to update the layer 66 without increasing the number. - This has been fixed and now we're up-to-date again. - -General improvements (v0.9) -=========================== - -*Published at 2017/05/19* - -+-----------------------+ -| Scheme layer used: 66 | -+-----------------------+ - -Additions -~~~~~~~~~ - -- The **documentation**, available online - `here `__, has a new search bar. -- Better **cross-thread safety** by using ``threading.Event``. -- More improvements for running Telethon during a **long period of time**. - -Bug fixes -~~~~~~~~~ - -- **Avoid a certain crash on login** (occurred if an unexpected object - ID was received). -- Avoid crashing with certain invalid UTF-8 strings. -- Avoid crashing on certain terminals by using known ASCII characters - where possible. -- The ``UpdatesThread`` is now a daemon, and should cause less issues. -- Temporary sessions didn't actually work (with ``session=None``). - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. - -Bot login and proxy support (v0.8) -================================== - -*Published at 2017/04/14* - -Additions -~~~~~~~~~ - -- **Bot login**, thanks to @JuanPotato for hinting me about how to do - it. -- **Proxy support**, thanks to @exzhawk for implementing it. -- **Logging support**, used by passing ``--telethon-log=DEBUG`` (or - ``INFO``) as a command line argument. - -Bug fixes -~~~~~~~~~ - -- Connection fixes, such as avoiding connection until ``.connect()`` is - explicitly invoked. -- Uploading big files now works correctly. -- Fix uploading big files. -- Some fixes on the updates thread, such as correctly sleeping when required. - -Long-run bug fix (v0.7.1) -========================= - -*Published at 2017/02/19* - -If you're one of those who runs Telethon for a long time (more than 30 -minutes), this update by @strayge will be great for you. It sends -periodic pings to the Telegram servers so you don't get disconnected and -you can still send and receive updates! - -Two factor authentication (v0.7) -================================ - -*Published at 2017/01/31* - -+-----------------------+ -| Scheme layer used: 62 | -+-----------------------+ - -If you're one of those who love security the most, these are good news. -You can now use two factor authentication with Telethon too! As internal -changes, the coding style has been improved, and you can easily use -custom session objects, and various little bugs have been fixed. - -Updated pip version (v0.6) -========================== - -*Published at 2016/11/13* - -+-----------------------+ -| Scheme layer used: 57 | -+-----------------------+ - -This release has no new major features. However, it contains some small -changes that make using Telethon a little bit easier. Now those who have -installed Telethon via ``pip`` can also take advantage of changes, such -as less bugs, creating empty instances of ``TLObjects``, specifying a -timeout and more! - -Ready, pip, go! (v0.5) -====================== - -*Published at 2016/09/18* - -Telethon is now available as a **`Python -package `__**! Those are -really exciting news (except, sadly, the project structure had to change -*a lot* to be able to do that; but hopefully it won't need to change -much more, any more!) - -Not only that, but more improvements have also been made: you're now -able to both **sign up** and **logout**, watch a pretty -"Uploading/Downloading… x%" progress, and other minor changes which make -using Telethon **easier**. - -Made InteractiveTelegramClient cool (v0.4) -========================================== - -*Published at 2016/09/12* - -Yes, really cool! I promise. Even though this is meant to be a -*library*, that doesn't mean it can't have a good *interactive client* -for you to try the library out. This is why now you can do many, many -things with the ``InteractiveTelegramClient``: - -- **List dialogs** (chats) and pick any you wish. -- **Send any message** you like, text, photos or even documents. -- **List** the **latest messages** in the chat. -- **Download** any message's media (photos, documents or even contacts!). -- **Receive message updates** as you talk (i.e., someone sent you a message). - -It actually is an usable-enough client for your day by day. You could -even add ``libnotify`` and pop, you're done! A great cli-client with -desktop notifications. - -Also, being able to download and upload media implies that you can do -the same with the library itself. Did I need to mention that? Oh, and -now, with even less bugs! I hope. - -Media revolution and improvements to update handling! (v0.3) -============================================================ - -*Published at 2016/09/11* - -Telegram is more than an application to send and receive messages. You -can also **send and receive media**. Now, this implementation also gives -you the power to upload and download media from any message that -contains it! Nothing can now stop you from filling up all your disk -space with all the photos! If you want to, of course. - -Handle updates in their own thread! (v0.2) -========================================== - -*Published at 2016/09/10* - -This version handles **updates in a different thread** (if you wish to -do so). This means that both the low level ``TcpClient`` and the -not-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can -use them with more than a single thread without worrying! - -This also implies that you won't need to send a request to **receive an -update** (is someone typing? did they send me a message? has someone -gone offline?). They will all be received **instantly**. - -Some other cool examples of things that you can do: when someone tells -you "*Hello*", you can automatically reply with another "*Hello*" -without even needing to type it by yourself :) - -However, be careful with spamming!! Do **not** use the program for that! - -First working alpha version! (v0.1) -=================================== - -*Published at 2016/09/06* - -+-----------------------+ -| Scheme layer used: 55 | -+-----------------------+ - -There probably are some bugs left, which haven't yet been found. -However, the majority of code works and the application is already -usable! Not only that, but also uses the latest scheme as of now *and* -handles way better the errors. This tag is being used to mark this -release as stable enough. diff --git a/readthedocs/misc/compatibility-and-convenience.rst b/readthedocs/misc/compatibility-and-convenience.rst index 2d0769d4..e69de29b 100644 --- a/readthedocs/misc/compatibility-and-convenience.rst +++ b/readthedocs/misc/compatibility-and-convenience.rst @@ -1,187 +0,0 @@ -.. _compatibility-and-convenience: - -============================= -Compatibility and Convenience -============================= - -Telethon is an `asyncio` library. Compatibility is an important concern, -and while it can't always be kept and mistakes happens, the :ref:`changelog` -is there to tell you when these important changes happen. - -.. contents:: - - -Compatibility -============= - -Some decisions when developing will inevitable be proven wrong in the future. -One of these decisions was using threads. Now that Python 3.4 is reaching EOL -and using `asyncio` is usable as of Python 3.5 it makes sense for a library -like Telethon to make a good use of it. - -If you have old code, **just use old versions** of the library! There is -nothing wrong with that other than not getting new updates or fixes, but -using a fixed version with ``pip install telethon==0.19.1.6`` is easy -enough to do. - -You might want to consider using `Virtual Environments -`_ in your projects. - -There's no point in maintaining a synchronous version because the whole point -is that people don't have time to upgrade, and there has been several changes -and clean-ups. Using an older version is the right way to go. - -Sometimes, other small decisions are made. These all will be reflected in the -:ref:`changelog` which you should read when upgrading. - -If you want to jump the `asyncio` boat, here are some of the things you will -need to start migrating really old code: - -.. code-block:: python - - # 1. Import the client from telethon.sync - from telethon.sync import TelegramClient - - # 2. Change this monster... - try: - assert client.connect() - if not client.is_user_authorized(): - client.send_code_request(phone_number) - me = client.sign_in(phone_number, input('Enter code: ')) - - ... # REST OF YOUR CODE - finally: - client.disconnect() - - # ...for this: - with client: - ... # REST OF YOUR CODE - - # 3. client.idle() no longer exists. - # Change this... - client.idle() - # ...to this: - client.run_until_disconnected() - - # 4. client.add_update_handler no longer exists. - # Change this... - client.add_update_handler(handler) - # ...to this: - client.add_event_handler(handler) - - -In addition, all the update handlers must be ``async def``, and you need -to ``await`` method calls that rely on network requests, such as getting -the chat or sender. If you don't use updates, you're done! - - -Convenience -=========== - -.. note:: - - The entire documentation assumes you have done one of the following: - - .. code-block:: python - - from telethon import TelegramClient, sync - # or - from telethon.sync import TelegramClient - - This makes the examples shorter and easier to think about. - -For quick scripts that don't need updates, it's a lot more convenient to -forget about `asyncio` and just work with sequential code. This can prove -to be a powerful hybrid for running under the Python REPL too. - -.. code-block:: python - - from telethon.sync import TelegramClient - # ^~~~~ note this part; it will manage the asyncio loop for you - - with TelegramClient(...) as client: - print(client.get_me().username) - # ^ notice the lack of await, or loop.run_until_complete(). - # Since there is no loop running, this is done behind the scenes. - # - message = client.send_message('me', 'Hi!') - import time - time.sleep(5) - message.delete() - - # You can also have an hybrid between a synchronous - # part and asynchronous event handlers. - # - from telethon import events - @client.on(events.NewMessage(pattern='(?i)hi|hello')) - async def handler(event): - await event.reply('hey') - - client.run_until_disconnected() - - -Some methods, such as ``with``, ``start``, ``disconnect`` and -``run_until_disconnected`` work both in synchronous and asynchronous -contexts by default for convenience, and to avoid the little overhead -it has when using methods like sending a message, getting messages, etc. -This keeps the best of both worlds as a sane default. - -.. note:: - - As a rule of thumb, if you're inside an ``async def`` and you need - the client, you need to ``await`` calls to the API. If you call other - functions that also need API calls, make them ``async def`` and ``await`` - them too. Otherwise, there is no need to do so with this mode. - -Speed -===== - -When you're ready to micro-optimize your application, or if you simply -don't need to call any non-basic methods from a synchronous context, -just get rid of ``telethon.sync`` and work inside an ``async def``: - -.. code-block:: python - - import asyncio - from telethon import TelegramClient, events - - async def main(): - async with TelegramClient(...) as client: - print((await client.get_me()).username) - # ^_____________________^ notice these parenthesis - # You want to ``await`` the call, not the username. - # - message = await client.send_message('me', 'Hi!') - await asyncio.sleep(5) - await message.delete() - - @client.on(events.NewMessage(pattern='(?i)hi|hello')) - async def handler(event): - await event.reply('hey') - - await client.run_until_disconnected() - - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) - - -The ``telethon.sync`` magic module simply wraps every method behind: - -.. code-block:: python - - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) - -So that you don't have to write it yourself every time. That's the -overhead you pay if you import it, and what you save if you don't. - -Learning -======== - -You know the library uses `asyncio` everywhere, and you want to learn -how to do things right. Even though `asyncio` is its own topic, the -documentation wants you to learn how to use Telethon correctly, and for -that, you need to use `asyncio` correctly too. For this reason, there -is a section called :ref:`mastering-asyncio` that will introduce you to -the `asyncio` world, with links to more resources for learning how to -use it. Feel free to check that section out once you have read the rest. diff --git a/readthedocs/misc/wall-of-shame.rst b/readthedocs/misc/wall-of-shame.rst index 87be0464..e69de29b 100644 --- a/readthedocs/misc/wall-of-shame.rst +++ b/readthedocs/misc/wall-of-shame.rst @@ -1,65 +0,0 @@ -============= -Wall of Shame -============= - - -This project has an -`issues `__ section for -you to file **issues** whenever you encounter any when working with the -library. Said section is **not** for issues on *your* program but rather -issues with Telethon itself. - -If you have not made the effort to 1. read through the docs and 2. -`look for the method you need `__, -you will end up on the `Wall of -Shame `__, -i.e. all issues labeled -`"RTFM" `__: - - **rtfm** - Literally "Read The F--king Manual"; a term showing the - frustration of being bothered with questions so trivial that the asker - could have quickly figured out the answer on their own with minimal - effort, usually by reading readily-available documents. People who - say"RTFM!" might be considered rude, but the true rude ones are the - annoying people who take absolutely no self-responibility and expect to - have all the answers handed to them personally. - - *"Damn, that's the twelveth time that somebody posted this question - to the messageboard today! RTFM, already!"* - - *by Bill M. July 27, 2004* - -If you have indeed read the docs, and have tried looking for the method, -and yet you didn't find what you need, **that's fine**. Telegram's API -can have some obscure names at times, and for this reason, there is a -`"question" -label `__ -with questions that are okay to ask. Just state what you've tried so -that we know you've made an effort, or you'll go to the Wall of Shame. - -Of course, if the issue you're going to open is not even a question but -a real issue with the library (thankfully, most of the issues have been -that!), you won't end up here. Don't worry. - -Current winner --------------- - -The current winner is `issue -213 `__: - -**Issue:** - -.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg - -:alt: Winner issue - -Winner issue - -**Answer:** - -.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg - -:alt: Winner issue answer - -Winner issue answer diff --git a/readthedocs/modules/client.rst b/readthedocs/modules/client.rst index 6639200f..e69de29b 100644 --- a/readthedocs/modules/client.rst +++ b/readthedocs/modules/client.rst @@ -1,102 +0,0 @@ -.. _telethon-client: - -============== -TelegramClient -============== - -.. currentmodule:: telethon.client - -The `TelegramClient ` aggregates several mixin -classes to provide all the common functionality in a nice, Pythonic interface. -Each mixin has its own methods, which you all can use. - -**In short, to create a client you must run:** - -.. code-block:: python - - import asyncio - from telethon import TelegramClient - - async def main(): - client = await TelegramClient(name, api_id, api_hash).start() - # Now you can use all client methods listed below, like for example... - await client.send_message('me', 'Hello to myself!') - - asyncio.get_event_loop().run_until_complete(main()) - - -You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. -Together they are the `TelegramClient ` and -you can access all of their methods. - -See :ref:`client-ref` for a short summary. - -.. automodule:: telethon.client.telegramclient - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.telegrambaseclient - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.account - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.auth - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.bots - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.buttons - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.chats - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.dialogs - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.downloads - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.messageparse - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.messages - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.updates - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.uploads - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.users - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/custom.rst b/readthedocs/modules/custom.rst index 1f1329bf..e69de29b 100644 --- a/readthedocs/modules/custom.rst +++ b/readthedocs/modules/custom.rst @@ -1,145 +0,0 @@ -============== -Custom package -============== - -The `telethon.tl.custom` package contains custom classes that the library -uses in order to make working with Telegram easier. Only those that you -are supposed to use will be documented here. You can use undocumented ones -at your own risk. - -More often than not, you don't need to import these (unless you want -type hinting), nor do you need to manually create instances of these -classes. They are returned by client methods. - -.. contents:: - -.. automodule:: telethon.tl.custom - :members: - :undoc-members: - :show-inheritance: - - -AdminLogEvent -============= - -.. automodule:: telethon.tl.custom.adminlogevent - :members: - :undoc-members: - :show-inheritance: - - -Button -====== - -.. automodule:: telethon.tl.custom.button - :members: - :undoc-members: - :show-inheritance: - - -ChatGetter -========== - -.. automodule:: telethon.tl.custom.chatgetter - :members: - :undoc-members: - :show-inheritance: - - -Conversation -============ - -.. automodule:: telethon.tl.custom.conversation - :members: - :undoc-members: - :show-inheritance: - - -Dialog -====== - -.. automodule:: telethon.tl.custom.dialog - :members: - :undoc-members: - :show-inheritance: - - -Draft -===== - -.. automodule:: telethon.tl.custom.draft - :members: - :undoc-members: - :show-inheritance: - - -File -==== - -.. automodule:: telethon.tl.custom.file - :members: - :undoc-members: - :show-inheritance: - - -Forward -======= - -.. automodule:: telethon.tl.custom.forward - :members: - :undoc-members: - :show-inheritance: - - -InlineBuilder -============= - -.. automodule:: telethon.tl.custom.inlinebuilder - :members: - :undoc-members: - :show-inheritance: - - -InlineResult -============ - -.. automodule:: telethon.tl.custom.inlineresult - :members: - :undoc-members: - :show-inheritance: - - -InlineResults -============= - -.. automodule:: telethon.tl.custom.inlineresults - :members: - :undoc-members: - :show-inheritance: - - -Message -======= - -.. automodule:: telethon.tl.custom.message - :members: - :undoc-members: - :show-inheritance: - - -MessageButton -============= - -.. automodule:: telethon.tl.custom.messagebutton - :members: - :undoc-members: - :show-inheritance: - - -SenderGetter -============ - -.. automodule:: telethon.tl.custom.sendergetter - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/errors.rst b/readthedocs/modules/errors.rst index 0df239c9..e69de29b 100644 --- a/readthedocs/modules/errors.rst +++ b/readthedocs/modules/errors.rst @@ -1,19 +0,0 @@ -.. _telethon-errors: - -========== -API Errors -========== - -These are the base errors that Telegram's API may raise. - -See :ref:`rpc-errors` for a more friendly explanation. - -.. automodule:: telethon.errors.common - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.errors.rpcbaseerrors - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/events.rst b/readthedocs/modules/events.rst index 961885a0..e69de29b 100644 --- a/readthedocs/modules/events.rst +++ b/readthedocs/modules/events.rst @@ -1,65 +0,0 @@ -.. _telethon-events: - -============= -Update Events -============= - -.. currentmodule:: telethon.events - -Every event (builder) subclasses `common.EventBuilder`, -so all the methods in it can be used from any event builder/event instance. - -.. automodule:: telethon.events.common - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.newmessage - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.chataction - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.userupdate - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.messageedited - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.messagedeleted - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.messageread - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.callbackquery - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.inlinequery - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events.raw - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.events - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/helpers.rst b/readthedocs/modules/helpers.rst index cffe53e5..e69de29b 100644 --- a/readthedocs/modules/helpers.rst +++ b/readthedocs/modules/helpers.rst @@ -1,8 +0,0 @@ -======= -Helpers -======= - -.. automodule:: telethon.helpers - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/network.rst b/readthedocs/modules/network.rst index 3395fa51..e69de29b 100644 --- a/readthedocs/modules/network.rst +++ b/readthedocs/modules/network.rst @@ -1,33 +0,0 @@ -.. _telethon-network: - -================ -Connection Modes -================ - -The only part about network that you should worry about are -the different connection modes, which are the following: - -.. automodule:: telethon.network.connection.tcpfull - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.network.connection.tcpabridged - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.network.connection.tcpintermediate - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.network.connection.tcpobfuscated - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.network.connection.http - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/sessions.rst b/readthedocs/modules/sessions.rst index 86ae22a4..e69de29b 100644 --- a/readthedocs/modules/sessions.rst +++ b/readthedocs/modules/sessions.rst @@ -1,27 +0,0 @@ -.. _telethon-sessions: - -======== -Sessions -======== - -These are the different built-in session storage that you may subclass. - -.. automodule:: telethon.sessions.abstract - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.sessions.memory - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.sessions.sqlite - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.sessions.string - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/utils.rst b/readthedocs/modules/utils.rst index 2fab89a2..e69de29b 100644 --- a/readthedocs/modules/utils.rst +++ b/readthedocs/modules/utils.rst @@ -1,12 +0,0 @@ -.. _telethon-utils: - -========= -Utilities -========= - -These are the utilities that the library has to offer. - -.. automodule:: telethon.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/quick-references/client-reference.rst b/readthedocs/quick-references/client-reference.rst index 7a3e71ef..e69de29b 100644 --- a/readthedocs/quick-references/client-reference.rst +++ b/readthedocs/quick-references/client-reference.rst @@ -1,193 +0,0 @@ -.. _client-ref: - -================ -Client Reference -================ - -This page contains a summary of all the important methods and properties that -you may need when using Telethon. They are sorted by relevance and are not in -alphabetical order. - -You should use this page to learn about which methods are available, and -if you need an usage example or further description of the arguments, be -sure to follow the links. - -.. contents:: - -TelegramClient -============== - -This is a summary of the methods and -properties you will find at :ref:`telethon-client`. - -Auth ----- - -.. currentmodule:: telethon.client.auth.AuthMethods - -.. autosummary:: - :nosignatures: - - start - send_code_request - sign_in - sign_up - log_out - edit_2fa - -Base ----- - -.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient - -.. autosummary:: - :nosignatures: - - connect - disconnect - is_connected - disconnected - loop - -Messages --------- - -.. py:currentmodule:: telethon.client.messages.MessageMethods - -.. autosummary:: - :nosignatures: - - send_message - edit_message - delete_messages - forward_messages - iter_messages - get_messages - pin_message - send_read_acknowledge - -Uploads -------- - -.. py:currentmodule:: telethon.client.uploads.UploadMethods - -.. autosummary:: - :nosignatures: - - send_file - upload_file - -Downloads ---------- - -.. currentmodule:: telethon.client.downloads.DownloadMethods - -.. autosummary:: - :nosignatures: - - download_media - download_profile_photo - download_file - -Dialogs -------- - -.. py:currentmodule:: telethon.client.dialogs.DialogMethods - -.. autosummary:: - :nosignatures: - - iter_dialogs - get_dialogs - archive - iter_drafts - get_drafts - delete_dialog - conversation - -Users ------ - -.. py:currentmodule:: telethon.client.users.UserMethods - -.. autosummary:: - :nosignatures: - - get_me - is_bot - is_user_authorized - get_entity - get_input_entity - get_peer_id - -Chats ------ - -.. currentmodule:: telethon.client.chats.ChatMethods - -.. autosummary:: - :nosignatures: - - iter_participants - get_participants - iter_admin_log - get_admin_log - iter_profile_photos - get_profile_photos - action - -Parse Mode ----------- - -.. py:currentmodule:: telethon.client.messageparse.MessageParseMethods - -.. autosummary:: - :nosignatures: - - parse_mode - -Updates -------- - -.. py:currentmodule:: telethon.client.updates.UpdateMethods - -.. autosummary:: - :nosignatures: - - on - run_until_disconnected - add_event_handler - remove_event_handler - list_event_handlers - catch_up - -Bots ----- - -.. currentmodule:: telethon.client.bots.BotMethods - -.. autosummary:: - :nosignatures: - - inline_query - -Buttons -------- - -.. currentmodule:: telethon.client.buttons.ButtonMethods - -.. autosummary:: - :nosignatures: - - build_reply_markup - -Account -------- - -.. currentmodule:: telethon.client.account.AccountMethods - -.. autosummary:: - :nosignatures: - - takeout - end_takeout diff --git a/readthedocs/quick-references/events-reference.rst b/readthedocs/quick-references/events-reference.rst index cba224cd..e69de29b 100644 --- a/readthedocs/quick-references/events-reference.rst +++ b/readthedocs/quick-references/events-reference.rst @@ -1,202 +0,0 @@ -================ -Events Reference -================ - -Here you will find a quick summary of all the methods -and properties that you can access when working with events. - -You can access the client that creates this event by doing -``event.client``, and you should view the description of the -events to find out what arguments it allows on creation and -its **attributes** (the properties will be shown here). - -.. important:: - - Remember that **all events base** `ChatGetter - `! Please see :ref:`faq` - if you don't know what this means or the implications of it. - -.. contents:: - - -NewMessage -========== - -Occurs whenever a new text message or a message with media arrives. - -.. note:: - - The new message event **should be treated as** a - normal `Message `, with - the following exceptions: - - * ``pattern_match`` is the match object returned by ``pattern=``. - * ``message`` is **not** the message string. It's the `Message - ` object. - - Remember, this event is just a proxy over the message, so while - you won't see its attributes and properties, you can still access - them. Please see the full documentation for examples. - -Full documentation for the `NewMessage -`. - - -MessageEdited -============= - -Occurs whenever a message is edited. Just like `NewMessage -`, you should treat -this event as a `Message `. - -Full documentation for the `MessageEdited -`. - - -MessageDeleted -============== - -Occurs whenever a message is deleted. Note that this event isn't 100% -reliable, since Telegram doesn't always notify the clients that a message -was deleted. - -It only has the ``deleted_id`` and ``deleted_ids`` attributes -(in addition to the chat if the deletion happened in a channel). - -Full documentation for the `MessageDeleted -`. - - -MessageRead -=========== - -Occurs whenever one or more messages are read in a chat. - -Full documentation for the `MessageRead -`. - -.. currentmodule:: telethon.events.messageread.MessageRead.Event - -.. autosummary:: - :nosignatures: - - inbox - message_ids - - get_messages - is_read - - -ChatAction -========== - -Occurs whenever a user joins or leaves a chat, or a message is pinned. - -Full documentation for the `ChatAction -`. - -.. currentmodule:: telethon.events.chataction.ChatAction.Event - -.. autosummary:: - :nosignatures: - - added_by - kicked_by - user - input_user - user_id - users - input_users - user_ids - - respond - reply - delete - get_pinned_message - get_added_by - get_kicked_by - get_user - get_input_user - get_users - get_input_users - - -UserUpdate -========== - -Occurs whenever a user goes online, starts typing, etc. - -A lot of fields are attributes and not properties, so they -are not shown here. Please refer to its full documentation. - -Full documentation for the `UserUpdate -`. - -.. currentmodule:: telethon.events.userupdate.UserUpdate.Event - -.. autosummary:: - :nosignatures: - - user - input_user - user_id - - get_user - get_input_user - - -CallbackQuery -============= - -Occurs whenever you sign in as a bot and a user -clicks one of the inline buttons on your messages. - -Full documentation for the `CallbackQuery -`. - -.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event - -.. autosummary:: - :nosignatures: - - id - message_id - data - chat_instance - via_inline - - respond - reply - edit - delete - answer - get_message - -InlineQuery -=========== - -Occurs whenever you sign in as a bot and a user -sends an inline query such as ``@bot query``. - -Full documentation for the `InlineQuery -`. - -.. currentmodule:: telethon.events.inlinequery.InlineQuery.Event - -.. autosummary:: - :nosignatures: - - id - text - offset - geo - builder - - answer - -Raw -=== - -Raw events are not actual events. Instead, they are the raw -:tl:`Update` object that Telegram sends. You normally shouldn't -need these. diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst index f452b9ea..e69de29b 100644 --- a/readthedocs/quick-references/faq.rst +++ b/readthedocs/quick-references/faq.rst @@ -1,221 +0,0 @@ -.. _faq: - -=== -FAQ -=== - -Let's start the quick references section with some useful tips to keep in -mind, with the hope that you will understand why certain things work the -way that they do. - -.. contents:: - - -Code without errors doesn't work -================================ - -Then it probably has errors, but you haven't enabled logging yet. -To enable logging, at the following code to the top of your main file: - -.. code-block:: python - - import logging - logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s', - level=logging.WARNING) - -You can change the logging level to be something different, from less to more information: - -.. code-block:: python - - level=logging.CRITICAL # won't show errors (same as disabled) - level=logging.ERROR # will only show errors that you didn't handle - level=logging.WARNING # will also show messages with medium severity, such as internal Telegram issues - level=logging.INFO # will also show informational messages, such as connection or disconnections - level=logging.DEBUG # will show a lot of output to help debugging issues in the library - -See the official Python documentation for more information on logging_. - - -How can I except FloodWaitError? -================================ - -You can use all errors from the API by importing: - -.. code-block:: python - - from telethon import errors - -And except them as such: - -.. code-block:: python - - try: - client.send_message(chat, 'Hi') - except errors.FloodWaitError as e: - # e.seconds is how many seconds you have - # to wait before making the request again. - print('Flood for', e.seconds) - - -My account was deleted/limited when using the library -===================================================== - -The library will only do things that you tell it to do. If you use -the library with bad intentions, Telegram will hopefully ban you. - -However, you may also be part of a limited country, such as Iran or Russia. -In that case, we have bad news for you. Telegram is much more likely to ban -these numbers, as they are often used to spam other accounts, likely through -the use of libraries like this one. The best advice we can give you is to not -abuse the API, like calling many requests really quickly, and to sign up with -these phones through an official application. - -We have also had reports from Kazakhstan and China, where connecting -would fail. To solve these connection problems, you should use a proxy. - -Telegram may also ban virtual (VoIP) phone numbers, -as again, they're likely to be used for spam. - -If you want to check if your account has been limited, -simply send a private message to `@SpamBot`_ through Telegram itself. -You should notice this by getting errors like ``PeerFloodError``, -which means you're limited, for instance, -when sending a message to some accounts but not others. - -For more discussion, please see `issue 297`_. - - -How can I use a proxy? -====================== - -This was one of the first things described in :ref:`signing-in`. - - -How do I access a field? -======================== - -This is basic Python knowledge. You should use the dot operator: - -.. code-block:: python - - me = client.get_me() - print(me.username) - # ^ we used the dot operator to access the username attribute - - result = client(functions.photos.GetUserPhotosRequest( - user_id='me', - offset=0, - max_id=0, - limit=100 - )) - - # Working with list is also pretty basic - print(result.photos[0].sizes[-1].type) - # ^ ^ ^ ^ ^ - # | | | | \ type - # | | | \ last size - # | | \ list of sizes - # access | \ first photo from the list - # the... \ list of photos - # - # To print all, you could do (or mix-and-match): - for photo in result.photos: - for size in photo.sizes: - print(size.type) - - -AttributeError: 'coroutine' object has no attribute 'id' -======================================================== - -You either forgot to: - -.. code-block:: python - - import telethon.sync - # ^^^^^ import sync - -Or: - -.. code-block:: python - - async def handler(event): - me = await client.get_me() - # ^^^^^ note the await - print(me.username) - - -sqlite3.OperationalError: database is locked -============================================ - -An older process is still running and is using the same ``'session'`` file. - -This error occurs when **two or more clients use the same session**, -that is, when you write the same session name to be used in the client: - -* You have an older process using the same session file. -* You have two different scripts running (interactive sessions count too). -* You have two clients in the same script running at the same time. - -The solution is, if you need two clients, use two sessions. If the -problem persists and you're on Linux, you can use ``fuser my.session`` -to find out the process locking the file. As a last resort, you can -reboot your system. - -If you really dislike SQLite, use a different session storage. There -is an entire section covering that at :ref:`sessions`. - - -event.chat or event.sender is None -================================== - -Telegram doesn't always send this information in order to save bandwidth. -If you need the information, you should fetch it yourself, since the library -won't do unnecessary work unless you need to: - -.. code-block:: python - - async def handler(event): - chat = await event.get_chat() - sender = await event.get_sender() - - -What does "bases ChatGetter" mean? -================================== - -In Python, classes can base others. This is called `inheritance -`_. What it means is that -"if a class bases another, you can use the other's methods too". - -For example, `Message ` *bases* -`ChatGetter `. In turn, -`ChatGetter ` defines -things like `obj.chat_id `. - -So if you have a message, you can access that too: - -.. code-block:: python - - # ChatGetter has a chat_id property, and Message bases ChatGetter. - # Thus you can use ChatGetter properties and methods from Message - print(message.chat_id) - - -Telegram has a lot to offer, and inheritance helps the library reduce -boilerplate, so it's important to know this concept. For newcomers, -this may be a problem, so we explain what it means here in the FAQ. - - -Can I use Flask with the library? -================================= - -Yes, if you know what you are doing. However, you will probably have a -lot of headaches to get threads and asyncio to work together. Instead, -consider using `Quart `_, an asyncio-based -alternative to `Flask `_. - -Check out `quart_login.py`_ for an example web-application based on Quart. - -.. _logging: https://docs.python.org/3/library/logging.html -.. _@SpamBot: https://t.me/SpamBot -.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297 -.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples#quart_loginpy diff --git a/readthedocs/quick-references/objects-reference.rst b/readthedocs/quick-references/objects-reference.rst index c8b2e876..e69de29b 100644 --- a/readthedocs/quick-references/objects-reference.rst +++ b/readthedocs/quick-references/objects-reference.rst @@ -1,347 +0,0 @@ -================= -Objects Reference -================= - -This is the quick reference for those objects returned by client methods -or other useful modules that the library has to offer. They are kept in -a separate page to help finding and discovering them. - -Remember that this page only shows properties and methods, -**not attributes**. Make sure to open the full documentation -to find out about the attributes. - -.. contents:: - - -ChatGetter -========== - -All events base `ChatGetter `, -and some of the objects below do too, so it's important to know its methods. - -.. currentmodule:: telethon.tl.custom.chatgetter.ChatGetter - -.. autosummary:: - :nosignatures: - - chat - input_chat - chat_id - is_private - is_group - is_channel - - get_chat - get_input_chat - - -SenderGetter -============ - -Similar to `ChatGetter `, a -`SenderGetter ` is the same, -but it works for senders instead. - -.. currentmodule:: telethon.tl.custom.sendergetter.SenderGetter - -.. autosummary:: - :nosignatures: - - sender - input_sender - sender_id - - get_sender - get_input_sender - - -Message -======= - -.. currentmodule:: telethon.tl.custom.message - -The `Message` type is very important, mostly because we are working -with a library for a *messaging* platform, so messages are widely used: -in events, when fetching history, replies, etc. - -It bases `ChatGetter ` and -`SenderGetter `. - -Properties ----------- - -.. note:: - - We document *custom properties* here, not all the attributes of the - `Message` (which is the information Telegram actually returns). - -.. currentmodule:: telethon.tl.custom.message.Message - -.. autosummary:: - :nosignatures: - - text - raw_text - is_reply - forward - buttons - button_count - file - photo - document - web_preview - audio - voice - video - video_note - gif - sticker - contact - game - geo - invoice - poll - venue - action_entities - via_bot - via_input_bot - client - - -Methods -------- - -.. autosummary:: - :nosignatures: - - respond - reply - forward_to - edit - delete - get_reply_message - click - pin - download_media - get_entities_text - get_buttons - - -File -==== - -The `File ` type is a wrapper object -returned by `Message.file `, -and you can use it to easily access a document's attributes, such as -its name, bot-API style file ID, etc. - -.. currentmodule:: telethon.tl.custom.file.File - -.. autosummary:: - :nosignatures: - - id - name - width - height - size - duration - title - performer - emoji - sticker_set - - -Conversation -============ - -The `Conversation ` object -is returned by the `client.conversation() -` method to easily -send and receive responses like a normal conversation. - -It bases `ChatGetter `. - -.. currentmodule:: telethon.tl.custom.conversation.Conversation - -.. autosummary:: - :nosignatures: - - send_message - send_file - mark_read - get_response - get_reply - get_edit - wait_read - wait_event - cancel - - -AdminLogEvent -============= - -The `AdminLogEvent ` object -is returned by the `client.iter_admin_log() -` method to easily iterate -over past "events" (deleted messages, edits, title changes, leaving members…) - -These are all the properties you can find in it: - -.. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent - -.. autosummary:: - :nosignatures: - - id - date - user_id - action - old - new - changed_about - changed_title - changed_username - changed_photo - changed_sticker_set - changed_message - deleted_message - changed_admin - changed_restrictions - changed_invites - joined - joined_invite - left - changed_hide_history - changed_signatures - changed_pin - changed_default_banned_rights - stopped_poll - - -Button -====== - -The `Button ` class is used when you login -as a bot account to send messages with reply markup, such as inline buttons -or custom keyboards. - -These are the static methods you can use to create instances of the markup: - -.. currentmodule:: telethon.tl.custom.button.Button - -.. autosummary:: - :nosignatures: - - inline - switch_inline - url - text - request_location - request_phone - clear - force_reply - - -InlineResult -============ - -The `InlineResult ` object -is returned inside a list by the `client.inline_query() -` method to make an inline -query to a bot that supports being used in inline mode, such as -`@like `_. - -Note that the list returned is in fact a *subclass* of a list called -`InlineResults `, which, -in addition of being a list (iterator, indexed access, etc.), has extra -attributes and methods. - -These are the constants for the types, properties and methods you -can find the individual results: - -.. currentmodule:: telethon.tl.custom.inlineresult.InlineResult - -.. autosummary:: - :nosignatures: - - ARTICLE - PHOTO - GIF - VIDEO - VIDEO_GIF - AUDIO - DOCUMENT - LOCATION - VENUE - CONTACT - GAME - type - message - title - description - url - photo - document - click - download_media - - -Dialog -====== - -The `Dialog ` object is returned when -you call `client.iter_dialogs() `. - -.. currentmodule:: telethon.tl.custom.dialog.Dialog - -.. autosummary:: - :nosignatures: - - send_message - archive - delete - - -Draft -====== - -The `Draft ` object is returned when -you call `client.iter_drafts() `. - -.. currentmodule:: telethon.tl.custom.draft.Draft - -.. autosummary:: - :nosignatures: - - entity - input_entity - get_entity - get_input_entity - text - raw_text - is_empty - set_message - send - delete - - -Utils -===== - -The `telethon.utils` module has plenty of methods that make using the -library a lot easier. Only the interesting ones will be listed here. - -.. currentmodule:: telethon.utils - -.. autosummary:: - :nosignatures: - - get_display_name - get_extension - get_inner_text - get_peer_id - resolve_id - pack_bot_file_id - resolve_bot_file_id - resolve_invite_link diff --git a/readthedocs/requirements.txt b/readthedocs/requirements.txt index 97c7493d..e69de29b 100644 --- a/readthedocs/requirements.txt +++ b/readthedocs/requirements.txt @@ -1 +0,0 @@ -telethon \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2b650ec4..e69de29b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +0,0 @@ -pyaes -rsa diff --git a/setup.py b/setup.py index 8e2c30b3..e69de29b 100755 --- a/setup.py +++ b/setup.py @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -"""A setuptools based setup module. - -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject - -Extra supported commands are: -* gen, to generate the classes required for Telethon to run or docs -* pypi, to generate sdist, bdist_wheel, and push to PyPi -""" - -import itertools -import json -import re -import shutil -from os import chdir -from pathlib import Path -from subprocess import run -from sys import argv - -from setuptools import find_packages, setup - - -class TempWorkDir: - """Switches the working directory to be the one on which this file lives, - while within the 'with' block. - """ - def __init__(self): - self.original = None - - def __enter__(self): - self.original = Path('.') - chdir(str(Path(__file__).parent)) - return self - - def __exit__(self, *args): - chdir(str(self.original)) - - -GENERATOR_DIR = Path('telethon_generator') -LIBRARY_DIR = Path('telethon') - -ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' -ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' - -METHODS_IN = GENERATOR_DIR / 'data/methods.csv' - -# Which raw API methods are covered by *friendly* methods in the client? -FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv' - -TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] -TLOBJECT_OUT = LIBRARY_DIR / 'tl' -IMPORT_DEPTH = 2 - -DOCS_IN_RES = GENERATOR_DIR / 'data/html' -DOCS_OUT = Path('docs') - - -def generate(which, action='gen'): - from telethon_generator.parsers import\ - parse_errors, parse_methods, parse_tl, find_layer - - from telethon_generator.generators import\ - generate_errors, generate_tlobjects, generate_docs, clean_tlobjects - - layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) - errors = list(parse_errors(ERRORS_IN)) - methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors})) - - tlobjects = list(itertools.chain(*( - parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS))) - - if not which: - which.extend(('tl', 'errors')) - - clean = action == 'clean' - action = 'Cleaning' if clean else 'Generating' - - if 'all' in which: - which.remove('all') - for x in ('tl', 'errors', 'docs'): - if x not in which: - which.append(x) - - if 'tl' in which: - which.remove('tl') - print(action, 'TLObjects...') - if clean: - clean_tlobjects(TLOBJECT_OUT) - else: - generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT) - - if 'errors' in which: - which.remove('errors') - print(action, 'RPCErrors...') - if clean: - if ERRORS_OUT.is_file(): - ERRORS_OUT.unlink() - else: - with ERRORS_OUT.open('w') as file: - generate_errors(errors, file) - - if 'docs' in which: - which.remove('docs') - print(action, 'documentation...') - if clean: - if DOCS_OUT.is_dir(): - shutil.rmtree(str(DOCS_OUT)) - else: - generate_docs(tlobjects, methods, layer, DOCS_IN_RES, DOCS_OUT) - - if 'json' in which: - which.remove('json') - print(action, 'JSON schema...') - json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS] - if clean: - for file in json_files: - if file.is_file(): - file.unlink() - else: - def gen_json(fin, fout): - meths = [] - constructors = [] - for tl in parse_tl(fin, layer): - if tl.is_function: - meths.append(tl.to_dict()) - else: - constructors.append(tl.to_dict()) - what = {'constructors': constructors, 'methods': meths} - with open(fout, 'w') as f: - json.dump(what, f, indent=2) - - for fs in zip(TLOBJECT_IN_TLS, json_files): - gen_json(*fs) - - if which: - print('The following items were not understood:', which) - print(' Consider using only "tl", "errors" and/or "docs".') - print(' Using only "clean" will clean them. "all" to act on all.') - print(' For instance "gen tl errors".') - - -def main(): - if len(argv) >= 2 and argv[1] in ('gen', 'clean'): - generate(argv[2:], argv[1]) - - elif len(argv) >= 2 and argv[1] == 'pypi': - # (Re)generate the code to make sure we don't push without it - generate(['tl', 'errors']) - - # Try importing the telethon module to assert it has no errors - try: - import telethon - except: - print('Packaging for PyPi aborted, importing the module failed.') - return - - for x in ('build', 'dist', 'Telethon.egg-info'): - shutil.rmtree(x, ignore_errors=True) - run('python3 setup.py sdist', shell=True) - run('python3 setup.py bdist_wheel', shell=True) - run('twine upload dist/*', shell=True) - for x in ('build', 'dist', 'Telethon.egg-info'): - shutil.rmtree(x, ignore_errors=True) - - else: - # e.g. install from GitHub - if GENERATOR_DIR.is_dir(): - generate(['tl', 'errors']) - - # Get the long description from the README file - with open('README.rst', 'r', encoding='utf-8') as f: - long_description = f.read() - - with open('telethon/version.py', 'r', encoding='utf-8') as f: - version = re.search(r"^__version__\s*=\s*'(.*)'.*$", - f.read(), flags=re.MULTILINE).group(1) - setup( - name='Telethon', - version=version, - description="Full-featured Telegram client library for Python 3", - long_description=long_description, - - url='https://github.com/LonamiWebs/Telethon', - download_url='https://github.com/LonamiWebs/Telethon/releases', - - author='Lonami Exo', - author_email='totufals@hotmail.com', - - license='MIT', - - # See https://stackoverflow.com/a/40300957/4759433 - # -> https://www.python.org/dev/peps/pep-0345/#requires-python - # -> http://setuptools.readthedocs.io/en/latest/setuptools.html - python_requires='>=3.5', - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 5 - Production/Stable', - - 'Intended Audience :: Developers', - 'Topic :: Communications :: Chat', - - 'License :: OSI Approved :: MIT License', - - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6' - ], - keywords='telegram api chat client library messaging mtproto', - packages=find_packages(exclude=[ - 'telethon_*', 'run_tests.py', 'try_telethon.py' - ]), - install_requires=['pyaes', 'rsa'], - extras_require={ - 'cryptg': ['cryptg'] - } - ) - - -if __name__ == '__main__': - with TempWorkDir(): # Could just use a try/finally but this is + reusable - main() diff --git a/telethon/__init__.py b/telethon/__init__.py index 2df62135..e69de29b 100644 --- a/telethon/__init__.py +++ b/telethon/__init__.py @@ -1,13 +0,0 @@ -from .client.telegramclient import TelegramClient -from .network import connection -from .tl import types, functions, custom -from .tl.custom import Button -from . import version, events, utils, errors - -__version__ = version.__version__ - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/client/__init__.py b/telethon/client/__init__.py index e0463ab0..e69de29b 100644 --- a/telethon/client/__init__.py +++ b/telethon/client/__init__.py @@ -1,25 +0,0 @@ -""" -This package defines clients as subclasses of others, and then a single -`telethon.client.telegramclient.TelegramClient` which is subclass of them -all to provide the final unified interface while the methods can live in -different subclasses to be more maintainable. - -The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the -first implementor is `telethon.client.users.UserMethods`, since calling -requests require them to be resolved first, and that requires accessing -entities (users). -""" -from .telegrambaseclient import TelegramBaseClient -from .users import UserMethods # Required for everything -from .messageparse import MessageParseMethods # Required for messages -from .uploads import UploadMethods # Required for messages to send files -from .updates import UpdateMethods # Required for buttons (register callbacks) -from .buttons import ButtonMethods # Required for messages to use buttons -from .messages import MessageMethods -from .chats import ChatMethods -from .dialogs import DialogMethods -from .downloads import DownloadMethods -from .account import AccountMethods -from .auth import AuthMethods -from .bots import BotMethods -from .telegramclient import TelegramClient diff --git a/telethon/client/account.py b/telethon/client/account.py index eaa4cf89..e69de29b 100644 --- a/telethon/client/account.py +++ b/telethon/client/account.py @@ -1,243 +0,0 @@ -import functools -import inspect -import typing - -from .users import _NOT_A_REQUEST -from .. import helpers, utils -from ..tl import functions, TLRequest - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -# TODO Make use of :tl:`InvokeWithMessagesRange` somehow -# For that, we need to use :tl:`GetSplitRanges` first. -class _TakeoutClient: - """ - Proxy object over the client. - """ - __PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__') - - def __init__(self, finalize, client, request): - # We use the name mangling for attributes to make them inaccessible - # from within the shadowed client object and to distinguish them from - # its own attributes where needed. - self.__finalize = finalize - self.__client = client - self.__request = request - self.__success = None - - @property - def success(self): - return self.__success - - @success.setter - def success(self, value): - self.__success = value - - async def __aenter__(self): - # Enter/Exit behaviour is "overrode", we don't want to call start. - client = self.__client - if client.session.takeout_id is None: - client.session.takeout_id = (await client(self.__request)).id - elif self.__request is not None: - raise ValueError("Can't send a takeout request while another " - "takeout for the current session still not been finished yet.") - return self - - async def __aexit__(self, exc_type, exc_value, traceback): - if self.__success is None and self.__finalize: - self.__success = exc_type is None - - if self.__success is not None: - result = await self(functions.account.FinishTakeoutSessionRequest( - self.__success)) - if not result: - raise ValueError("Failed to finish the takeout.") - self.session.takeout_id = None - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - async def __call__(self, request, ordered=False): - takeout_id = self.__client.session.takeout_id - if takeout_id is None: - raise ValueError('Takeout mode has not been initialized ' - '(are you calling outside of "with"?)') - - single = not utils.is_list_like(request) - requests = ((request,) if single else request) - wrapped = [] - for r in requests: - if not isinstance(r, TLRequest): - raise _NOT_A_REQUEST() - await r.resolve(self, utils) - wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r)) - - return await self.__client( - wrapped[0] if single else wrapped, ordered=ordered) - - def __getattribute__(self, name): - # We access class via type() because __class__ will recurse infinitely. - # Also note that since we've name-mangled our own class attributes, - # they'll be passed to __getattribute__() as already decorated. For - # example, 'self.__client' will be passed as '_TakeoutClient__client'. - # https://docs.python.org/3/tutorial/classes.html#private-variables - if name.startswith('__') and name not in type(self).__PROXY_INTERFACE: - raise AttributeError # force call of __getattr__ - - # Try to access attribute in the proxy object and check for the same - # attribute in the shadowed object (through our __getattr__) if failed. - return super().__getattribute__(name) - - def __getattr__(self, name): - value = getattr(self.__client, name) - if inspect.ismethod(value): - # Emulate bound methods behavior by partially applying our proxy - # class as the self parameter instead of the client. - return functools.partial( - getattr(self.__client.__class__, name), self) - - return value - - def __setattr__(self, name, value): - if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))): - # This is our own name-mangled attribute, keep calm. - return super().__setattr__(name, value) - return setattr(self.__client, name, value) - - -class AccountMethods: - def takeout( - self: 'TelegramClient', - finalize: bool = True, - *, - contacts: bool = None, - users: bool = None, - chats: bool = None, - megagroups: bool = None, - channels: bool = None, - files: bool = None, - max_file_size: bool = None) -> 'TelegramClient': - """ - Returns a :ref:`telethon-client` which calls methods behind a takeout session. - - It does so by creating a proxy object over the current client through - which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap - them. In other words, returns the current client modified so that - requests are done as a takeout: - - Some of the calls made through the takeout session will have lower - flood limits. This is useful if you want to export the data from - conversations or mass-download media, since the rate limits will - be lower. Only some requests will be affected, and you will need - to adjust the `wait_time` of methods like `client.iter_messages - `. - - By default, all parameters are ``None``, and you need to enable those - you plan to use by setting them to either ``True`` or ``False``. - - You should ``except errors.TakeoutInitDelayError as e``, since this - exception will raise depending on the condition of the session. You - can then access ``e.seconds`` to know how long you should wait for - before calling the method again. - - There's also a `success` property available in the takeout proxy - object, so from the `with` body you can set the boolean result that - will be sent back to Telegram. But if it's left ``None`` as by - default, then the action is based on the `finalize` parameter. If - it's ``True`` then the takeout will be finished, and if no exception - occurred during it, then ``True`` will be considered as a result. - Otherwise, the takeout will not be finished and its ID will be - preserved for future usage as `client.session.takeout_id - `. - - Arguments - finalize (`bool`): - Whether the takeout session should be finalized upon - exit or not. - - contacts (`bool`): - Set to ``True`` if you plan on downloading contacts. - - users (`bool`): - Set to ``True`` if you plan on downloading information - from users and their private conversations with you. - - chats (`bool`): - Set to ``True`` if you plan on downloading information - from small group chats, such as messages and media. - - megagroups (`bool`): - Set to ``True`` if you plan on downloading information - from megagroups (channels), such as messages and media. - - channels (`bool`): - Set to ``True`` if you plan on downloading information - from broadcast channels, such as messages and media. - - files (`bool`): - Set to ``True`` if you plan on downloading media and - you don't only wish to export messages. - - max_file_size (`int`): - The maximum file size, in bytes, that you plan - to download for each message with media. - - Example - .. code-block:: python - - from telethon import errors - - try: - with client.takeout() as takeout: - client.get_messages('me') # normal call - takeout.get_messages('me') # wrapped through takeout (less limits) - - for message in takeout.iter_messages(chat, wait_time=0): - ... # Do something with the message - - except errors.TakeoutInitDelayError as e: - print('Must wait', e.seconds, 'before takeout') - """ - request_kwargs = dict( - contacts=contacts, - message_users=users, - message_chats=chats, - message_megagroups=megagroups, - message_channels=channels, - files=files, - file_max_size=max_file_size - ) - arg_specified = (arg is not None for arg in request_kwargs.values()) - - if self.session.takeout_id is None or any(arg_specified): - request = functions.account.InitTakeoutSessionRequest( - **request_kwargs) - else: - request = None - - return _TakeoutClient(finalize, self, request) - - async def end_takeout(self: 'TelegramClient', success: bool) -> bool: - """ - Finishes the current takeout session. - - Arguments - success (`bool`): - Whether the takeout completed successfully or not. - - Returns - ``True`` if the operation was successful, ``False`` otherwise. - - Example - .. code-block:: python - - client.end_takeout(success=False) - """ - try: - async with _TakeoutClient(True, self, None) as takeout: - takeout.success = success - except ValueError: - return False - return True diff --git a/telethon/client/auth.py b/telethon/client/auth.py index c201c112..e69de29b 100644 --- a/telethon/client/auth.py +++ b/telethon/client/auth.py @@ -1,638 +0,0 @@ -import getpass -import inspect -import os -import sys -import typing - -from .. import utils, helpers, errors, password as pwd_mod -from ..tl import types, functions - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class AuthMethods: - - # region Public methods - - def start( - self: 'TelegramClient', - phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '), - password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '), - *, - bot_token: str = None, - force_sms: bool = False, - code_callback: typing.Callable[[], typing.Union[str, int]] = None, - first_name: str = 'New User', - last_name: str = '', - max_attempts: int = 3) -> 'TelegramClient': - """ - Starts the client (connects and logs in if necessary). - - By default, this method will be interactive (asking for - user input if needed), and will handle 2FA if enabled too. - - If the phone doesn't belong to an existing account (and will hence - `sign_up` for a new one), **you are agreeing to Telegram's - Terms of Service. This is required and your account - will be banned otherwise.** See https://telegram.org/tos - and https://core.telegram.org/api/terms. - - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. - - Arguments - phone (`str` | `int` | `callable`): - The phone (or callable without arguments to get it) - to which the code will be sent. If a bot-token-like - string is given, it will be used as such instead. - The argument may be a coroutine. - - password (`str`, `callable`, optional): - The password for 2 Factor Authentication (2FA). - This is only required if it is enabled in your account. - The argument may be a coroutine. - - bot_token (`str`): - Bot Token obtained by `@BotFather `_ - to log in as a bot. Cannot be specified with ``phone`` (only - one of either allowed). - - force_sms (`bool`, optional): - Whether to force sending the code request as SMS. - This only makes sense when signing in with a `phone`. - - code_callback (`callable`, optional): - A callable that will be used to retrieve the Telegram - login code. Defaults to `input()`. - The argument may be a coroutine. - - first_name (`str`, optional): - The first name to be used if signing up. This has no - effect if the account already exists and you sign in. - - last_name (`str`, optional): - Similar to the first name, but for the last. Optional. - - max_attempts (`int`, optional): - How many times the code/password callback should be - retried or switching between signing in and signing up. - - Returns - This `TelegramClient`, so initialization - can be chained with ``.start()``. - - Example - .. code-block:: python - - client = TelegramClient('anon', api_id, api_hash) - - # Starting as a bot account - client.start(bot_token=bot_token) - - # Starting as an user account - client.start(phone) - # Please enter the code you received: 12345 - # Please enter your password: ******* - # (You are now logged in) - - # Starting using a context manager (this calls start()): - with client: - pass - """ - if code_callback is None: - def code_callback(): - return input('Please enter the code you received: ') - elif not callable(code_callback): - raise ValueError( - 'The code_callback parameter needs to be a callable ' - 'function that returns the code you received by Telegram.' - ) - - if not phone and not bot_token: - raise ValueError('No phone number or bot token provided.') - - if phone and bot_token and not callable(phone): - raise ValueError('Both a phone and a bot token provided, ' - 'must only provide one of either') - - coro = self._start( - phone=phone, - password=password, - bot_token=bot_token, - force_sms=force_sms, - code_callback=code_callback, - first_name=first_name, - last_name=last_name, - max_attempts=max_attempts - ) - return ( - coro if self.loop.is_running() - else self.loop.run_until_complete(coro) - ) - - async def _start( - self, phone, password, bot_token, force_sms, - code_callback, first_name, last_name, max_attempts): - if not self.is_connected(): - await self.connect() - - if await self.is_user_authorized(): - return self - - if not bot_token: - # Turn the callable into a valid phone number (or bot token) - while callable(phone): - value = phone() - if inspect.isawaitable(value): - value = await value - - if ':' in value: - # Bot tokens have 'user_id:access_hash' format - bot_token = value - break - - phone = utils.parse_phone(value) or phone - - if bot_token: - await self.sign_in(bot_token=bot_token) - return self - - me = None - attempts = 0 - two_step_detected = False - - sent_code = await self.send_code_request(phone, force_sms=force_sms) - sign_up = not sent_code.phone_registered - while attempts < max_attempts: - try: - value = code_callback() - if inspect.isawaitable(value): - value = await value - - # Since sign-in with no code works (it sends the code) - # we must double-check that here. Else we'll assume we - # logged in, and it will return None as the User. - if not value: - raise errors.PhoneCodeEmptyError(request=None) - - if sign_up: - me = await self.sign_up(value, first_name, last_name) - else: - # Raises SessionPasswordNeededError if 2FA enabled - me = await self.sign_in(phone, code=value) - break - except errors.SessionPasswordNeededError: - two_step_detected = True - break - except errors.PhoneNumberOccupiedError: - sign_up = False - except errors.PhoneNumberUnoccupiedError: - sign_up = True - except (errors.PhoneCodeEmptyError, - errors.PhoneCodeExpiredError, - errors.PhoneCodeHashEmptyError, - errors.PhoneCodeInvalidError): - print('Invalid code. Please try again.', file=sys.stderr) - - attempts += 1 - else: - raise RuntimeError( - '{} consecutive sign-in attempts failed. Aborting' - .format(max_attempts) - ) - - if two_step_detected: - if not password: - raise ValueError( - "Two-step verification is enabled for this account. " - "Please provide the 'password' argument to 'start()'." - ) - - if callable(password): - for _ in range(max_attempts): - try: - value = password() - if inspect.isawaitable(value): - value = await value - - me = await self.sign_in(phone=phone, password=value) - break - except errors.PasswordHashInvalidError: - print('Invalid password. Please try again', - file=sys.stderr) - else: - raise errors.PasswordHashInvalidError(request=None) - else: - me = await self.sign_in(phone=phone, password=password) - - # We won't reach here if any step failed (exit by exception) - signed, name = 'Signed in successfully as', utils.get_display_name(me) - try: - print(signed, name) - except UnicodeEncodeError: - # Some terminals don't support certain characters - print(signed, name.encode('utf-8', errors='ignore') - .decode('ascii', errors='ignore')) - - return self - - def _parse_phone_and_hash(self, phone, phone_hash): - """ - Helper method to both parse and validate phone and its hash. - """ - phone = utils.parse_phone(phone) or self._phone - if not phone: - raise ValueError( - 'Please make sure to call send_code_request first.' - ) - - phone_hash = phone_hash or self._phone_code_hash.get(phone, None) - if not phone_hash: - raise ValueError('You also need to provide a phone_code_hash.') - - return phone, phone_hash - - async def sign_in( - self: 'TelegramClient', - phone: str = None, - code: typing.Union[str, int] = None, - *, - password: str = None, - bot_token: str = None, - phone_code_hash: str = None) -> 'types.User': - """ - Logs in to Telegram to an existing user or bot account. - - You should only use this if you are not authorized yet. - - This method will send the code if it's not provided. - - .. note:: - - In most cases, you should simply use `start()` and not this method. - - Arguments - phone (`str` | `int`): - The phone to send the code to if no code was provided, - or to override the phone that was previously used with - these requests. - - code (`str` | `int`): - The code that Telegram sent. Note that if you have sent this - code through the application itself it will immediately - expire. If you want to send the code, obfuscate it somehow. - If you're not doing any of this you can ignore this note. - - password (`str`): - 2FA password, should be used if a previous call raised - ``SessionPasswordNeededError``. - - bot_token (`str`): - Used to sign in as a bot. Not all requests will be available. - This should be the hash the `@BotFather `_ - gave you. - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - ``None`` to use the last hash known for the phone to be used. - - Returns - The signed in user, or the information about - :meth:`send_code_request`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - client.sign_in(phone) # send code - - code = input('enter code: ') - client.sign_in(phone, code) - """ - me = await self.get_me() - if me: - return me - - if phone and not code and not password: - return await self.send_code_request(phone) - elif code: - phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) - - # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, - # PhoneCodeHashEmptyError or PhoneCodeInvalidError. - result = await self(functions.auth.SignInRequest( - phone, phone_code_hash, str(code))) - elif password: - pwd = await self(functions.account.GetPasswordRequest()) - result = await self(functions.auth.CheckPasswordRequest( - pwd_mod.compute_check(pwd, password) - )) - elif bot_token: - result = await self(functions.auth.ImportBotAuthorizationRequest( - flags=0, bot_auth_token=bot_token, - api_id=self.api_id, api_hash=self.api_hash - )) - else: - raise ValueError( - 'You must provide a phone and a code the first time, ' - 'and a password only if an RPCError was raised before.' - ) - - return self._on_login(result.user) - - async def sign_up( - self: 'TelegramClient', - code: typing.Union[str, int], - first_name: str, - last_name: str = '', - *, - phone: str = None, - phone_code_hash: str = None) -> 'types.User': - """ - Signs up to Telegram as a new user account. - - Use this if you don't have an account yet. - - You must call `send_code_request` first. - - **By using this method you're agreeing to Telegram's - Terms of Service. This is required and your account - will be banned otherwise.** See https://telegram.org/tos - and https://core.telegram.org/api/terms. - - Arguments - code (`str` | `int`): - The code sent by Telegram - - first_name (`str`): - The first name to be used by the new account. - - last_name (`str`, optional) - Optional last name. - - phone (`str` | `int`, optional): - The phone to sign up. This will be the last phone used by - default (you normally don't need to set this). - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - ``None`` to use the last hash known for the phone to be used. - - Returns - The new created :tl:`User`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - client.send_code_request(phone) - - code = input('enter code: ') - client.sign_up(code, first_name='Anna', last_name='Banana') - """ - me = await self.get_me() - if me: - return me - - if self._tos and self._tos.text: - if self.parse_mode: - t = self.parse_mode.unparse(self._tos.text, self._tos.entities) - else: - t = self._tos.text - sys.stderr.write("{}\n".format(t)) - sys.stderr.flush() - - phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) - - result = await self(functions.auth.SignUpRequest( - phone_number=phone, - phone_code_hash=phone_code_hash, - phone_code=str(code), - first_name=first_name, - last_name=last_name - )) - - if self._tos: - await self( - functions.help.AcceptTermsOfServiceRequest(self._tos.id)) - - return self._on_login(result.user) - - def _on_login(self, user): - """ - Callback called whenever the login or sign up process completes. - - Returns the input user parameter. - """ - self._bot = bool(user.bot) - self._self_input_peer = utils.get_input_peer(user, allow_self=False) - self._authorized = True - - return user - - async def send_code_request( - self: 'TelegramClient', - phone: str, - *, - force_sms: bool = False) -> 'types.auth.SentCode': - """ - Sends the Telegram code needed to login to the given phone number. - - Arguments - phone (`str` | `int`): - The phone to which the code will be sent. - - force_sms (`bool`, optional): - Whether to force sending as SMS. - - Returns - An instance of :tl:`SentCode`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - sent = client.send_code_request(phone) - print(sent) - - if sent.phone_registered: - print('This phone has an existing account registered') - else: - print('This phone does not have an account registered') - """ - result = None - phone = utils.parse_phone(phone) or self._phone - phone_hash = self._phone_code_hash.get(phone) - - if not phone_hash: - try: - result = await self(functions.auth.SendCodeRequest( - phone, self.api_id, self.api_hash, types.CodeSettings())) - except errors.AuthRestartError: - return await self.send_code_request(phone, force_sms=force_sms) - - self._tos = result.terms_of_service - self._phone_code_hash[phone] = phone_hash = result.phone_code_hash - else: - force_sms = True - - self._phone = phone - - if force_sms: - result = await self( - functions.auth.ResendCodeRequest(phone, phone_hash)) - - self._phone_code_hash[phone] = result.phone_code_hash - - return result - - async def log_out(self: 'TelegramClient') -> bool: - """ - Logs out Telegram and deletes the current ``*.session`` file. - - Returns - ``True`` if the operation was successful. - - Example - .. code-block:: python - - # Note: you will need to login again! - client.log_out() - """ - try: - await self(functions.auth.LogOutRequest()) - except errors.RPCError: - return False - - self._bot = None - self._self_input_peer = None - self._authorized = False - self._state_cache.reset() - - await self.disconnect() - self.session.delete() - return True - - async def edit_2fa( - self: 'TelegramClient', - current_password: str = None, - new_password: str = None, - *, - hint: str = '', - email: str = None, - email_code_callback: typing.Callable[[int], str] = None) -> bool: - """ - Changes the 2FA settings of the logged in user. - - Review carefully the parameter explanations before using this method. - - Note that this method may be *incredibly* slow depending on the - prime numbers that must be used during the process to make sure - that everything is safe. - - Has no effect if both current and new password are omitted. - - Arguments - current_password (`str`, optional): - The current password, to authorize changing to ``new_password``. - Must be set if changing existing 2FA settings. - Must **not** be set if 2FA is currently disabled. - Passing this by itself will remove 2FA (if correct). - - new_password (`str`, optional): - The password to set as 2FA. - If 2FA was already enabled, ``current_password`` **must** be set. - Leaving this blank or ``None`` will remove the password. - - hint (`str`, optional): - Hint to be displayed by Telegram when it asks for 2FA. - Leaving unspecified is highly discouraged. - Has no effect if ``new_password`` is not set. - - email (`str`, optional): - Recovery and verification email. If present, you must also - set `email_code_callback`, else it raises ``ValueError``. - - email_code_callback (`callable`, optional): - If an email is provided, a callback that returns the code sent - to it must also be set. This callback may be asynchronous. - It should return a string with the code. The length of the - code will be passed to the callback as an input parameter. - - If the callback returns an invalid code, it will raise - ``CodeInvalidError``. - - Returns - ``True`` if successful, ``False`` otherwise. - - Example - .. code-block:: python - - # Setting a password for your account which didn't have - client.edit_2fa(new_password='I_<3_Telethon') - - # Removing the password - client.edit_2fa(current_password='I_<3_Telethon') - """ - if new_password is None and current_password is None: - return False - - if email and not callable(email_code_callback): - raise ValueError('email present without email_code_callback') - - pwd = await self(functions.account.GetPasswordRequest()) - pwd.new_algo.salt1 += os.urandom(32) - assert isinstance(pwd, types.account.Password) - if not pwd.has_password and current_password: - current_password = None - - if current_password: - password = pwd_mod.compute_check(pwd, current_password) - else: - password = types.InputCheckPasswordEmpty() - - if new_password: - new_password_hash = pwd_mod.compute_digest( - pwd.new_algo, new_password) - else: - new_password_hash = b'' - - try: - await self(functions.account.UpdatePasswordSettingsRequest( - password=password, - new_settings=types.account.PasswordInputSettings( - new_algo=pwd.new_algo, - new_password_hash=new_password_hash, - hint=hint, - email=email, - new_secure_settings=None - ) - )) - except errors.EmailUnconfirmedError as e: - code = email_code_callback(e.code_length) - if inspect.isawaitable(code): - code = await code - - code = str(code) - await self(functions.account.ConfirmPasswordEmailRequest(code)) - - return True - - # endregion - - # region with blocks - - async def __aenter__(self): - return await self.start() - - async def __aexit__(self, *args): - await self.disconnect() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - # endregion diff --git a/telethon/client/bots.py b/telethon/client/bots.py index 72efdd9e..e69de29b 100644 --- a/telethon/client/bots.py +++ b/telethon/client/bots.py @@ -1,57 +0,0 @@ -import typing - -from .. import hints -from ..tl import types, functions, custom - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class BotMethods: - async def inline_query( - self: 'TelegramClient', - bot: 'hints.EntityLike', - query: str, - *, - offset: str = None, - geo_point: 'types.GeoPoint' = None) -> custom.InlineResults: - """ - Makes an inline query to the specified bot (e.g. ``@vote New Poll``). - - Arguments - bot (`entity`): - The bot entity to which the inline query should be made. - - query (`str`): - The query that should be made to the bot. - - offset (`str`, optional): - The string offset to use for the bot. - - geo_point (:tl:`GeoPoint`, optional) - The geo point location information to send to the bot - for localised results. Available under some bots. - - Returns - A list of `custom.InlineResult - `. - - Example - .. code-block:: python - - # Make an inline query to @like - results = client.inline_query('like', 'Do you like Telethon?') - - # Send the first result to some chat - message = results[0].click('TelethonOffTopic') - """ - bot = await self.get_input_entity(bot) - result = await self(functions.messages.GetInlineBotResultsRequest( - bot=bot, - peer=types.InputPeerEmpty(), - query=query, - offset=offset or '', - geo_point=geo_point - )) - - return custom.InlineResults(self, result) diff --git a/telethon/client/buttons.py b/telethon/client/buttons.py index aa6b7fa1..e69de29b 100644 --- a/telethon/client/buttons.py +++ b/telethon/client/buttons.py @@ -1,95 +0,0 @@ -import typing - -from .. import utils, hints -from ..tl import types, custom - - -class ButtonMethods: - @staticmethod - def build_reply_markup( - buttons: 'typing.Optional[hints.MarkupLike]', - inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]': - """ - Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for - the given buttons. - - Does nothing if either no buttons are provided or the provided - argument is already a reply markup. - - You should consider using this method if you are going to reuse - the markup very often. Otherwise, it is not necessary. - - This method is **not** asynchronous (don't use ``await`` on it). - - Arguments - buttons (`hints.MarkupLike`): - The button, list of buttons, array of buttons or markup - to convert into a markup. - - inline_only (`bool`, optional): - Whether the buttons **must** be inline buttons only or not. - - Example - .. code-block:: python - - from telethon import Button - - markup = client.build_reply_markup(Button.inline('hi')) - client.send_message('click me', buttons=markup) - """ - if buttons is None: - return None - - try: - if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: - return buttons # crc32(b'ReplyMarkup'): - except AttributeError: - pass - - if not utils.is_list_like(buttons): - buttons = [[buttons]] - elif not utils.is_list_like(buttons[0]): - buttons = [buttons] - - is_inline = False - is_normal = False - resize = None - single_use = None - selective = None - - rows = [] - for row in buttons: - current = [] - for button in row: - if isinstance(button, custom.Button): - if button.resize is not None: - resize = button.resize - if button.single_use is not None: - single_use = button.single_use - if button.selective is not None: - selective = button.selective - - button = button.button - elif isinstance(button, custom.MessageButton): - button = button.button - - inline = custom.Button._is_inline(button) - is_inline |= inline - is_normal |= not inline - - if button.SUBCLASS_OF_ID == 0xbad74a3: - # 0xbad74a3 == crc32(b'KeyboardButton') - current.append(button) - - if current: - rows.append(types.KeyboardButtonRow(current)) - - if inline_only and is_normal: - raise ValueError('You cannot use non-inline buttons here') - elif is_inline == is_normal and is_normal: - raise ValueError('You cannot mix inline with normal buttons') - elif is_inline: - return types.ReplyInlineMarkup(rows) - # elif is_normal: - return types.ReplyKeyboardMarkup( - rows, resize=resize, single_use=single_use, selective=selective) diff --git a/telethon/client/chats.py b/telethon/client/chats.py index 7f5f804c..e69de29b 100644 --- a/telethon/client/chats.py +++ b/telethon/client/chats.py @@ -1,764 +0,0 @@ -import asyncio -import itertools -import string -import typing - -from .. import helpers, utils, hints -from ..requestiter import RequestIter -from ..tl import types, functions, custom - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - -_MAX_PARTICIPANTS_CHUNK_SIZE = 200 -_MAX_ADMIN_LOG_CHUNK_SIZE = 100 -_MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 - - -class _ChatAction: - _str_mapping = { - 'typing': types.SendMessageTypingAction(), - 'contact': types.SendMessageChooseContactAction(), - 'game': types.SendMessageGamePlayAction(), - 'location': types.SendMessageGeoLocationAction(), - - 'record-audio': types.SendMessageRecordAudioAction(), - 'record-voice': types.SendMessageRecordAudioAction(), # alias - 'record-round': types.SendMessageRecordRoundAction(), - 'record-video': types.SendMessageRecordVideoAction(), - - 'audio': types.SendMessageUploadAudioAction(1), - 'voice': types.SendMessageUploadAudioAction(1), # alias - 'round': types.SendMessageUploadRoundAction(1), - 'video': types.SendMessageUploadVideoAction(1), - - 'photo': types.SendMessageUploadPhotoAction(1), - 'document': types.SendMessageUploadDocumentAction(1), - 'file': types.SendMessageUploadDocumentAction(1), # alias - 'song': types.SendMessageUploadDocumentAction(1), # alias - - 'cancel': types.SendMessageCancelAction() - } - - def __init__(self, client, chat, action, *, delay, auto_cancel): - self._client = client - self._chat = chat - self._action = action - self._delay = delay - self._auto_cancel = auto_cancel - self._request = None - self._task = None - self._running = False - - async def __aenter__(self): - self._chat = await self._client.get_input_entity(self._chat) - - # Since `self._action` is passed by reference we can avoid - # recreating the request all the time and still modify - # `self._action.progress` directly in `progress`. - self._request = functions.messages.SetTypingRequest( - self._chat, self._action) - - self._running = True - self._task = self._client.loop.create_task(self._update()) - - async def __aexit__(self, *args): - self._running = False - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - - self._task = None - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - async def _update(self): - try: - while self._running: - await self._client(self._request) - await asyncio.sleep(self._delay) - except ConnectionError: - pass - except asyncio.CancelledError: - if self._auto_cancel: - await self._client(functions.messages.SetTypingRequest( - self._chat, types.SendMessageCancelAction())) - - def progress(self, current, total): - if hasattr(self._action, 'progress'): - self._action.progress = 100 * round(current / total) - - -class _ParticipantsIter(RequestIter): - async def _init(self, entity, filter, search, aggressive): - if isinstance(filter, type): - if filter in (types.ChannelParticipantsBanned, - types.ChannelParticipantsKicked, - types.ChannelParticipantsSearch, - types.ChannelParticipantsContacts): - # These require a `q` parameter (support types for convenience) - filter = filter('') - else: - filter = filter() - - entity = await self.client.get_input_entity(entity) - if search and (filter - or not isinstance(entity, types.InputPeerChannel)): - # We need to 'search' ourselves unless we have a PeerChannel - search = search.casefold() - - self.filter_entity = lambda ent: ( - search in utils.get_display_name(ent).casefold() or - search in (getattr(ent, 'username', None) or '').casefold() - ) - else: - self.filter_entity = lambda ent: True - - # Only used for channels, but we should always set the attribute - self.requests = [] - - if isinstance(entity, types.InputPeerChannel): - self.total = (await self.client( - functions.channels.GetFullChannelRequest(entity) - )).full_chat.participants_count - - if self.limit <= 0: - raise StopAsyncIteration - - self.seen = set() - if aggressive and not filter: - self.requests.extend(functions.channels.GetParticipantsRequest( - channel=entity, - filter=types.ChannelParticipantsSearch(x), - offset=0, - limit=_MAX_PARTICIPANTS_CHUNK_SIZE, - hash=0 - ) for x in (search or string.ascii_lowercase)) - else: - self.requests.append(functions.channels.GetParticipantsRequest( - channel=entity, - filter=filter or types.ChannelParticipantsSearch(search), - offset=0, - limit=_MAX_PARTICIPANTS_CHUNK_SIZE, - hash=0 - )) - - elif isinstance(entity, types.InputPeerChat): - full = await self.client( - functions.messages.GetFullChatRequest(entity.chat_id)) - if not isinstance( - full.full_chat.participants, types.ChatParticipants): - # ChatParticipantsForbidden won't have ``.participants`` - self.total = 0 - raise StopAsyncIteration - - self.total = len(full.full_chat.participants.participants) - - users = {user.id: user for user in full.users} - for participant in full.full_chat.participants.participants: - user = users[participant.user_id] - if not self.filter_entity(user): - continue - - user = users[participant.user_id] - user.participant = participant - self.buffer.append(user) - - return True - else: - self.total = 1 - if self.limit != 0: - user = await self.client.get_entity(entity) - if self.filter_entity(user): - user.participant = None - self.buffer.append(user) - - return True - - async def _load_next_chunk(self): - if not self.requests: - return True - - # Only care about the limit for the first request - # (small amount of people, won't be aggressive). - # - # Most people won't care about getting exactly 12,345 - # members so it doesn't really matter not to be 100% - # precise with being out of the offset/limit here. - self.requests[0].limit = min( - self.limit - self.requests[0].offset, _MAX_PARTICIPANTS_CHUNK_SIZE) - - if self.requests[0].offset > self.limit: - return True - - results = await self.client(self.requests) - for i in reversed(range(len(self.requests))): - participants = results[i] - if not participants.users: - self.requests.pop(i) - continue - - self.requests[i].offset += len(participants.participants) - users = {user.id: user for user in participants.users} - for participant in participants.participants: - user = users[participant.user_id] - if not self.filter_entity(user) or user.id in self.seen: - continue - - self.seen.add(participant.user_id) - user = users[participant.user_id] - user.participant = participant - self.buffer.append(user) - - -class _AdminLogIter(RequestIter): - async def _init( - self, entity, admins, search, min_id, max_id, - join, leave, invite, restrict, unrestrict, ban, unban, - promote, demote, info, settings, pinned, edit, delete - ): - if any((join, leave, invite, restrict, unrestrict, ban, unban, - promote, demote, info, settings, pinned, edit, delete)): - events_filter = types.ChannelAdminLogEventsFilter( - join=join, leave=leave, invite=invite, ban=restrict, - unban=unrestrict, kick=ban, unkick=unban, promote=promote, - demote=demote, info=info, settings=settings, pinned=pinned, - edit=edit, delete=delete - ) - else: - events_filter = None - - self.entity = await self.client.get_input_entity(entity) - - admin_list = [] - if admins: - if not utils.is_list_like(admins): - admins = (admins,) - - for admin in admins: - admin_list.append(await self.client.get_input_entity(admin)) - - self.request = functions.channels.GetAdminLogRequest( - self.entity, q=search or '', min_id=min_id, max_id=max_id, - limit=0, events_filter=events_filter, admins=admin_list or None - ) - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE) - r = await self.client(self.request) - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - self.request.max_id = min((e.id for e in r.events), default=0) - for ev in r.events: - if isinstance(ev.action, - types.ChannelAdminLogEventActionEditMessage): - ev.action.prev_message._finish_init( - self.client, entities, self.entity) - - ev.action.new_message._finish_init( - self.client, entities, self.entity) - - elif isinstance(ev.action, - types.ChannelAdminLogEventActionDeleteMessage): - ev.action.message._finish_init( - self.client, entities, self.entity) - - self.buffer.append(custom.AdminLogEvent(ev, entities)) - - if len(r.events) < self.request.limit: - return True - - -class _ProfilePhotoIter(RequestIter): - async def _init( - self, entity, offset, max_id - ): - entity = await self.client.get_input_entity(entity) - if isinstance(entity, (types.InputPeerUser, types.InputPeerSelf)): - self.request = functions.photos.GetUserPhotosRequest( - entity, - offset=offset, - max_id=max_id, - limit=1 - ) - else: - self.request = functions.messages.SearchRequest( - peer=entity, - q='', - filter=types.InputMessagesFilterChatPhotos(), - min_date=None, - max_date=None, - offset_id=0, - add_offset=offset, - limit=1, - max_id=max_id, - min_id=0, - hash=0 - ) - - if self.limit == 0: - self.request.limit = 1 - result = await self.client(self.request) - if isinstance(result, types.photos.Photos): - self.total = len(result.photos) - elif isinstance(result, types.messages.Messages): - self.total = len(result.messages) - else: - # Luckily both photosSlice and messages have a count for total - self.total = getattr(result, 'count', None) - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE) - result = await self.client(self.request) - - if isinstance(result, types.photos.Photos): - self.buffer = result.photos - self.left = len(self.buffer) - self.total = len(self.buffer) - elif isinstance(result, types.messages.Messages): - self.buffer = [x.action.photo for x in result.messages - if isinstance(x.action, types.MessageActionChatEditPhoto)] - - self.left = len(self.buffer) - self.total = len(self.buffer) - elif isinstance(result, types.photos.PhotosSlice): - self.buffer = result.photos - self.total = result.count - if len(self.buffer) < self.request.limit: - self.left = len(self.buffer) - else: - self.request.offset += len(result.photos) - else: - self.buffer = [x.action.photo for x in result.messages - if isinstance(x.action, types.MessageActionChatEditPhoto)] - self.total = getattr(result, 'count', None) - if len(result.messages) < self.request.limit: - self.left = len(self.buffer) - elif result.messages: - self.request.add_offset = 0 - self.request.offset_id = result.messages[-1].id - - -class ChatMethods: - - # region Public methods - - def iter_participants( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - search: str = '', - filter: 'types.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> _ParticipantsIter: - """ - Iterator over the participants belonging to the specified chat. - - Arguments - entity (`entity`): - The entity from which to retrieve the participants list. - - limit (`int`): - Limits amount of participants fetched. - - search (`str`, optional): - Look for participants with this string in name/username. - - If ``aggressive is True``, the symbols from this string will - be used. - - filter (:tl:`ChannelParticipantsFilter`, optional): - The filter to be used, if you want e.g. only admins - Note that you might not have permissions for some filter. - This has no effect for normal chats or users. - - .. note:: - - The filter :tl:`ChannelParticipantsBanned` will return - *restricted* users. If you want *banned* users you should - use :tl:`ChannelParticipantsKicked` instead. - - aggressive (`bool`, optional): - Aggressively looks for all participants in the chat. - - This is useful for channels since 20 July 2018, - Telegram added a server-side limit where only the - first 200 members can be retrieved. With this flag - set, more than 200 will be often be retrieved. - - This has no effect if a ``filter`` is given. - - Yields - The :tl:`User` objects returned by :tl:`GetParticipantsRequest` - with an additional ``.participant`` attribute which is the - matched :tl:`ChannelParticipant` type for channels/megagroups - or :tl:`ChatParticipants` for normal chats. - - Example - .. code-block:: python - - # Show all user IDs in a chat - for user in client.iter_participants(chat): - print(user.id) - - # Search by name - for user in client.iter_participants(chat, search='name'): - print(user.username) - - # Filter by admins - from telethon.tl.types import ChannelParticipantsAdmins - for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): - print(user.first_name) - """ - return _ParticipantsIter( - self, - limit, - entity=entity, - filter=filter, - search=search, - aggressive=aggressive - ) - - async def get_participants( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_participants()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - users = client.get_participants(chat) - print(users[0].first_name) - - for user in users: - if user.username is not None: - print(user.username) - """ - return await self.iter_participants(*args, **kwargs).collect() - - def iter_admin_log( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - max_id: int = 0, - min_id: int = 0, - search: str = None, - admins: 'hints.EntitiesLike' = None, - join: bool = None, - leave: bool = None, - invite: bool = None, - restrict: bool = None, - unrestrict: bool = None, - ban: bool = None, - unban: bool = None, - promote: bool = None, - demote: bool = None, - info: bool = None, - settings: bool = None, - pinned: bool = None, - edit: bool = None, - delete: bool = None) -> _AdminLogIter: - """ - Iterator over the admin log for the specified channel. - - Note that you must be an administrator of it to use this method. - - If none of the filters are present (i.e. they all are ``None``), - *all* event types will be returned. If at least one of them is - ``True``, only those that are true will be returned. - - Arguments - entity (`entity`): - The channel entity from which to get its admin log. - - limit (`int` | `None`, optional): - Number of events to be retrieved. - - The limit may also be ``None``, which would eventually return - the whole history. - - max_id (`int`): - All the events with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the events with a lower (older) ID or equal to this will - be excluded. - - search (`str`): - The string to be used as a search query. - - admins (`entity` | `list`): - If present, the events will be filtered by these admins - (or single admin) and only those caused by them will be - returned. - - join (`bool`): - If ``True``, events for when a user joined will be returned. - - leave (`bool`): - If ``True``, events for when a user leaves will be returned. - - invite (`bool`): - If ``True``, events for when a user joins through an invite - link will be returned. - - restrict (`bool`): - If ``True``, events with partial restrictions will be - returned. This is what the API calls "ban". - - unrestrict (`bool`): - If ``True``, events removing restrictions will be returned. - This is what the API calls "unban". - - ban (`bool`): - If ``True``, events applying or removing all restrictions will - be returned. This is what the API calls "kick" (restricting - all permissions removed is a ban, which kicks the user). - - unban (`bool`): - If ``True``, events removing all restrictions will be - returned. This is what the API calls "unkick". - - promote (`bool`): - If ``True``, events with admin promotions will be returned. - - demote (`bool`): - If ``True``, events with admin demotions will be returned. - - info (`bool`): - If ``True``, events changing the group info will be returned. - - settings (`bool`): - If ``True``, events changing the group settings will be - returned. - - pinned (`bool`): - If ``True``, events of new pinned messages will be returned. - - edit (`bool`): - If ``True``, events of message edits will be returned. - - delete (`bool`): - If ``True``, events of message deletions will be returned. - - Yields - Instances of `AdminLogEvent `. - - Example - .. code-block:: python - - for event in client.iter_admin_log(channel): - if event.changed_title: - print('The title changed from', event.old, 'to', event.new) - """ - return _AdminLogIter( - self, - limit, - entity=entity, - admins=admins, - search=search, - min_id=min_id, - max_id=max_id, - join=join, - leave=leave, - invite=invite, - restrict=restrict, - unrestrict=unrestrict, - ban=ban, - unban=unban, - promote=promote, - demote=demote, - info=info, - settings=settings, - pinned=pinned, - edit=edit, - delete=delete - ) - - async def get_admin_log( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_admin_log()`, but returns a ``list`` instead. - - Example - .. code-block:: python - - # Get a list of deleted message events which said "heck" - events = client.get_admin_log(channel, search='heck', delete=True) - - # Print the old message before it was deleted - print(events[0].old) - """ - return await self.iter_admin_log(*args, **kwargs).collect() - - def iter_profile_photos( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: int = None, - *, - offset: int = 0, - max_id: int = 0) -> _ProfilePhotoIter: - """ - Iterator over a user's profile photos or a chat's photos. - - Arguments - entity (`entity`): - The entity from which to get the profile or chat photos. - - limit (`int` | `None`, optional): - Number of photos to be retrieved. - - The limit may also be ``None``, which would eventually all - the photos that are still available. - - offset (`int`): - How many photos should be skipped before returning the first one. - - max_id (`int`): - The maximum ID allowed when fetching photos. - - Yields - Instances of :tl:`Photo`. - - Example - .. code-block:: python - - # Download all the profile photos of some user - for photo in client.iter_profile_photos(user): - client.download_media(photo) - """ - return _ProfilePhotoIter( - self, - limit, - entity=entity, - offset=offset, - max_id=max_id - ) - - async def get_profile_photos( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_profile_photos()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get the photos of a channel - photos = client.get_profile_photos(channel) - - # Download the oldest photo - client.download_media(photos[-1]) - """ - return await self.iter_profile_photos(*args, **kwargs).collect() - - def action( - self: 'TelegramClient', - entity: 'hints.EntityLike', - action: 'typing.Union[str, types.TypeSendMessageAction]', - *, - delay: float = 4, - auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': - """ - Returns a context-manager object to represent a "chat action". - - Chat actions indicate things like "user is typing", "user is - uploading a photo", etc. - - If the action is ``'cancel'``, you should just ``await`` the result, - since it makes no sense to use a context-manager for it. - - See the example below for intended usage. - - Arguments - entity (`entity`): - The entity where the action should be showed in. - - action (`str` | :tl:`SendMessageAction`): - The action to show. You can either pass a instance of - :tl:`SendMessageAction` or better, a string used while: - - * ``'typing'``: typing a text message. - * ``'contact'``: choosing a contact. - * ``'game'``: playing a game. - * ``'location'``: choosing a geo location. - * ``'record-audio'``: recording a voice note. - You may use ``'record-voice'`` as alias. - * ``'record-round'``: recording a round video. - * ``'record-video'``: recording a normal video. - * ``'audio'``: sending an audio file (voice note or song). - You may use ``'voice'`` and ``'song'`` as aliases. - * ``'round'``: uploading a round video. - * ``'video'``: uploading a video file. - * ``'photo'``: uploading a photo. - * ``'document'``: uploading a document file. - You may use ``'file'`` as alias. - * ``'cancel'``: cancel any pending action in this chat. - - Invalid strings will raise a ``ValueError``. - - delay (`int` | `float`): - The delay, in seconds, to wait between sending actions. - For example, if the delay is 5 and it takes 7 seconds to - do something, three requests will be made at 0s, 5s, and - 7s to cancel the action. - - auto_cancel (`bool`): - Whether the action should be cancelled once the context - manager exists or not. The default is ``True``, since - you don't want progress to be shown when it has already - completed. - - Returns - Either a context-manager object or a coroutine. - - Example - .. code-block:: python - - # Type for 2 seconds, then send a message - async with client.action(chat, 'typing'): - await asyncio.sleep(2) - await client.send_message(chat, 'Hello world! I type slow ^^') - - # Cancel any previous action - await client.action(chat, 'cancel') - - # Upload a document, showing its progress (most clients ignore this) - async with client.action(chat, 'document') as action: - client.send_file(chat, zip_file, progress_callback=action.progress) - """ - if isinstance(action, str): - try: - action = _ChatAction._str_mapping[action.lower()] - except KeyError: - raise ValueError('No such action "{}"'.format(action)) from None - elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: - # 0x20b2cc21 = crc32(b'SendMessageAction') - if isinstance(action, type): - raise ValueError('You must pass an instance, not the class') - else: - raise ValueError('Cannot use {} as action'.format(action)) - - if isinstance(action, types.SendMessageCancelAction): - # ``SetTypingRequest.resolve`` will get input peer of ``entity``. - return self(functions.messages.SetTypingRequest( - entity, types.SendMessageCancelAction())) - - return _ChatAction( - self, entity, action, delay=delay, auto_cancel=auto_cancel) - - # endregion diff --git a/telethon/client/dialogs.py b/telethon/client/dialogs.py index c388ac52..e69de29b 100644 --- a/telethon/client/dialogs.py +++ b/telethon/client/dialogs.py @@ -1,507 +0,0 @@ -import asyncio -import itertools -import typing - -from .. import utils, hints -from ..requestiter import RequestIter -from ..tl import types, functions, custom - -_MAX_CHUNK_SIZE = 100 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class _DialogsIter(RequestIter): - async def _init( - self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder - ): - self.request = functions.messages.GetDialogsRequest( - offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - limit=1, - hash=0, - exclude_pinned=ignore_pinned, - folder_id=folder - ) - - if self.limit <= 0: - # Special case, get a single dialog and determine count - dialogs = await self.client(self.request) - self.total = getattr(dialogs, 'count', len(dialogs.dialogs)) - raise StopAsyncIteration - - self.seen = set() - self.offset_date = offset_date - self.ignore_migrated = ignore_migrated - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) - r = await self.client(self.request) - - self.total = getattr(r, 'count', len(r.dialogs)) - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - messages = {} - for m in r.messages: - m._finish_init(self.client, entities, None) - messages[m.id] = m - - for d in r.dialogs: - # We check the offset date here because Telegram may ignore it - if self.offset_date: - date = getattr(messages.get( - d.top_message, None), 'date', None) - - if not date or date.timestamp() > self.offset_date.timestamp(): - continue - - peer_id = utils.get_peer_id(d.peer) - if peer_id not in self.seen: - self.seen.add(peer_id) - cd = custom.Dialog(self.client, d, entities, messages) - if cd.dialog.pts: - self.client._channel_pts[cd.id] = cd.dialog.pts - - if not self.ignore_migrated or getattr( - cd.entity, 'migrated_to', None) is None: - self.buffer.append(cd) - - if len(r.dialogs) < self.request.limit\ - or not isinstance(r, types.messages.DialogsSlice): - # Less than we requested means we reached the end, or - # we didn't get a DialogsSlice which means we got all. - return True - - # We can't use `messages[-1]` as the offset ID / date. - # Why? Because pinned dialogs will mess with the order - # in this list. Instead, we find the last dialog which - # has a message, and use it as an offset. - last_message = next(( - messages[d.top_message] - for d in reversed(r.dialogs) - if d.top_message in messages - ), None) - - self.request.exclude_pinned = True - self.request.offset_id = last_message.id if last_message else 0 - self.request.offset_date = last_message.date if last_message else None - self.request.offset_peer =\ - entities[utils.get_peer_id(r.dialogs[-1].peer)] - - -class _DraftsIter(RequestIter): - async def _init(self, **kwargs): - r = await self.client(functions.messages.GetAllDraftsRequest()) - self.buffer.extend(custom.Draft._from_update(self.client, u) - for u in r.updates) - - async def _load_next_chunk(self): - return [] - - -class DialogMethods: - - # region Public methods - - def iter_dialogs( - self: 'TelegramClient', - limit: float = None, - *, - offset_date: 'hints.DateLike' = None, - offset_id: int = 0, - offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), - ignore_pinned: bool = False, - ignore_migrated: bool = False, - folder: int = None, - archived: bool = None - ) -> _DialogsIter: - """ - Iterator over the dialogs (open conversations/subscribed channels). - - Arguments - limit (`int` | `None`): - How many dialogs to be retrieved as maximum. Can be set to - ``None`` to retrieve all dialogs. Note that this may take - whole minutes if you have hundreds of dialogs, as Telegram - will tell the library to slow down through a - ``FloodWaitError``. - - offset_date (`datetime`, optional): - The offset date to be used. - - offset_id (`int`, optional): - The message ID to be used as an offset. - - offset_peer (:tl:`InputPeer`, optional): - The peer to be used as an offset. - - ignore_pinned (`bool`, optional): - Whether pinned dialogs should be ignored or not. - When set to ``True``, these won't be yielded at all. - - ignore_migrated (`bool`, optional): - Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` - should be included or not. By default all the chats in your - dialogs are returned, but setting this to ``True`` will ignore - (i.e. skip) them in the same way official applications do. - - folder (`int`, optional): - The folder from which the dialogs should be retrieved. - - If left unspecified, all dialogs (including those from - folders) will be returned. - - If set to ``0``, all dialogs that don't belong to any - folder will be returned. - - If set to a folder number like ``1``, only those from - said folder will be returned. - - By default Telegram assigns the folder ID ``1`` to - archived chats, so you should use that if you need - to fetch the archived dialogs. - - archived (`bool`, optional): - Alias for `folder`. If unspecified, all will be returned, - ``False`` implies ``folder=0`` and ``True`` implies ``folder=1``. - Yields - Instances of `Dialog `. - - Example - .. code-block:: python - - # Print all dialog IDs and the title, nicely formatted - for dialog in client.iter_dialogs(): - print('{:>14}: {}'.format(dialog.id, dialog.title)) - """ - if archived is not None: - folder = 1 if archived else 0 - - return _DialogsIter( - self, - limit, - offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - ignore_pinned=ignore_pinned, - ignore_migrated=ignore_migrated, - folder=folder - ) - - async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_dialogs()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get all open conversation, print the title of the first - dialogs = client.get_dialogs() - first = dialogs[0] - print(first.title) - - # Use the dialog somewhere else - client.send_message(first, 'hi') - - # Getting only non-archived dialogs (both equivalent) - non_archived = client.get_dialogs(folder=0) - non_archived = client.get_dialogs(archived=False) - - # Getting only archived dialogs (both equivalent) - archived = client.get_dialogs(folder=1) - non_archived = client.get_dialogs(archived=True) - """ - return await self.iter_dialogs(*args, **kwargs).collect() - - def iter_drafts(self: 'TelegramClient') -> _DraftsIter: - """ - Iterator over all open draft messages. - - Yields - Instances of `Draft `. - - Example - .. code-block:: python - - # Clear all drafts - for draft in client.get_drafts(): - draft.delete() - """ - # TODO Passing a limit here makes no sense - return _DraftsIter(self, None) - - async def get_drafts(self: 'TelegramClient') -> 'hints.TotalList': - """ - Same as `iter_drafts()`, but returns a list instead. - - Example - .. code-block:: python - - # Get drafts, print the text of the first - drafts = client.get_drafts() - print(drafts[0].text) - """ - return await self.iter_drafts().collect() - - async def edit_folder( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None, - folder: typing.Union[int, typing.Sequence[int]] = None, - *, - unpack=None - ) -> types.Updates: - """ - Edits the folder used by one or more dialogs to archive them. - - Arguments - entity (entities): - The entity or list of entities to move to the desired - archive folder. - - folder (`int`): - The folder to which the dialog should be archived to. - - If you want to "archive" a dialog, use ``folder=1``. - - If you want to "un-archive" it, use ``folder=0``. - - You may also pass a list with the same length as - `entities` if you want to control where each entity - will go. - - unpack (`int`, optional): - If you want to unpack an archived folder, set this - parameter to the folder number that you want to - delete. - - When you unpack a folder, all the dialogs inside are - moved to the folder number 0. - - You can only use this parameter if the other two - are not set. - - Returns - The :tl:`Updates` object that the request produces. - - Example - .. code-block:: python - - # Archiving the first 5 dialogs - dialogs = client.get_dialogs(5) - client.edit_folder(dialogs, 1) - - # Un-archiving the third dialog (archiving to folder 0) - client.edit_folder(dialog[2], 0) - - # Moving the first dialog to folder 0 and the second to 1 - dialogs = client.get_dialogs(2) - client.edit_folder(dialogs, [0, 1]) - - # Un-archiving all dialogs - client.archive(unpack=1) - """ - if (entity is None) == (unpack is None): - raise ValueError('You can only set either entities or unpack, not both') - - if unpack is not None: - return await self(functions.folders.DeleteFolderRequest( - folder_id=unpack - )) - - if not utils.is_list_like(entity): - entities = [await self.get_input_entity(entity)] - else: - entities = await asyncio.gather( - *(self.get_input_entity(x) for x in entity), loop=self.loop) - - if folder is None: - raise ValueError('You must specify a folder') - elif not utils.is_list_like(folder): - folder = [folder] * len(entities) - elif len(entities) != len(folder): - raise ValueError('Number of folders does not match number of entities') - - return await self(functions.folders.EditPeerFoldersRequest([ - types.InputFolderPeer(x, folder_id=y) - for x, y in zip(entities, folder) - ])) - - async def delete_dialog( - self: 'TelegramClient', - entity: 'hints.EntityLike', - *, - revoke: bool = False - ): - """ - Deletes a dialog (leaves a chat or channel). - - See also `Dialog.delete() `. - - Arguments - entity (entities): - The entity of the dialog to delete. If it's a chat or - channel, you will leave it. Note that the chat itself - is not deleted, only the dialog, because you left it. - - revoke (`bool`, optional): - On private chats, you may revoke the messages from - the other peer too. By default, it's ``False``. Set - it to ``True`` to delete the history for both. - - Returns - The :tl:`Updates` object that the request produces, - or nothing for private conversations. - - Example - .. code-block:: python - - # Deleting the first dialog - dialogs = client.get_dialogs(5) - client.delete_dialog(dialogs[0]) - - # Leaving a channel by username - client.delete_dialog('username') - """ - entity = await self.get_input_entity(entity) - if isinstance(entity, types.InputPeerChannel): - return await self(functions.channels.LeaveChannelRequest(entity)) - - if isinstance(entity, types.InputPeerChat): - result = await self(functions.messages.DeleteChatUserRequest( - entity.chat_id, types.InputUserSelf())) - else: - result = None - - await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke)) - return result - - def conversation( - self: 'TelegramClient', - entity: 'hints.EntityLike', - *, - timeout: float = 60, - total_timeout: float = None, - max_messages: int = 100, - exclusive: bool = True, - replies_are_responses: bool = True) -> custom.Conversation: - """ - Creates a `Conversation ` - with the given entity. - - This is not the same as just sending a message to create a "dialog" - with them, but rather a way to easily send messages and await for - responses or other reactions. Refer to its documentation for more. - - Arguments - entity (`entity`): - The entity with which a new conversation should be opened. - - timeout (`int` | `float`, optional): - The default timeout (in seconds) *per action* to be used. You - may also override this timeout on a per-method basis. By - default each action can take up to 60 seconds (the value of - this timeout). - - total_timeout (`int` | `float`, optional): - The total timeout (in seconds) to use for the whole - conversation. This takes priority over per-action - timeouts. After these many seconds pass, subsequent - actions will result in ``asyncio.TimeoutError``. - - max_messages (`int`, optional): - The maximum amount of messages this conversation will - remember. After these many messages arrive in the - specified chat, subsequent actions will result in - ``ValueError``. - - exclusive (`bool`, optional): - By default, conversations are exclusive within a single - chat. That means that while a conversation is open in a - chat, you can't open another one in the same chat, unless - you disable this flag. - - If you try opening an exclusive conversation for - a chat where it's already open, it will raise - ``AlreadyInConversationError``. - - replies_are_responses (`bool`, optional): - Whether replies should be treated as responses or not. - - If the setting is enabled, calls to `conv.get_response - ` - and a subsequent call to `conv.get_reply - ` - will return different messages, otherwise they may return - the same message. - - Consider the following scenario with one outgoing message, - 1, and two incoming messages, the second one replying:: - - Hello! <1 - 2> (reply to 1) Hi! - 3> (reply to 1) How are you? - - And the following code: - - .. code-block:: python - - async with client.conversation(chat) as conv: - msg1 = await conv.send_message('Hello!') - msg2 = await conv.get_response() - msg3 = await conv.get_reply() - - With the setting enabled, ``msg2`` will be ``'Hi!'`` and - ``msg3`` be ``'How are you?'`` since replies are also - responses, and a response was already returned. - - With the setting disabled, both ``msg2`` and ``msg3`` will - be ``'Hi!'`` since one is a response and also a reply. - - Returns - A `Conversation `. - - Example - .. code-block:: python - - # denotes outgoing messages you sent - # denotes incoming response messages - with bot.conversation(chat) as conv: - # Hi! - conv.send_message('Hi!') - - # Hello! - hello = conv.get_response() - - # Please tell me your name - conv.send_message('Please tell me your name') - - # ? - name = conv.get_response().raw_text - - while not any(x.isalpha() for x in name): - # Your name didn't have any letters! Try again - conv.send_message("Your name didn't have any letters! Try again") - - # Lonami - name = conv.get_response().raw_text - - # Thanks Lonami! - conv.send_message('Thanks {}!'.format(name)) - """ - return custom.Conversation( - self, - entity, - timeout=timeout, - total_timeout=total_timeout, - max_messages=max_messages, - exclusive=exclusive, - replies_are_responses=replies_are_responses - - ) - - # endregion diff --git a/telethon/client/downloads.py b/telethon/client/downloads.py index 10e22781..e69de29b 100644 --- a/telethon/client/downloads.py +++ b/telethon/client/downloads.py @@ -1,862 +0,0 @@ -import datetime -import io -import os -import pathlib -import typing - -from .. import utils, helpers, errors, hints -from ..requestiter import RequestIter -from ..tl import TLObject, types, functions - -try: - import aiohttp -except ImportError: - aiohttp = None - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -# Chunk sizes for upload.getFile must be multiples of the smallest size -MIN_CHUNK_SIZE = 4096 -MAX_CHUNK_SIZE = 512 * 1024 - - -class _DirectDownloadIter(RequestIter): - async def _init( - self, file, dc_id, offset, stride, chunk_size, request_size, file_size - ): - self.request = functions.upload.GetFileRequest( - file, offset=offset, limit=request_size) - - self.total = file_size - self._stride = stride - self._chunk_size = chunk_size - self._last_part = None - - self._exported = dc_id and self.client.session.dc_id != dc_id - if not self._exported: - # The used sender will also change if ``FileMigrateError`` occurs - self._sender = self.client._sender - else: - try: - self._sender = await self.client._borrow_exported_sender(dc_id) - except errors.DcIdInvalidError: - # Can't export a sender for the ID we are currently in - config = await self.client(functions.help.GetConfigRequest()) - for option in config.dc_options: - if option.ip_address == self.client.session.server_address: - self.client.session.set_dc( - option.id, option.ip_address, option.port) - self.client.session.save() - break - - # TODO Figure out why the session may have the wrong DC ID - self._sender = self.client._sender - self._exported = False - - async def _load_next_chunk(self): - cur = await self._request() - self.buffer.append(cur) - if len(cur) < self.request.limit: - self.left = len(self.buffer) - await self.close() - else: - self.request.offset += self._stride - - async def _request(self): - try: - result = await self._sender.send(self.request) - if isinstance(result, types.upload.FileCdnRedirect): - raise NotImplementedError # TODO Implement - else: - return result.bytes - - except errors.FileMigrateError as e: - self.client._log[__name__].info('File lives in another DC') - self._sender = await self.client._borrow_exported_sender(e.new_dc) - self._exported = True - return await self._request() - - async def close(self): - if not self._sender: - return - - try: - if self._exported: - await self.client._return_exported_sender(self._sender) - elif self._sender != self.client._sender: - await self._sender.disconnect() - finally: - self._sender = None - - async def __aenter__(self): - pass - - async def __aexit__(self, *args): - await self.close() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - -class _GenericDownloadIter(_DirectDownloadIter): - async def _load_next_chunk(self, mask=MIN_CHUNK_SIZE - 1): - # 1. Fetch enough for one chunk - data = b'' - - # 1.1. ``bad`` is how much into the data we have we need to offset - bad = self.request.offset & mask - before = self.request.offset - - # 1.2. We have to fetch from a valid offset, so remove that bad part - self.request.offset -= bad - - done = False - while not done and len(data) - bad < self._chunk_size: - cur = await self._request() - self.request.offset += self.request.limit - - data += cur - done = len(cur) < self.request.limit - - # 1.3 Restore our last desired offset - self.request.offset = before - - # 2. Fill the buffer with the data we have - # 2.1. Slicing ``bytes`` is expensive, yield ``memoryview`` instead - mem = memoryview(data) - - # 2.2. The current chunk starts at ``bad`` offset into the data, - # and each new chunk is ``stride`` bytes apart of the other - for i in range(bad, len(data), self._stride): - self.buffer.append(mem[i:i + self._chunk_size]) - - # 2.3. We will yield this offset, so move to the next one - self.request.offset += self._stride - - # 2.4. If we are in the last chunk, we will return the last partial data - if done: - self.left = len(self.buffer) - await self.close() - return - - # 2.5. If we are not done, we can't return incomplete chunks. - if len(self.buffer[-1]) != self._chunk_size: - self._last_part = self.buffer.pop().tobytes() - - # 3. Be careful with the offsets. Re-fetching a bit of data - # is fine, since it greatly simplifies things. - # TODO Try to not re-fetch data - self.request.offset -= self._stride - - -class DownloadMethods: - - # region Public methods - - async def download_profile_photo( - self: 'TelegramClient', - entity: 'hints.EntityLike', - file: 'hints.FileLike' = None, - *, - download_big: bool = True) -> typing.Optional[str]: - """ - Downloads the profile photo from the given user, chat or channel. - - Arguments - entity (`entity`): - From who the photo will be downloaded. - - .. note:: - - This method expects the full entity (which has the data - to download the photo), not an input variant. - - It's possible that sometimes you can't fetch the entity - from its input (since you can get errors like - ``ChannelPrivateError``) but you already have it through - another call, like getting a forwarded message from it. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - as a bytestring (e.g. ``file=bytes``). - - download_big (`bool`, optional): - Whether to use the big version of the available photos. - - Returns - ``None`` if no photo was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. - - Example - .. code-block:: python - - # Download your own profile photo - path = client.download_profile_photo('me') - print(path) - """ - # hex(crc32(x.encode('ascii'))) for x in - # ('User', 'Chat', 'UserFull', 'ChatFull') - ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) - # ('InputPeer', 'InputUser', 'InputChannel') - INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) - if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: - entity = await self.get_entity(entity) - - thumb = -1 if download_big else 0 - - possible_names = [] - if entity.SUBCLASS_OF_ID not in ENTITIES: - photo = entity - else: - if not hasattr(entity, 'photo'): - # Special case: may be a ChatFull with photo:Photo - # This is different from a normal UserProfilePhoto and Chat - if not hasattr(entity, 'chat_photo'): - return None - - return await self._download_photo( - entity.chat_photo, file, date=None, - thumb=thumb, progress_callback=None - ) - - for attr in ('username', 'first_name', 'title'): - possible_names.append(getattr(entity, attr, None)) - - photo = entity.photo - - if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): - dc_id = photo.dc_id - which = photo.photo_big if download_big else photo.photo_small - loc = types.InputPeerPhotoFileLocation( - peer=await self.get_input_entity(entity), - local_id=which.local_id, - volume_id=which.volume_id, - big=download_big - ) - else: - # It doesn't make any sense to check if `photo` can be used - # as input location, because then this method would be able - # to "download the profile photo of a message", i.e. its - # media which should be done with `download_media` instead. - return None - - file = self._get_proper_filename( - file, 'profile_photo', '.jpg', - possible_names=possible_names - ) - - try: - result = await self.download_file(loc, file, dc_id=dc_id) - return result if file is bytes else file - except errors.LocationInvalidError: - # See issue #500, Android app fails as of v4.6.0 (1155). - # The fix seems to be using the full channel chat photo. - ie = await self.get_input_entity(entity) - if isinstance(ie, types.InputPeerChannel): - full = await self(functions.channels.GetFullChannelRequest(ie)) - return await self._download_photo( - full.full_chat.chat_photo, file, - date=None, progress_callback=None, - thumb=thumb - ) - else: - # Until there's a report for chats, no need to. - return None - - async def download_media( - self: 'TelegramClient', - message: 'hints.MessageLike', - file: 'hints.FileLike' = None, - *, - thumb: 'hints.FileLike' = None, - progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: - """ - Downloads the given media from a message object. - - Note that if the download is too slow, you should consider installing - ``cryptg`` (through ``pip install cryptg``) so that decrypting the - received data is done in C instead of Python (much faster). - - See also `Message.download_media() `. - - Arguments - message (`Message ` | :tl:`Media`): - The media or message containing the media that will be downloaded. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - as a bytestring (e.g. ``file=bytes``). - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(received bytes, total)``. - - thumb (`int` | :tl:`PhotoSize`, optional): - Which thumbnail size from the document or photo to download, - instead of downloading the document or photo itself. - - If it's specified but the file does not have a thumbnail, - this method will return ``None``. - - The parameter should be an integer index between ``0`` and - ``len(sizes)``. ``0`` will download the smallest thumbnail, - and ``len(sizes) - 1`` will download the largest thumbnail. - You can also use negative indices. - - You can also pass the :tl:`PhotoSize` instance to use. - - In short, use ``thumb=0`` if you want the smallest thumbnail - and ``thumb=-1`` if you want the largest thumbnail. - - Returns - ``None`` if no media was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. - - Example - .. code-block:: python - - path = client.download_media(message) - client.download_media(message, filename) - # or - path = message.download_media() - message.download_media(filename) - """ - # TODO This won't work for messageService - if isinstance(message, types.Message): - date = message.date - media = message.media - else: - date = datetime.datetime.now() - media = message - - if isinstance(media, str): - media = utils.resolve_bot_file_id(media) - - if isinstance(media, types.MessageMediaWebPage): - if isinstance(media.webpage, types.WebPage): - media = media.webpage.document or media.webpage.photo - - if isinstance(media, (types.MessageMediaPhoto, types.Photo)): - return await self._download_photo( - media, file, date, thumb, progress_callback - ) - elif isinstance(media, (types.MessageMediaDocument, types.Document)): - return await self._download_document( - media, file, date, thumb, progress_callback - ) - elif isinstance(media, types.MessageMediaContact) and thumb is None: - return self._download_contact( - media, file - ) - elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: - return await self._download_web_document( - media, file, progress_callback - ) - - async def download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None) -> typing.Optional[bytes]: - """ - Low-level method to download files from their input location. - - Arguments - input_location (:tl:`InputFileLocation`): - The file location from which the file will be downloaded. - See `telethon.utils.get_input_location` source for a complete - list of supported types. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - - If the file path is ``None`` or ``bytes``, then the result - will be saved in memory and returned as `bytes`. - - part_size_kb (`int`, optional): - Chunk size when downloading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_size (`int`, optional): - The file size that is about to be downloaded, if known. - Only used if ``progress_callback`` is specified. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(downloaded bytes, total)``. Note that the - ``total`` is the provided ``file_size``. - - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. - - Example - .. code-block:: python - - # Download a file and print its header - data = client.download_file(input_file, bytes) - print(data[:16]) - """ - if not part_size_kb: - if not file_size: - part_size_kb = 64 # Reasonable default - else: - part_size_kb = utils.get_appropriated_part_size(file_size) - - part_size = int(part_size_kb * 1024) - if part_size % MIN_CHUNK_SIZE != 0: - raise ValueError( - 'The part size must be evenly divisible by 4096.') - - in_memory = file is None or file is bytes - if in_memory: - f = io.BytesIO() - elif isinstance(file, str): - # Ensure that we'll be able to download the media - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - try: - async for chunk in self.iter_download( - input_location, request_size=part_size, dc_id=dc_id): - f.write(chunk) - if progress_callback: - progress_callback(f.tell(), file_size) - - f.flush() - if in_memory: - return f.getvalue() - finally: - if isinstance(file, str) or in_memory: - f.close() - - def iter_download( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - offset: int = 0, - stride: int = None, - limit: int = None, - chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, - file_size: int = None, - dc_id: int = None - ): - """ - Iterates over a file download, yielding chunks of the file. - - This method can be used to stream files in a more convenient - way, since it offers more control (pausing, resuming, etc.) - - .. note:: - - Using a value for `offset` or `stride` which is not a multiple - of the minimum allowed `request_size`, or if `chunk_size` is - different from `request_size`, the library will need to do a - bit more work to fetch the data in the way you intend it to. - - You normally shouldn't worry about this. - - Arguments - file (`hints.FileLike`): - The file of which contents you want to iterate over. - - offset (`int`, optional): - The offset in bytes into the file from where the - download should start. For example, if a file is - 1024KB long and you just want the last 512KB, you - would use ``offset=512 * 1024``. - - stride (`int`, optional): - The stride of each chunk (how much the offset should - advance between reading each chunk). This parameter - should only be used for more advanced use cases. - - It must be bigger than or equal to the `chunk_size`. - - limit (`int`, optional): - The limit for how many *chunks* will be yielded at most. - - chunk_size (`int`, optional): - The maximum size of the chunks that will be yielded. - Note that the last chunk may be less than this value. - By default, it equals to `request_size`. - - request_size (`int`, optional): - How many bytes will be requested to Telegram when more - data is required. By default, as many bytes as possible - are requested. If you would like to request data in - smaller sizes, adjust this parameter. - - Note that values outside the valid range will be clamped, - and the final value will also be a multiple of the minimum - allowed size. - - file_size (`int`, optional): - If the file size is known beforehand, you should set - this parameter to said value. Depending on the type of - the input file passed, this may be set automatically. - - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. - - Yields - - ``bytes`` objects representing the chunks of the file if the - right conditions are met, or ``memoryview`` objects instead. - - Example - .. code-block:: python - - # Streaming `media` to an output file - # After the iteration ends, the sender is cleaned up - with open('photo.jpg', 'wb') as fd: - for chunk client.iter_download(media): - fd.write(chunk) - - # Fetching only the header of a file (32 bytes) - # You should manually close the iterator in this case. - stream = client.iter_download(media, request_size=32) - header = next(stream) - stream.close() - assert len(header) == 32 - - # Fetching only the header, inside of an ``async def`` - async def main(): - stream = client.iter_download(media, request_size=32) - header = await stream.__anext__() - await stream.close() - assert len(header) == 32 - """ - if chunk_size is None: - chunk_size = request_size - - if limit is None and file_size is not None: - limit = (file_size + chunk_size - 1) // chunk_size - - if stride is None: - stride = chunk_size - elif stride < chunk_size: - raise ValueError('stride must be >= chunk_size') - - request_size -= request_size % MIN_CHUNK_SIZE - if request_size < MIN_CHUNK_SIZE: - request_size = MIN_CHUNK_SIZE - elif request_size > MAX_CHUNK_SIZE: - request_size = MAX_CHUNK_SIZE - - old_dc = dc_id - dc_id, file = utils.get_input_location(file) - if dc_id is None: - dc_id = old_dc - - if chunk_size == request_size \ - and offset % MIN_CHUNK_SIZE == 0 \ - and stride % MIN_CHUNK_SIZE == 0: - cls = _DirectDownloadIter - self._log[__name__].info('Starting direct file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - else: - cls = _GenericDownloadIter - self._log[__name__].info('Starting indirect file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - - return cls( - self, - limit, - file=file, - dc_id=dc_id, - offset=offset, - stride=stride, - chunk_size=chunk_size, - request_size=request_size, - file_size=file_size - ) - - # endregion - - # region Private methods - - @staticmethod - def _get_thumb(thumbs, thumb): - if thumb is None: - return thumbs[-1] - elif isinstance(thumb, int): - return thumbs[thumb] - elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, - types.PhotoStrippedSize)): - return thumb - else: - return None - - def _download_cached_photo_size(self: 'TelegramClient', size, file): - # No need to download anything, simply write the bytes - if isinstance(size, types.PhotoStrippedSize): - data = utils.stripped_photo_to_jpg(size.bytes) - else: - data = size.bytes - - if file is bytes: - return data - elif isinstance(file, str): - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - try: - f.write(data) - finally: - if isinstance(file, str): - f.close() - return file - - async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): - """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size - if isinstance(photo, types.MessageMediaPhoto): - photo = photo.photo - if not isinstance(photo, types.Photo): - return - - size = self._get_thumb(photo.sizes, thumb) - if not size or isinstance(size, types.PhotoSizeEmpty): - return - - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) - - result = await self.download_file( - types.InputPhotoFileLocation( - id=photo.id, - access_hash=photo.access_hash, - file_reference=photo.file_reference, - thumb_size=size.type - ), - file, - file_size=size.size, - progress_callback=progress_callback - ) - return result if file is bytes else file - - @staticmethod - def _get_kind_and_names(attributes): - """Gets kind and possible names for :tl:`DocumentAttribute`.""" - kind = 'document' - possible_names = [] - for attr in attributes: - if isinstance(attr, types.DocumentAttributeFilename): - possible_names.insert(0, attr.file_name) - - elif isinstance(attr, types.DocumentAttributeAudio): - kind = 'audio' - if attr.performer and attr.title: - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) - elif attr.performer: - possible_names.append(attr.performer) - elif attr.title: - possible_names.append(attr.title) - elif attr.voice: - kind = 'voice' - - return kind, possible_names - - async def _download_document( - self, document, file, date, thumb, progress_callback): - """Specialized version of .download_media() for documents.""" - if isinstance(document, types.MessageMediaDocument): - document = document.document - if not isinstance(document, types.Document): - return - - kind, possible_names = self._get_kind_and_names(document.attributes) - file = self._get_proper_filename( - file, kind, utils.get_extension(document), - date=date, possible_names=possible_names - ) - - if thumb is None: - size = None - else: - size = self._get_thumb(document.thumbs, thumb) - if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) - - result = await self.download_file( - types.InputDocumentFileLocation( - id=document.id, - access_hash=document.access_hash, - file_reference=document.file_reference, - thumb_size=size.type if size else '' - ), - file, - file_size=size.size if size else document.size, - progress_callback=progress_callback - ) - - return result if file is bytes else file - - @classmethod - def _download_contact(cls, mm_contact, file): - """ - Specialized version of .download_media() for contacts. - Will make use of the vCard 4.0 format. - """ - first_name = mm_contact.first_name - last_name = mm_contact.last_name - phone_number = mm_contact.phone_number - - # Remove these pesky characters - first_name = first_name.replace(';', '') - last_name = (last_name or '').replace(';', '') - result = ( - 'BEGIN:VCARD\n' - 'VERSION:4.0\n' - 'N:{f};{l};;;\n' - 'FN:{f} {l}\n' - 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' - 'END:VCARD\n' - ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') - - if file is bytes: - return result - elif isinstance(file, str): - file = cls._get_proper_filename( - file, 'contact', '.vcard', - possible_names=[first_name, phone_number, last_name] - ) - f = open(file, 'wb') - else: - f = file - - try: - f.write(result) - finally: - # Only close the stream if we opened it - if isinstance(file, str): - f.close() - - return file - - @classmethod - async def _download_web_document(cls, web, file, progress_callback): - """ - Specialized version of .download_media() for web documents. - """ - if not aiohttp: - raise ValueError( - 'Cannot download web documents without the aiohttp ' - 'dependency install it (pip install aiohttp)' - ) - - # TODO Better way to get opened handles of files and auto-close - in_memory = file is bytes - if in_memory: - f = io.BytesIO() - elif isinstance(file, str): - kind, possible_names = cls._get_kind_and_names(web.attributes) - file = cls._get_proper_filename( - file, kind, utils.get_extension(web), - possible_names=possible_names - ) - f = open(file, 'wb') - else: - f = file - - try: - with aiohttp.ClientSession() as session: - # TODO Use progress_callback; get content length from response - # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 - async with session.get(web.url) as response: - while True: - chunk = await response.content.read(128 * 1024) - if not chunk: - break - f.write(chunk) - finally: - if isinstance(file, str) or file is bytes: - f.close() - - return f.getvalue() if in_memory else file - - @staticmethod - def _get_proper_filename(file, kind, extension, - date=None, possible_names=None): - """Gets a proper filename for 'file', if this is a path. - - 'kind' should be the kind of the output file (photo, document...) - 'extension' should be the extension to be added to the file if - the filename doesn't have any yet - 'date' should be when this file was originally sent, if known - 'possible_names' should be an ordered list of possible names - - If no modification is made to the path, any existing file - will be overwritten. - If any modification is made to the path, this method will - ensure that no existing file will be overwritten. - """ - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - if file is not None and not isinstance(file, str): - # Probably a stream-like object, we cannot set a filename here - return file - - if file is None: - file = '' - elif os.path.isfile(file): - # Make no modifications to valid existing paths - return file - - if os.path.isdir(file) or not file: - try: - name = None if possible_names is None else next( - x for x in possible_names if x - ) - except StopIteration: - name = None - - if not name: - if not date: - date = datetime.datetime.now() - name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( - kind, - date.year, date.month, date.day, - date.hour, date.minute, date.second, - ) - file = os.path.join(file, name) - - directory, name = os.path.split(file) - name, ext = os.path.splitext(name) - if not ext: - ext = extension - - result = os.path.join(directory, name + ext) - if not os.path.isfile(result): - return result - - i = 1 - while True: - result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) - if not os.path.isfile(result): - return result - i += 1 - - # endregion diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py index 80eabbb2..e69de29b 100644 --- a/telethon/client/messageparse.py +++ b/telethon/client/messageparse.py @@ -1,172 +0,0 @@ -import itertools -import re -import typing - -from .. import utils -from ..tl import types - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class MessageParseMethods: - - # region Public properties - - @property - def parse_mode(self: 'TelegramClient'): - """ - This property is the default parse mode used when sending messages. - Defaults to `telethon.extensions.markdown`. It will always - be either ``None`` or an object with ``parse`` and ``unparse`` - methods. - - When setting a different value it should be one of: - - * Object with ``parse`` and ``unparse`` methods. - * A ``callable`` to act as the parse method. - * A ``str`` indicating the ``parse_mode``. For Markdown ``'md'`` - or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` - may be used. - - The ``parse`` method should be a function accepting a single - parameter, the text to parse, and returning a tuple consisting - of ``(parsed message str, [MessageEntity instances])``. - - The ``unparse`` method should be the inverse of ``parse`` such - that ``assert text == unparse(*parse(text))``. - - See :tl:`MessageEntity` for allowed message entities. - - Example - .. code-block:: python - - # Disabling default formatting - client.parse_mode = None - - # Enabling HTML as the default format - client.parse_mode = 'html' - """ - return self._parse_mode - - @parse_mode.setter - def parse_mode(self: 'TelegramClient', mode: str): - self._parse_mode = utils.sanitize_parse_mode(mode) - - # endregion - - # region Private methods - - async def _replace_with_mention(self: 'TelegramClient', entities, i, user): - """ - Helper method to replace ``entities[i]`` to mention ``user``, - or do nothing if it can't be found. - """ - try: - entities[i] = types.InputMessageEntityMentionName( - entities[i].offset, entities[i].length, - await self.get_input_entity(user) - ) - return True - except (ValueError, TypeError): - return False - - async def _parse_message_text(self: 'TelegramClient', message, parse_mode): - """ - Returns a (parsed message, entities) tuple depending on ``parse_mode``. - """ - if parse_mode is (): - parse_mode = self._parse_mode - else: - parse_mode = utils.sanitize_parse_mode(parse_mode) - - if not parse_mode: - return message, [] - - message, msg_entities = parse_mode.parse(message) - for i in reversed(range(len(msg_entities))): - e = msg_entities[i] - if isinstance(e, types.MessageEntityTextUrl): - m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) - if m: - user = int(m.group(1)) if m.group(1) else e.url - is_mention = await self._replace_with_mention(msg_entities, i, user) - if not is_mention: - del msg_entities[i] - elif isinstance(e, (types.MessageEntityMentionName, - types.InputMessageEntityMentionName)): - is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) - if not is_mention: - del msg_entities[i] - - return message, msg_entities - - def _get_response_message(self: 'TelegramClient', request, result, input_chat): - """ - Extracts the response message known a request and Update result. - The request may also be the ID of the message to match. - - If ``request is None`` this method returns ``{id: message}``. - - If ``request.random_id`` is a list, this method returns a list too. - """ - if isinstance(result, types.UpdateShort): - updates = [result.update] - entities = {} - elif isinstance(result, (types.Updates, types.UpdatesCombined)): - updates = result.updates - entities = {utils.get_peer_id(x): x - for x in - itertools.chain(result.users, result.chats)} - else: - return None - - random_to_id = {} - id_to_message = {} - for update in updates: - if isinstance(update, types.UpdateMessageID): - random_to_id[update.random_id] = update.id - - elif isinstance(update, ( - types.UpdateNewChannelMessage, types.UpdateNewMessage)): - update.message._finish_init(self, entities, input_chat) - id_to_message[update.message.id] = update.message - - elif (isinstance(update, types.UpdateEditMessage) - and not isinstance(request.peer, types.InputPeerChannel)): - if request.id == update.message.id: - update.message._finish_init(self, entities, input_chat) - return update.message - - elif (isinstance(update, types.UpdateEditChannelMessage) - and utils.get_peer_id(request.peer) == - utils.get_peer_id(update.message.to_id)): - if request.id == update.message.id: - update.message._finish_init(self, entities, input_chat) - return update.message - - if request is None: - return id_to_message - - random_id = request if isinstance(request, int) else request.random_id - if not utils.is_list_like(random_id): - msg = id_to_message.get(random_to_id.get(random_id)) - if not msg: - self._log[__name__].warning( - 'Request %s had missing message mapping %s', request, result) - - return msg - - try: - return [id_to_message[random_to_id[rnd]] for rnd in random_id] - except KeyError: - # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets - # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at - # Telegram), in which case we get some "missing" message mappings. - # Log them with the hope that we can better work around them. - self._log[__name__].warning( - 'Request %s had missing message mappings %s', request, result) - - return [id_to_message.get(random_to_id.get(rnd)) for rnd in random_to_id] - - # endregion diff --git a/telethon/client/messages.py b/telethon/client/messages.py index 54b262da..e69de29b 100644 --- a/telethon/client/messages.py +++ b/telethon/client/messages.py @@ -1,1166 +0,0 @@ -import itertools -import typing - -from .. import utils, errors, hints -from ..requestiter import RequestIter -from ..tl import types, functions - -_MAX_CHUNK_SIZE = 100 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class _MessagesIter(RequestIter): - """ - Common factor for all requests that need to iterate over messages. - """ - async def _init( - self, entity, offset_id, min_id, max_id, - from_user, offset_date, add_offset, filter, search - ): - # Note that entity being ``None`` will perform a global search. - if entity: - self.entity = await self.client.get_input_entity(entity) - else: - self.entity = None - if self.reverse: - raise ValueError('Cannot reverse global search') - - # Telegram doesn't like min_id/max_id. If these IDs are low enough - # (starting from last_id - 100), the request will return nothing. - # - # We can emulate their behaviour locally by setting offset = max_id - # and simply stopping once we hit a message with ID <= min_id. - if self.reverse: - offset_id = max(offset_id, min_id) - if offset_id and max_id: - if max_id - offset_id <= 1: - raise StopAsyncIteration - - if not max_id: - max_id = float('inf') - else: - offset_id = max(offset_id, max_id) - if offset_id and min_id: - if offset_id - min_id <= 1: - raise StopAsyncIteration - - if self.reverse: - if offset_id: - offset_id += 1 - elif not offset_date: - # offset_id has priority over offset_date, so don't - # set offset_id to 1 if we want to offset by date. - offset_id = 1 - - if from_user: - from_user = await self.client.get_input_entity(from_user) - if not isinstance(from_user, ( - types.InputPeerUser, types.InputPeerSelf)): - from_user = None # Ignore from_user unless it's a user - - if from_user: - self.from_id = await self.client.get_peer_id(from_user) - else: - self.from_id = None - - if not self.entity: - self.request = functions.messages.SearchGlobalRequest( - q=search or '', - offset_rate=offset_date, - offset_peer=types.InputPeerEmpty(), - offset_id=offset_id, - limit=1 - ) - elif search is not None or filter or from_user: - if filter is None: - filter = types.InputMessagesFilterEmpty() - - # Telegram completely ignores `from_id` in private chats - if isinstance( - self.entity, (types.InputPeerUser, types.InputPeerSelf)): - # Don't bother sending `from_user` (it's ignored anyway), - # but keep `from_id` defined above to check it locally. - from_user = None - else: - # Do send `from_user` to do the filtering server-side, - # and set `from_id` to None to avoid checking it locally. - self.from_id = None - - self.request = functions.messages.SearchRequest( - peer=self.entity, - q=search or '', - filter=filter() if isinstance(filter, type) else filter, - min_date=None, - max_date=offset_date, - offset_id=offset_id, - add_offset=add_offset, - limit=0, # Search actually returns 0 items if we ask it to - max_id=0, - min_id=0, - hash=0, - from_id=from_user - ) - - # Workaround issue #1124 until a better solution is found. - # Telegram seemingly ignores `max_date` if `filter` (and - # nothing else) is specified, so we have to rely on doing - # a first request to offset from the ID instead. - # - # Even better, using `filter` and `from_id` seems to always - # trigger `RPC_CALL_FAIL` which is "internal issues"... - if filter and offset_date and not search and not offset_id: - async for m in self.client.iter_messages( - self.entity, 1, offset_date=offset_date): - self.request.offset_id = m.id + 1 - else: - self.request = functions.messages.GetHistoryRequest( - peer=self.entity, - limit=1, - offset_date=offset_date, - offset_id=offset_id, - min_id=0, - max_id=0, - add_offset=add_offset, - hash=0 - ) - - if self.limit <= 0: - # No messages, but we still need to know the total message count - result = await self.client(self.request) - if isinstance(result, types.messages.MessagesNotModified): - self.total = result.count - else: - self.total = getattr(result, 'count', len(result.messages)) - raise StopAsyncIteration - - if self.wait_time is None: - self.wait_time = 1 if self.limit > 3000 else 0 - - # When going in reverse we need an offset of `-limit`, but we - # also want to respect what the user passed, so add them together. - if self.reverse: - self.request.add_offset -= _MAX_CHUNK_SIZE - - self.add_offset = add_offset - self.max_id = max_id - self.min_id = min_id - self.last_id = 0 if self.reverse else float('inf') - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) - if self.reverse and self.request.limit != _MAX_CHUNK_SIZE: - # Remember that we need -limit when going in reverse - self.request.add_offset = self.add_offset - self.request.limit - - r = await self.client(self.request) - self.total = getattr(r, 'count', len(r.messages)) - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - messages = reversed(r.messages) if self.reverse else r.messages - for message in messages: - if (isinstance(message, types.MessageEmpty) - or self.from_id and message.from_id != self.from_id): - continue - - if not self._message_in_range(message): - return True - - # There has been reports that on bad connections this method - # was returning duplicated IDs sometimes. Using ``last_id`` - # is an attempt to avoid these duplicates, since the message - # IDs are returned in descending order (or asc if reverse). - self.last_id = message.id - message._finish_init(self.client, entities, self.entity) - self.buffer.append(message) - - if len(r.messages) < self.request.limit: - return True - - # Get the last message that's not empty (in some rare cases - # it can happen that the last message is :tl:`MessageEmpty`) - if self.buffer: - self._update_offset(self.buffer[-1]) - else: - # There are some cases where all the messages we get start - # being empty. This can happen on migrated mega-groups if - # the history was cleared, and we're using search. Telegram - # acts incredibly weird sometimes. Messages are returned but - # only "empty", not their contents. If this is the case we - # should just give up since there won't be any new Message. - return True - - def _message_in_range(self, message): - """ - Determine whether the given message is in the range or - it should be ignored (and avoid loading more chunks). - """ - # No entity means message IDs between chats may vary - if self.entity: - if self.reverse: - if message.id <= self.last_id or message.id >= self.max_id: - return False - else: - if message.id >= self.last_id or message.id <= self.min_id: - return False - - return True - - def _update_offset(self, last_message): - """ - After making the request, update its offset with the last message. - """ - self.request.offset_id = last_message.id - if self.reverse: - # We want to skip the one we already have - self.request.offset_id += 1 - - if isinstance(self.request, functions.messages.SearchRequest): - # Unlike getHistory and searchGlobal that use *offset* date, - # this is *max* date. This means that doing a search in reverse - # will break it. Since it's not really needed once we're going - # (only for the first request), it's safe to just clear it off. - self.request.max_date = None - else: - # getHistory and searchGlobal call it offset_date - self.request.offset_date = last_message.date - - if isinstance(self.request, functions.messages.SearchGlobalRequest): - self.request.offset_peer = last_message.input_chat - - -class _IDsIter(RequestIter): - async def _init(self, entity, ids): - # TODO We never actually split IDs in chunks, but maybe we should - if not utils.is_list_like(ids): - ids = [ids] - elif not ids: - raise StopAsyncIteration - elif self.reverse: - ids = list(reversed(ids)) - else: - ids = ids - - if entity: - entity = await self.client.get_input_entity(entity) - - self.total = len(ids) - - from_id = None # By default, no need to validate from_id - if isinstance(entity, (types.InputChannel, types.InputPeerChannel)): - try: - r = await self.client( - functions.channels.GetMessagesRequest(entity, ids)) - except errors.MessageIdsEmptyError: - # All IDs were invalid, use a dummy result - r = types.messages.MessagesNotModified(len(ids)) - else: - r = await self.client(functions.messages.GetMessagesRequest(ids)) - if entity: - from_id = await self.client.get_peer_id(entity) - - if isinstance(r, types.messages.MessagesNotModified): - self.buffer.extend(None for _ in ids) - return - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - # Telegram seems to return the messages in the order in which - # we asked them for, so we don't need to check it ourselves, - # unless some messages were invalid in which case Telegram - # may decide to not send them at all. - # - # The passed message IDs may not belong to the desired entity - # since the user can enter arbitrary numbers which can belong to - # arbitrary chats. Validate these unless ``from_id is None``. - for message in r.messages: - if isinstance(message, types.MessageEmpty) or ( - from_id and message.chat_id != from_id): - self.buffer.append(None) - else: - message._finish_init(self.client, entities, entity) - self.buffer.append(message) - - async def _load_next_chunk(self): - return True # no next chunk, all done in init - - -class MessageMethods: - - # region Public methods - - # region Message retrieval - - def iter_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - offset_date: 'hints.DateLike' = None, - offset_id: int = 0, - max_id: int = 0, - min_id: int = 0, - add_offset: int = 0, - search: str = None, - filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, - from_user: 'hints.EntityLike' = None, - wait_time: float = None, - ids: 'typing.Union[int, typing.Sequence[int]]' = None, - reverse: bool = False - ) -> 'typing.Union[_MessagesIter, _IDsIter]': - """ - Iterator over the messages for the given chat. - - If either `search`, `filter` or `from_user` are provided, - :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. - - .. note:: - - Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to - be around 30 seconds per 10 requests, therefore a sleep of 1 - second is the default for this limit (or above). - - Arguments - entity (`entity`): - The entity from whom to retrieve the message history. - - It may be ``None`` to perform a global search, or - to get messages by their ID from no particular chat. - Note that some of the offsets will not work if this - is the case. - - Note that if you want to perform a global search, - you **must** set a non-empty `search` string. - - limit (`int` | `None`, optional): - Number of messages to be retrieved. Due to limitations with - the API retrieving more than 3000 messages will take longer - than half a minute (or even more based on previous calls). - - The limit may also be ``None``, which would eventually return - the whole history. - - offset_date (`datetime`): - Offset date (messages *previous* to this date will be - retrieved). Exclusive. - - offset_id (`int`): - Offset message ID (only messages *previous* to the given - ID will be retrieved). Exclusive. - - max_id (`int`): - All the messages with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the messages with a lower (older) ID or equal to this will - be excluded. - - add_offset (`int`): - Additional message offset (all of the specified offsets + - this offset = older messages). - - search (`str`): - The string to be used as a search query. - - filter (:tl:`MessagesFilter` | `type`): - The filter to use when returning messages. For instance, - :tl:`InputMessagesFilterPhotos` would yield only messages - containing photos. - - from_user (`entity`): - Only messages from this user will be returned. - This parameter will be ignored if it is not an user. - - wait_time (`int`): - Wait time (in seconds) between different - :tl:`GetHistoryRequest`. Use this parameter to avoid hitting - the ``FloodWaitError`` as needed. If left to ``None``, it will - default to 1 second only if the limit is higher than 3000. - - ids (`int`, `list`): - A single integer ID (or several IDs) for the message that - should be returned. This parameter takes precedence over - the rest (which will be ignored if this is set). This can - for instance be used to get the message with ID 123 from - a channel. Note that if the message doesn't exist, ``None`` - will appear in its place, so that zipping the list of IDs - with the messages can match one-to-one. - - .. note:: - - At the time of writing, Telegram will **not** return - :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that - failed (i.e. the message is not replying to any, or is - replying to a deleted message). This means that it is - **not** possible to match messages one-by-one, so be - careful if you use non-integers in this parameter. - - reverse (`bool`, optional): - If set to ``True``, the messages will be returned in reverse - order (from oldest to newest, instead of the default newest - to oldest). This also means that the meaning of `offset_id` - and `offset_date` parameters is reversed, although they will - still be exclusive. `min_id` becomes equivalent to `offset_id` - instead of being `max_id` as well since messages are returned - in ascending order. - - You cannot use this if both `entity` and `ids` are ``None``. - - Yields - Instances of `Message `. - - Example - .. code-block:: python - - # From most-recent to oldest - for message in client.iter_messages(chat): - print(message.id, message.text) - - # From oldest to most-recent - for message in client.iter_messages(chat, reverse=True): - print(message.id, message.text) - - # Filter by sender - for message in client.iter_messages(chat, from_user='me'): - print(message.text) - - # Server-side search with fuzzy text - for message in client.iter_messages(chat, search='hello'): - print(message.id) - - # Filter by message type: - from telethon.tl.types import InputMessagesFilterPhotos - for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): - print(message.photo) - """ - if ids is not None: - return _IDsIter(self, limit, entity=entity, ids=ids) - - return _MessagesIter( - client=self, - reverse=reverse, - wait_time=wait_time, - limit=limit, - entity=entity, - offset_id=offset_id, - min_id=min_id, - max_id=max_id, - from_user=from_user, - offset_date=offset_date, - add_offset=add_offset, - filter=filter, - search=search - ) - - async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_messages()`, but returns a - `TotalList ` instead. - - If the `limit` is not set, it will be 1 by default unless both - `min_id` **and** `max_id` are set (as *named* arguments), in - which case the entire range will be returned. - - This is so because any integer limit would be rather arbitrary and - it's common to only want to fetch one message, but if a range is - specified it makes sense that it should return the entirety of it. - - If `ids` is present in the *named* arguments and is not a list, - a single `Message ` will be - returned for convenience instead of a list. - - Example - .. code-block:: python - - # Get 0 photos and print the total to show how many photos there are - from telethon.tl.types import InputMessagesFilterPhotos - photos = client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) - print(photos.total) - - # Get all the photos - photos = client.get_messages(chat, None, filter=InputMessagesFilterPhotos) - - # Get messages by ID: - message_1337 = client.get_messages(chats, ids=1337) - """ - if len(args) == 1 and 'limit' not in kwargs: - if 'min_id' in kwargs and 'max_id' in kwargs: - kwargs['limit'] = None - else: - kwargs['limit'] = 1 - - it = self.iter_messages(*args, **kwargs) - - ids = kwargs.get('ids') - if ids and not utils.is_list_like(ids): - async for message in it: - return message - else: - # Iterator exhausted = empty, to handle InputMessageReplyTo - return None - - return await it.collect() - - # endregion - - # region Message sending/editing/deleting - - async def send_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'hints.MessageLike' = '', - *, - reply_to: 'typing.Union[int, types.Message]' = None, - parse_mode: typing.Optional[str] = (), - link_preview: bool = True, - file: 'hints.FileLike' = None, - force_document: bool = False, - clear_draft: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None) -> 'types.Message': - """ - Sends a message to the specified user, chat or channel. - - The default parse mode is the same as the official applications - (a custom flavour of markdown). ``**bold**, `code` or __italic__`` - are available. In addition you can send ``[links](https://example.com)`` - and ``[mentions](@username)`` (or using IDs like in the Bot API: - ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three - backticks. - - Sending a ``/start`` command with a parameter (like ``?start=data``) - is also done through this method. Simply send ``'/start data'`` to - the bot. - - See also `Message.respond() ` - and `Message.reply() `. - - Arguments - entity (`entity`): - To who will it be sent. - - message (`str` | `Message `): - The message to be sent, or another message object to resend. - - The maximum length for a message is 35,000 bytes or 4,096 - characters. Longer messages will not be sliced automatically, - and you should slice them manually if the text to send is - longer than said length. - - reply_to (`int` | `Message `, optional): - Whether to reply to a message or not. If an integer is provided, - it should be the ID of the message that it should reply to. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`file`, optional): - Sends a message with a file attached (e.g. a photo, - video, audio or document). The ``message`` may be empty. - - force_document (`bool`, optional): - Whether to send the given file as a document or not. - - clear_draft (`bool`, optional): - Whether the existing draft should be cleared or not. - Has no effect when sending a file. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - All the following limits apply together: - - * There can be 100 buttons at most (any more are ignored). - * There can be 8 buttons per row at most (more are ignored). - * The maximum callback data per button is 64 bytes. - * The maximum data that can be embedded in total is just - over 4KB, shared between inline callback data and text. - - silent (`bool`, optional): - Whether the message should notify people in a broadcast - channel or not. Defaults to ``False``, which means it will - notify them. Set it to ``True`` to alter this behaviour. - - Returns - The sent `custom.Message `. - - Example - .. code-block:: python - - # Markdown is the default - client.send_message('lonami', 'Thanks for the **Telethon** library!') - - # Default to another parse mode - client.parse_mode = 'html' - - client.send_message('me', 'Some bold and italic text') - client.send_message('me', 'An URL') - # code and pre tags also work, but those break the documentation :) - client.send_message('me', 'Mentions') - - # Explicit parse mode - # No parse mode by default - client.parse_mode = None - - # ...but here I want markdown - client.send_message('me', 'Hello, **world**!', parse_mode='md') - - # ...and here I need HTML - client.send_message('me', 'Hello, world!', parse_mode='html') - - # If you logged in as a bot account, you can send buttons - from telethon import events, Button - - @client.on(events.CallbackQuery) - async def callback(event): - await event.edit('Thank you for clicking {}!'.format(event.data)) - - # Single inline button - client.send_message(chat, 'A single button, with "clk1" as data', - buttons=Button.inline('Click me', b'clk1')) - - # Matrix of inline buttons - client.send_message(chat, 'Pick one from this grid', buttons=[ - [Button.inline('Left'), Button.inline('Right')], - [Button.url('Check this site!', 'https://lonamiwebs.github.io')] - ]) - - # Reply keyboard - client.send_message(chat, 'Welcome', buttons=[ - Button.text('Thanks!', resize=True, single_use=True), - Button.request_phone('Send phone'), - Button.request_location('Send location') - ]) - - # Forcing replies or clearing buttons. - client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) - client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) - """ - if file is not None: - return await self.send_file( - entity, file, caption=message, reply_to=reply_to, - parse_mode=parse_mode, force_document=force_document, - buttons=buttons - ) - elif not message: - raise ValueError( - 'The message cannot be empty unless a file is provided' - ) - - entity = await self.get_input_entity(entity) - if isinstance(message, types.Message): - if buttons is None: - markup = message.reply_markup - else: - markup = self.build_reply_markup(buttons) - - if silent is None: - silent = message.silent - - if (message.media and not isinstance( - message.media, types.MessageMediaWebPage)): - return await self.send_file( - entity, - message.media, - caption=message.message, - silent=silent, - reply_to=reply_to, - buttons=markup, - entities=message.entities - ) - - request = functions.messages.SendMessageRequest( - peer=entity, - message=message.message or '', - silent=silent, - reply_to_msg_id=utils.get_message_id(reply_to), - reply_markup=markup, - entities=message.entities, - clear_draft=clear_draft, - no_webpage=not isinstance( - message.media, types.MessageMediaWebPage) - ) - message = message.message - else: - message, msg_ent = await self._parse_message_text(message, - parse_mode) - request = functions.messages.SendMessageRequest( - peer=entity, - message=message, - entities=msg_ent, - no_webpage=not link_preview, - reply_to_msg_id=utils.get_message_id(reply_to), - clear_draft=clear_draft, - silent=silent, - reply_markup=self.build_reply_markup(buttons) - ) - - result = await self(request) - if isinstance(result, types.UpdateShortSentMessage): - message = types.Message( - id=result.id, - to_id=utils.get_peer(entity), - message=message, - date=result.date, - out=result.out, - media=result.media, - entities=result.entities, - reply_markup=request.reply_markup - ) - message._finish_init(self, {}, entity) - return message - - return self._get_response_message(request, result, entity) - - async def forward_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - from_peer: 'hints.EntityLike' = None, - *, - silent: bool = None, - as_album: bool = None) -> 'typing.Sequence[types.Message]': - """ - Forwards the given messages to the specified entity. - - If you want to "forward" a message without the forward header - (the "forwarded from" text), you should use `send_message` with - the original message instead. This will send a copy of it. - - See also `Message.forward_to() `. - - Arguments - entity (`entity`): - To which entity the message(s) will be forwarded. - - messages (`list` | `int` | `Message `): - The message(s) to forward, or their integer IDs. - - from_peer (`entity`): - If the given messages are integer IDs and not instances - of the ``Message`` class, this *must* be specified in - order for the forward to work. This parameter indicates - the entity from which the messages should be forwarded. - - silent (`bool`, optional): - Whether the message should notify people in a broadcast - channel or not. Defaults to ``False``, which means it will - notify them. Set it to ``True`` to alter this behaviour. - - as_album (`bool`, optional): - Whether several image messages should be forwarded as an - album (grouped) or not. The default behaviour is to treat - albums specially and send outgoing requests with - ``as_album=True`` only for the albums if message objects - are used. If IDs are used it will group by default. - - In short, the default should do what you expect, - ``True`` will group always (even converting separate - images into albums), and ``False`` will never group. - - Returns - The list of forwarded `Message `, - or a single one if a list wasn't provided as input. - - Note that if all messages are invalid (i.e. deleted) the call - will fail with ``MessageIdInvalidError``. If only some are - invalid, the list will have ``None`` instead of those messages. - - Example - .. code-block:: python - - # a single one - client.forward_messages(chat, message) - # or - client.forward_messages(chat, message_id, from_chat) - # or - message.forward_to(chat) - - # multiple - client.forward_messages(chat, messages) - # or - client.forward_messages(chat, message_ids, from_chat) - - # Forwarding as a copy - client.send_message(chat, message) - """ - single = not utils.is_list_like(messages) - if single: - messages = (messages,) - - entity = await self.get_input_entity(entity) - - if from_peer: - from_peer = await self.get_input_entity(from_peer) - from_peer_id = await self.get_peer_id(from_peer) - else: - from_peer_id = None - - def _get_key(m): - if isinstance(m, int): - if from_peer_id is not None: - return from_peer_id, None - - raise ValueError('from_peer must be given if integer IDs are used') - elif isinstance(m, types.Message): - return m.chat_id, m.grouped_id - else: - raise TypeError('Cannot forward messages of type {}'.format(type(m))) - - # We want to group outgoing chunks differently if we are "smart" - # about sending as album. - # - # Why? We need separate requests for ``as_album=True/False``, so - # if we want that behaviour, when we group messages to create the - # chunks, we need to consider the grouped ID too. But if we don't - # care about that, we don't need to consider it for creating the - # chunks, so we can make less requests. - if as_album is None: - get_key = _get_key - else: - def get_key(m): - return _get_key(m)[0] # Ignore grouped_id - - sent = [] - for chat_id, chunk in itertools.groupby(messages, key=get_key): - chunk = list(chunk) - if isinstance(chunk[0], int): - chat = from_peer - grouped = True if as_album is None else as_album - else: - chat = await chunk[0].get_input_chat() - if as_album is None: - grouped = any(m.grouped_id is not None for m in chunk) - else: - grouped = as_album - - chunk = [m.id for m in chunk] - - req = functions.messages.ForwardMessagesRequest( - from_peer=chat, - id=chunk, - to_peer=entity, - silent=silent, - # Trying to send a single message as grouped will cause - # GROUPED_MEDIA_INVALID. If more than one message is forwarded - # (even without media...), this error goes away. - grouped=len(chunk) > 1 and grouped - ) - result = await self(req) - sent.extend(self._get_response_message(req, result, entity)) - - return sent[0] if single else sent - - async def edit_message( - self: 'TelegramClient', - entity: 'typing.Union[hints.EntityLike, types.Message]', - message: 'hints.MessageLike' = None, - text: str = None, - *, - parse_mode: str = (), - link_preview: bool = True, - file: 'hints.FileLike' = None, - buttons: 'hints.MarkupLike' = None) -> 'types.Message': - """ - Edits the given message to change its text or media. - - See also `Message.edit() `. - - Arguments - entity (`entity` | `Message `): - From which chat to edit the message. This can also be - the message to be edited, and the entity will be inferred - from it, so the next parameter will be assumed to be the - message text. - - You may also pass a :tl:`InputBotInlineMessageID`, - which is the only way to edit messages that were sent - after the user selects an inline query result. - - message (`int` | `Message ` | `str`): - The ID of the message (or `Message - ` itself) to be edited. - If the `entity` was a `Message - `, then this message - will be treated as the new text. - - text (`str`, optional): - The new text of the message. Does nothing if the `entity` - was a `Message `. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`str` | `bytes` | `file` | `media`, optional): - The file object that should replace the existing media - in the message. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - Returns - The edited `Message `, - unless `entity` was a :tl:`InputBotInlineMessageID` in which - case this method returns a boolean. - - Raises - ``MessageAuthorRequiredError`` if you're not the author of the - message but tried editing it anyway. - - ``MessageNotModifiedError`` if the contents of the message were - not modified at all. - - Example - .. code-block:: python - - message = client.send_message(chat, 'hello') - - client.edit_message(chat, message, 'hello!') - # or - client.edit_message(chat, message.id, 'hello!!') - # or - client.edit_message(message, 'hello!!!') - """ - if isinstance(entity, types.InputBotInlineMessageID): - text = message - message = entity - elif isinstance(entity, types.Message): - text = message # Shift the parameters to the right - message = entity - entity = entity.to_id - - text, msg_entities = await self._parse_message_text(text, parse_mode) - file_handle, media, image = await self._file_to_media(file) - - if isinstance(entity, types.InputBotInlineMessageID): - return await self(functions.messages.EditInlineBotMessageRequest( - id=entity, - message=text, - no_webpage=not link_preview, - entities=msg_entities, - media=media, - reply_markup=self.build_reply_markup(buttons) - )) - - entity = await self.get_input_entity(entity) - request = functions.messages.EditMessageRequest( - peer=entity, - id=utils.get_message_id(message), - message=text, - no_webpage=not link_preview, - entities=msg_entities, - media=media, - reply_markup=self.build_reply_markup(buttons) - ) - msg = self._get_response_message(request, await self(request), entity) - await self._cache_media(msg, file, file_handle, image=image) - return msg - - async def delete_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - *, - revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': - """ - Deletes the given messages, optionally "for everyone". - - See also `Message.delete() `. - - .. warning:: - - This method does **not** validate that the message IDs belong - to the chat that you passed! It's possible for the method to - delete messages from different private chats and small group - chats at once, so make sure to pass the right IDs. - - Arguments - entity (`entity`): - From who the message will be deleted. This can actually - be ``None`` for normal chats, but **must** be present - for channels and megagroups. - - message_ids (`list` | `int` | `Message `): - The IDs (or ID) or messages to be deleted. - - revoke (`bool`, optional): - Whether the message should be deleted for everyone or not. - By default it has the opposite behaviour of official clients, - and it will delete the message for everyone. - - `Since 24 March 2019 - `_, you can - also revoke messages of any age (i.e. messages sent long in - the past) the *other* person sent in private conversations - (and of course your messages too). - - Disabling this has no effect on channels or megagroups, - since it will unconditionally delete the message for everyone. - - Returns - A list of :tl:`AffectedMessages`, each item being the result - for the delete calls of the messages in chunks of 100 each. - - Example - .. code-block:: python - - client.delete_messages(chat, messages) - """ - if not utils.is_list_like(message_ids): - message_ids = (message_ids,) - - message_ids = ( - m.id if isinstance(m, ( - types.Message, types.MessageService, types.MessageEmpty)) - else int(m) for m in message_ids - ) - - entity = await self.get_input_entity(entity) if entity else None - if isinstance(entity, types.InputPeerChannel): - return await self([functions.channels.DeleteMessagesRequest( - entity, list(c)) for c in utils.chunks(message_ids)]) - else: - return await self([functions.messages.DeleteMessagesRequest( - list(c), revoke) for c in utils.chunks(message_ids)]) - - # endregion - - # region Miscellaneous - - async def send_read_acknowledge( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, - *, - max_id: int = None, - clear_mentions: bool = False) -> bool: - """ - Marks messages as read and optionally clears mentions. - - This effectively marks a message as read (or more than one) in the - given conversation. - - If neither message nor maximum ID are provided, all messages will be - marked as read by assuming that ``max_id = 0``. - - Arguments - entity (`entity`): - The chat where these messages are located. - - message (`list` | `Message `): - Either a list of messages or a single message. - - max_id (`int`): - Overrides messages, until which message should the - acknowledge should be sent. - - clear_mentions (`bool`): - Whether the mention badge should be cleared (so that - there are no more mentions) or not for the given entity. - - If no message is provided, this will be the only action - taken. - - Example - .. code-block:: python - - client.send_read_acknowledge(last_message) - # or - client.send_read_acknowledge(last_message_id) - # or - client.send_read_acknowledge(messages) - """ - if max_id is None: - if not message: - max_id = 0 - else: - if utils.is_list_like(message): - max_id = max(msg.id for msg in message) - else: - max_id = message.id - - entity = await self.get_input_entity(entity) - if clear_mentions: - await self(functions.messages.ReadMentionsRequest(entity)) - if max_id is None: - return True - - if max_id is not None: - if isinstance(entity, types.InputPeerChannel): - return await self(functions.channels.ReadHistoryRequest( - utils.get_input_channel(entity), max_id=max_id)) - else: - return await self(functions.messages.ReadHistoryRequest( - entity, max_id=max_id)) - - return False - - async def pin_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Optional[hints.MessageIDLike]', - *, - notify: bool = False - ): - """ - Pins or unpins a message in a chat. - - The default behaviour is to *not* notify members, unlike the - official applications. - - See also `Message.pin() `. - - Arguments - entity (`entity`): - The chat where the message should be pinned. - - message (`int` | `Message `): - The message or the message ID to pin. If it's - ``None``, the message will be unpinned instead. - - notify (`bool`, optional): - Whether the pin should notify people or not. - - Example - .. code-block:: python - - # Send and pin a message to annoy everyone - message = client.send_message(chat, 'Pinotifying is fun!') - client.pin_message(chat, message, notify=True) - """ - if not message: - message = 0 - - entity = await self.get_input_entity(entity) - await self(functions.messages.UpdatePinnedMessageRequest( - peer=entity, - id=message, - silent=not notify - )) - - # endregion - - # endregion diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py index e0fea3d2..e69de29b 100644 --- a/telethon/client/telegrambaseclient.py +++ b/telethon/client/telegrambaseclient.py @@ -1,682 +0,0 @@ -import abc -import asyncio -import collections -import logging -import platform -import time -import typing - -from .. import version, helpers, __name__ as __base_name__ -from ..crypto import rsa -from ..entitycache import EntityCache -from ..extensions import markdown -from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy -from ..sessions import Session, SQLiteSession, MemorySession -from ..statecache import StateCache -from ..tl import TLObject, functions, types -from ..tl.alltlobjects import LAYER - -DEFAULT_DC_ID = 4 -DEFAULT_IPV4_IP = '149.154.167.51' -DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]' -DEFAULT_PORT = 443 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - -__default_log__ = logging.getLogger(__base_name__) -__default_log__.addHandler(logging.NullHandler()) - - -# TODO How hard would it be to support both `trio` and `asyncio`? -class TelegramBaseClient(abc.ABC): - """ - This is the abstract base class for the client. It defines some - basic stuff like connecting, switching data center, etc, and - leaves the `__call__` unimplemented. - - Arguments - session (`str` | `telethon.sessions.abstract.Session`, `None`): - The file name of the session file to be used if a string is - given (it may be a full path), or the Session instance to be - used otherwise. If it's ``None``, the session will not be saved, - and you should call :meth:`.log_out()` when you're done. - - Note that if you pass a string it will be a file in the current - working directory, although you can also pass absolute paths. - - The session file contains enough information for you to login - without re-sending the code, so if you have to enter the code - more than once, maybe you're changing the working directory, - renaming or removing the file, or using random names. - - api_id (`int` | `str`): - The API ID you obtained from https://my.telegram.org. - - api_hash (`str`): - The API ID you obtained from https://my.telegram.org. - - connection (`telethon.network.connection.common.Connection`, optional): - The connection instance to be used when creating a new connection - to the servers. It **must** be a type. - - Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. - - use_ipv6 (`bool`, optional): - Whether to connect to the servers through IPv6 or not. - By default this is ``False`` as IPv6 support is not - too widespread yet. - - proxy (`tuple` | `list` | `dict`, optional): - An iterable consisting of the proxy info. If `connection` is - one of `MTProxy`, then it should contain MTProxy credentials: - ``('hostname', port, 'secret')``. Otherwise, it's meant to store - function parameters for PySocks, like ``(type, 'hostname', port)``. - See https://github.com/Anorov/PySocks#usage-1 for more. - - timeout (`int` | `float`, optional): - The timeout in seconds to be used when connecting. - This is **not** the timeout to be used when ``await``'ing for - invoked requests, and you should use ``asyncio.wait`` or - ``asyncio.wait_for`` for that. - - request_retries (`int` | `None`, optional): - How many times a request should be retried. Request are retried - when Telegram is having internal issues (due to either - ``errors.ServerError`` or ``errors.RpcCallFailError``), - when there is a ``errors.FloodWaitError`` less than - `flood_sleep_threshold`, or when there's a migrate error. - - May take a negative or ``None`` value for infinite retries, but - this is not recommended, since some requests can always trigger - a call fail (such as searching for messages). - - connection_retries (`int` | `None`, optional): - How many times the reconnection should retry, either on the - initial connection or when Telegram disconnects us. May be - set to a negative or ``None`` value for infinite retries, but - this is not recommended, since the program can get stuck in an - infinite loop. - - retry_delay (`int` | `float`, optional): - The delay in seconds to sleep between automatic reconnections. - - auto_reconnect (`bool`, optional): - Whether reconnection should be retried `connection_retries` - times automatically if Telegram disconnects us or not. - - sequential_updates (`bool`, optional): - By default every incoming update will create a new task, so - you can handle several updates in parallel. Some scripts need - the order in which updates are processed to be sequential, and - this setting allows them to do so. - - If set to ``True``, incoming updates will be put in a queue - and processed sequentially. This means your event handlers - should *not* perform long-running operations since new - updates are put inside of an unbounded queue. - - flood_sleep_threshold (`int` | `float`, optional): - The threshold below which the library should automatically - sleep on flood wait errors (inclusive). For instance, if a - ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` - is 20s, the library will ``sleep`` automatically. If the error - was for 21s, it would ``raise FloodWaitError`` instead. Values - larger than a day (like ``float('inf')``) will be changed to a day. - - device_model (`str`, optional): - "Device model" to be sent when creating the initial connection. - Defaults to ``platform.node()``. - - system_version (`str`, optional): - "System version" to be sent when creating the initial connection. - Defaults to ``platform.system()``. - - app_version (`str`, optional): - "App version" to be sent when creating the initial connection. - Defaults to `telethon.version.__version__`. - - lang_code (`str`, optional): - "Language code" to be sent when creating the initial connection. - Defaults to ``'en'``. - - system_lang_code (`str`, optional): - "System lang code" to be sent when creating the initial connection. - Defaults to `lang_code`. - - loop (`asyncio.AbstractEventLoop`, optional): - Asyncio event loop to use. Defaults to `asyncio.get_event_loop()` - - base_logger (`str` | `logging.Logger`, optional): - Base logger name or instance to use. - If a `str` is given, it'll be passed to `logging.getLogger()`. If a - `logging.Logger` is given, it'll be used directly. If something - else or nothing is given, the default logger will be used. - """ - - # Current TelegramClient version - __version__ = version.__version__ - - # Cached server configuration (with .dc_options), can be "global" - _config = None - _cdn_config = None - - # region Initialization - - def __init__( - self: 'TelegramClient', - session: 'typing.Union[str, Session]', - api_id: int, - api_hash: str, - *, - connection: 'typing.Type[Connection]' = ConnectionTcpFull, - use_ipv6: bool = False, - proxy: typing.Union[tuple, dict] = None, - timeout: int = 10, - request_retries: int = 5, - connection_retries: int =5, - retry_delay: int = 1, - auto_reconnect: bool = True, - sequential_updates: bool = False, - flood_sleep_threshold: int = 60, - device_model: str = None, - system_version: str = None, - app_version: str = None, - lang_code: str = 'en', - system_lang_code: str = 'en', - loop: asyncio.AbstractEventLoop = None, - base_logger: typing.Union[str, logging.Logger] = None): - if not api_id or not api_hash: - raise ValueError( - "Your API ID or Hash cannot be empty or None. " - "Refer to telethon.rtfd.io for more information.") - - self._use_ipv6 = use_ipv6 - self._loop = loop or asyncio.get_event_loop() - - if isinstance(base_logger, str): - base_logger = logging.getLogger(base_logger) - elif not isinstance(base_logger, logging.Logger): - base_logger = __default_log__ - - class _Loggers(dict): - def __missing__(self, key): - if key.startswith("telethon."): - key = key.split('.', maxsplit=1)[1] - - return base_logger.getChild(key) - - self._log = _Loggers() - - # Determine what session object we have - if isinstance(session, str) or session is None: - try: - session = SQLiteSession(session) - except ImportError: - import warnings - warnings.warn( - 'The sqlite3 module is not available under this ' - 'Python installation and no custom session ' - 'instance was given; using MemorySession.\n' - 'You will need to re-login every time unless ' - 'you use another session storage' - ) - session = MemorySession() - elif not isinstance(session, Session): - raise TypeError( - 'The given session must be a str or a Session instance.' - ) - - # ':' in session.server_address is True if it's an IPv6 address - if (not session.server_address or - (':' in session.server_address) != use_ipv6): - session.set_dc( - DEFAULT_DC_ID, - DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP, - DEFAULT_PORT - ) - - self.flood_sleep_threshold = flood_sleep_threshold - - # TODO Use AsyncClassWrapper(session) - # ChatGetter and SenderGetter can use the in-memory _entity_cache - # to avoid network access and the need for await in session files. - # - # The session files only wants the entities to persist - # them to disk, and to save additional useful information. - # TODO Session should probably return all cached - # info of entities, not just the input versions - self.session = session - self._entity_cache = EntityCache() - self.api_id = int(api_id) - self.api_hash = api_hash - - self._request_retries = request_retries - self._connection_retries = connection_retries - self._retry_delay = retry_delay or 0 - self._proxy = proxy - self._timeout = timeout - self._auto_reconnect = auto_reconnect - - assert isinstance(connection, type) - self._connection = connection - init_proxy = None if not issubclass(connection, TcpMTProxy) else \ - types.InputClientProxy(*connection.address_info(proxy)) - - # Used on connection. Capture the variables in a lambda since - # exporting clients need to create this InvokeWithLayerRequest. - system = platform.uname() - self._init_with = lambda x: functions.InvokeWithLayerRequest( - LAYER, functions.InitConnectionRequest( - api_id=self.api_id, - device_model=device_model or system.system or 'Unknown', - system_version=system_version or system.release or '1.0', - app_version=app_version or self.__version__, - lang_code=lang_code, - system_lang_code=system_lang_code, - lang_pack='', # "langPacks are for official apps only" - query=x, - proxy=init_proxy - ) - ) - - self._sender = MTProtoSender( - self.session.auth_key, self._loop, - loggers=self._log, - retries=self._connection_retries, - delay=self._retry_delay, - auto_reconnect=self._auto_reconnect, - connect_timeout=self._timeout, - auth_key_callback=self._auth_key_callback, - update_callback=self._handle_update, - auto_reconnect_callback=self._handle_auto_reconnect - ) - - # Remember flood-waited requests to avoid making them again - self._flood_waited_requests = {} - - # Cache ``{dc_id: (n, MTProtoSender)}`` for all borrowed senders, - # being ``n`` the amount of borrows a given sender has; once ``n`` - # reaches ``0`` it should be disconnected and removed. - self._borrowed_senders = {} - self._borrow_sender_lock = asyncio.Lock(loop=self._loop) - - self._updates_handle = None - self._last_request = time.time() - self._channel_pts = {} - - if sequential_updates: - self._updates_queue = asyncio.Queue(loop=self._loop) - self._dispatching_updates_queue = asyncio.Event(loop=self._loop) - else: - # Use a set of pending instead of a queue so we can properly - # terminate all pending updates on disconnect. - self._updates_queue = set() - self._dispatching_updates_queue = None - - self._authorized = None # None = unknown, False = no, True = yes - - # Update state (for catching up after a disconnection) - # TODO Get state from channels too - self._state_cache = StateCache( - self.session.get_update_state(0), self._log) - - # Some further state for subclasses - self._event_builders = [] - - # {chat_id: {Conversation}} - self._conversations = collections.defaultdict(set) - - # Default parse mode - self._parse_mode = markdown - - # Some fields to easy signing in. Let {phone: hash} be - # a dictionary because the user may change their mind. - self._phone_code_hash = {} - self._phone = None - self._tos = None - - # Sometimes we need to know who we are, cache the self peer - self._self_input_peer = None - self._bot = None - - # endregion - - # region Properties - - @property - def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: - """ - Property with the ``asyncio`` event loop used by this client. - - Example - .. code-block:: python - - # Download media in the background - task = client.loop_create_task(message.download_media()) - - # Do some work - ... - - # Join the task (wait for it to complete) - await task - """ - return self._loop - - @property - def disconnected(self: 'TelegramClient') -> asyncio.Future: - """ - Property with a ``Future`` that resolves upon disconnection. - - Example - .. code-block:: python - - # Wait for a disconnection to occur - try: - await client.disconnected - except OSError: - print('Error on disconnect') - """ - return self._sender.disconnected - - # endregion - - # region Connecting - - async def connect(self: 'TelegramClient') -> None: - """ - Connects to Telegram. - - .. note:: - - Connect means connect and nothing else, and only one low-level - request is made to notify Telegram about which layer we will be - using. - - Before Telegram sends you updates, you need to make a high-level - request, like `client.get_me() `, - as described in https://core.telegram.org/api/updates. - - Example - .. code-block:: python - - try: - client.connect() - except OSError: - print('Failed to connect') - """ - await self._sender.connect(self._connection( - self.session.server_address, - self.session.port, - self.session.dc_id, - loop=self._loop, - loggers=self._log, - proxy=self._proxy - )) - self.session.auth_key = self._sender.auth_key - self.session.save() - - await self._sender.send(self._init_with( - functions.help.GetConfigRequest())) - - self._updates_handle = self._loop.create_task(self._update_loop()) - - def is_connected(self: 'TelegramClient') -> bool: - """ - Returns ``True`` if the user has connected. - - This method is **not** asynchronous (don't use ``await`` on it). - - Example - .. code-block:: python - - while client.is_connected(): - await asyncio.sleep(1) - """ - sender = getattr(self, '_sender', None) - return sender and sender.is_connected() - - def disconnect(self: 'TelegramClient'): - """ - Disconnects from Telegram. - - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. - - Example - .. code-block:: python - - # You don't need to use this if you used "with client" - client.disconnect() - """ - if self._loop.is_running(): - return self._disconnect_coro() - else: - try: - self._loop.run_until_complete(self._disconnect_coro()) - except RuntimeError: - # Python 3.5.x complains when called from - # `__aexit__` and there were pending updates with: - # "Event loop stopped before Future completed." - # - # However, it doesn't really make a lot of sense. - pass - - async def _disconnect_coro(self: 'TelegramClient'): - await self._disconnect() - - # trio's nurseries would handle this for us, but this is asyncio. - # All tasks spawned in the background should properly be terminated. - if self._dispatching_updates_queue is None and self._updates_queue: - for task in self._updates_queue: - task.cancel() - - await asyncio.wait(self._updates_queue, loop=self._loop) - self._updates_queue.clear() - - pts, date = self._state_cache[None] - if pts and date: - self.session.set_update_state(0, types.updates.State( - pts=pts, - qts=0, - date=date, - seq=0, - unread_count=0 - )) - - self.session.close() - - async def _disconnect(self: 'TelegramClient'): - """ - Disconnect only, without closing the session. Used in reconnections - to different data centers, where we don't want to close the session - file; user disconnects however should close it since it means that - their job with the client is complete and we should clean it up all. - """ - await self._sender.disconnect() - await helpers._cancel(self._log[__name__], - updates_handle=self._updates_handle) - - async def _switch_dc(self: 'TelegramClient', new_dc): - """ - Permanently switches the current connection to the new data center. - """ - self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await self._get_dc(new_dc) - - self.session.set_dc(dc.id, dc.ip_address, dc.port) - # auth_key's are associated with a server, which has now changed - # so it's not valid anymore. Set to None to force recreating it. - self._sender.auth_key.key = None - self.session.auth_key = None - self.session.save() - await self._disconnect() - return await self.connect() - - def _auth_key_callback(self: 'TelegramClient', auth_key): - """ - Callback from the sender whenever it needed to generate a - new authorization key. This means we are not authorized. - """ - self.session.auth_key = auth_key - self.session.save() - - # endregion - - # region Working with different connections/Data Centers - - async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): - """Gets the Data Center (DC) associated to 'dc_id'""" - cls = self.__class__ - if not cls._config: - cls._config = await self(functions.help.GetConfigRequest()) - - if cdn and not self._cdn_config: - cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) - for pk in cls._cdn_config.public_keys: - rsa.add_key(pk.public_key) - - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id - and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn - ) - - async def _create_exported_sender(self: 'TelegramClient', dc_id): - """ - Creates a new exported `MTProtoSender` for the given `dc_id` and - returns it. This method should be used by `_borrow_exported_sender`. - """ - # Thanks badoualy/kotlogram on /telegram/api/DefaultTelegramClient.kt - # for clearly showing how to export the authorization - dc = await self._get_dc(dc_id) - # Can't reuse self._sender._connection as it has its own seqno. - # - # If one were to do that, Telegram would reset the connection - # with no further clues. - sender = MTProtoSender(None, self._loop, loggers=self._log) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loop=self._loop, - loggers=self._log, - proxy=self._proxy - )) - self._log[__name__].info('Exporting authorization for data center %s', - dc) - auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) - req = self._init_with(functions.auth.ImportAuthorizationRequest( - id=auth.id, bytes=auth.bytes - )) - await sender.send(req) - return sender - - async def _borrow_exported_sender(self: 'TelegramClient', dc_id): - """ - Borrows a connected `MTProtoSender` for the given `dc_id`. - If it's not cached, creates a new one if it doesn't exist yet, - and imports a freshly exported authorization key for it to be usable. - - Once its job is over it should be `_return_exported_sender`. - """ - async with self._borrow_sender_lock: - n, sender = self._borrowed_senders.get(dc_id, (0, None)) - if not sender: - sender = await self._create_exported_sender(dc_id) - sender.dc_id = dc_id - elif not n: - dc = await self._get_dc(dc_id) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loop=self._loop, - loggers=self._log, - proxy=self._proxy - )) - - self._borrowed_senders[dc_id] = (n + 1, sender) - - return sender - - async def _return_exported_sender(self: 'TelegramClient', sender): - """ - Returns a borrowed exported sender. If all borrows have - been returned, the sender is cleanly disconnected. - """ - async with self._borrow_sender_lock: - dc_id = sender.dc_id - n, _ = self._borrowed_senders[dc_id] - n -= 1 - self._borrowed_senders[dc_id] = (n, sender) - if not n: - self._log[__name__].info( - 'Disconnecting borrowed sender for DC %d', dc_id) - await sender.disconnect() - - async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): - """Similar to ._borrow_exported_client, but for CDNs""" - # TODO Implement - raise NotImplementedError - session = self._exported_sessions.get(cdn_redirect.dc_id) - if not session: - dc = await self._get_dc(cdn_redirect.dc_id, cdn=True) - session = self.session.clone() - await session.set_dc(dc.id, dc.ip_address, dc.port) - self._exported_sessions[cdn_redirect.dc_id] = session - - self._log[__name__].info('Creating new CDN client') - client = TelegramBareClient( - session, self.api_id, self.api_hash, - proxy=self._sender.connection.conn.proxy, - timeout=self._sender.connection.get_timeout() - ) - - # This will make use of the new RSA keys for this specific CDN. - # - # We won't be calling GetConfigRequest because it's only called - # when needed by ._get_dc, and also it's static so it's likely - # set already. Avoid invoking non-CDN methods by not syncing updates. - client.connect(_sync_updates=False) - return client - - # endregion - - # region Invoking Telegram requests - - @abc.abstractmethod - def __call__(self: 'TelegramClient', request, ordered=False): - """ - Invokes (sends) one or more MTProtoRequests and returns (receives) - their result. - - Args: - request (`TLObject` | `list`): - The request or requests to be invoked. - - ordered (`bool`, optional): - Whether the requests (if more than one was given) should be - executed sequentially on the server. They run in arbitrary - order by default. - - Returns: - The result of the request (often a `TLObject`) or a list of - results if more than one request was given. - """ - raise NotImplementedError - - @abc.abstractmethod - def _handle_update(self: 'TelegramClient', update): - raise NotImplementedError - - @abc.abstractmethod - def _update_loop(self: 'TelegramClient'): - raise NotImplementedError - - @abc.abstractmethod - async def _handle_auto_reconnect(self: 'TelegramClient'): - raise NotImplementedError - - # endregion diff --git a/telethon/client/telegramclient.py b/telethon/client/telegramclient.py index 144a6b2f..e69de29b 100644 --- a/telethon/client/telegramclient.py +++ b/telethon/client/telegramclient.py @@ -1,13 +0,0 @@ -from . import ( - AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, - BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, - MessageParseMethods, UserMethods, TelegramBaseClient -) - - -class TelegramClient( - AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, - BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, - MessageParseMethods, UserMethods, TelegramBaseClient -): - pass diff --git a/telethon/client/updates.py b/telethon/client/updates.py index 17fc82ac..e69de29b 100644 --- a/telethon/client/updates.py +++ b/telethon/client/updates.py @@ -1,545 +0,0 @@ -import asyncio -import itertools -import random -import time -import typing - -from .. import events, utils, errors -from ..events.common import EventBuilder, EventCommon -from ..tl import types, functions - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class UpdateMethods: - - # region Public methods - - async def _run_until_disconnected(self: 'TelegramClient'): - try: - # Make a high-level request to notify that we want updates - await self(functions.updates.GetStateRequest()) - return await self.disconnected - except KeyboardInterrupt: - pass - finally: - await self.disconnect() - - def run_until_disconnected(self: 'TelegramClient'): - """ - Runs the event loop until the library is disconnected. - - It also notifies Telegram that we want to receive updates - as described in https://core.telegram.org/api/updates. - - Manual disconnections can be made by calling `disconnect() - ` - or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on - the console window running the script). - - If a disconnection error occurs (i.e. the library fails to reconnect - automatically), said error will be raised through here, so you have a - chance to ``except`` it on your own code. - - If the loop is already running, this method returns a coroutine - that you should await on your own code. - - .. note:: - - If you want to handle ``KeyboardInterrupt`` in your code, - simply run the event loop in your code too in any way, such as - ``loop.run_forever()`` or ``await client.disconnected`` (e.g. - ``loop.run_until_complete(client.disconnected)``). - - Example - .. code-block:: python - - # Blocks the current task here until a disconnection occurs. - # - # You will still receive updates, since this prevents the - # script from exiting. - client.run_until_disconnected() - """ - if self.loop.is_running(): - return self._run_until_disconnected() - try: - return self.loop.run_until_complete(self._run_until_disconnected()) - except KeyboardInterrupt: - pass - finally: - # No loop.run_until_complete; it's already syncified - self.disconnect() - - def on(self: 'TelegramClient', event: EventBuilder): - """ - Decorator used to `add_event_handler` more conveniently. - - - Arguments - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - - Example - .. code-block:: python - - from telethon import TelegramClient, events - client = TelegramClient(...) - - # Here we use client.on - @client.on(events.NewMessage) - async def handler(event): - ... - """ - def decorator(f): - self.add_event_handler(f, event) - return f - - return decorator - - def add_event_handler( - self: 'TelegramClient', - callback: callable, - event: EventBuilder = None): - """ - Registers a new event handler callback. - - The callback will be called when the specified event occurs. - - Arguments - callback (`callable`): - The callable function accepting one parameter to be used. - - Note that if you have used `telethon.events.register` in - the callback, ``event`` will be ignored, and instead the - events you previously registered will be used. - - event (`_EventBuilder` | `type`, optional): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - - If left unspecified, `telethon.events.raw.Raw` (the - :tl:`Update` objects with no further processing) will - be passed instead. - - Example - .. code-block:: python - - from telethon import TelegramClient, events - client = TelegramClient(...) - - async def handler(event): - ... - - client.add_event_handler(handler, events.NewMessage) - """ - builders = events._get_handlers(callback) - if builders is not None: - for event in builders: - self._event_builders.append((event, callback)) - return - - if isinstance(event, type): - event = event() - elif not event: - event = events.Raw() - - self._event_builders.append((event, callback)) - - def remove_event_handler( - self: 'TelegramClient', - callback: callable, - event: EventBuilder = None) -> int: - """ - Inverse operation of `add_event_handler()`. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - - Example - .. code-block:: python - - @client.on(events.Raw) - @client.on(events.NewMessage) - async def handler(event): - ... - - # Removes only the "Raw" handling - # "handler" will still receive "events.NewMessage" - client.remove_event_handler(handler, events.Raw) - - # "handler" will stop receiving anything - client.remove_event_handler(handler) - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - i = len(self._event_builders) - while i: - i -= 1 - ev, cb = self._event_builders[i] - if cb == callback and (not event or isinstance(ev, event)): - del self._event_builders[i] - found += 1 - - return found - - def list_event_handlers(self: 'TelegramClient')\ - -> 'typing.Sequence[typing.Tuple[callable, EventBuilder]]': - """ - Lists all registered event handlers. - - Returns - A list of pairs consisting of ``(callback, event)``. - - Example - .. code-block:: python - - @client.on(events.NewMessage(pattern='hello')) - async def on_greeting(event): - '''Greets someone''' - await event.reply('Hi') - - for callback, event in client.list_event_handlers(): - print(id(callback), type(event)) - """ - return [(callback, event) for event, callback in self._event_builders] - - async def catch_up(self: 'TelegramClient'): - """ - "Catches up" on the missed updates while the client was offline. - You should call this method after registering the event handlers - so that the updates it loads can by processed by your script. - - This can also be used to forcibly fetch new updates if there are any. - - Example - .. code-block:: python - - client.catch_up() - """ - pts, date = self._state_cache[None] - if not pts: - return - - self.session.catching_up = True - try: - while True: - d = await self(functions.updates.GetDifferenceRequest( - pts, date, 0 - )) - if isinstance(d, (types.updates.DifferenceSlice, - types.updates.Difference)): - if isinstance(d, types.updates.Difference): - state = d.state - else: - state = d.intermediate_state - - pts, date = state.pts, state.date - self._handle_update(types.Updates( - users=d.users, - chats=d.chats, - date=state.date, - seq=state.seq, - updates=d.other_updates + [ - types.UpdateNewMessage(m, 0, 0) - for m in d.new_messages - ] - )) - - # TODO Implement upper limit (max_pts) - # We don't want to fetch updates we already know about. - # - # We may still get duplicates because the Difference - # contains a lot of updates and presumably only has - # the state for the last one, but at least we don't - # unnecessarily fetch too many. - # - # updates.getDifference's pts_total_limit seems to mean - # "how many pts is the request allowed to return", and - # if there is more than that, it returns "too long" (so - # there would be duplicate updates since we know about - # some). This can be used to detect collisions (i.e. - # it would return an update we have already seen). - else: - if isinstance(d, types.updates.DifferenceEmpty): - date = d.date - elif isinstance(d, types.updates.DifferenceTooLong): - pts = d.pts - break - except (ConnectionError, asyncio.CancelledError): - pass - finally: - # TODO Save new pts to session - self._state_cache._pts_date = (pts, date) - self.session.catching_up = False - - # endregion - - # region Private methods - - # It is important to not make _handle_update async because we rely on - # the order that the updates arrive in to update the pts and date to - # be always-increasing. There is also no need to make this async. - def _handle_update(self: 'TelegramClient', update): - self.session.process_entities(update) - self._entity_cache.add(update) - - if isinstance(update, (types.Updates, types.UpdatesCombined)): - entities = {utils.get_peer_id(x): x for x in - itertools.chain(update.users, update.chats)} - for u in update.updates: - self._process_update(u, entities) - elif isinstance(update, types.UpdateShort): - self._process_update(update.update) - else: - self._process_update(update) - - self._state_cache.update(update) - - def _process_update(self: 'TelegramClient', update, entities=None): - update._entities = entities or {} - - # This part is somewhat hot so we don't bother patching - # update with channel ID/its state. Instead we just pass - # arguments which is faster. - channel_id = self._state_cache.get_channel_id(update) - args = (update, channel_id, self._state_cache[channel_id]) - if self._dispatching_updates_queue is None: - task = self._loop.create_task(self._dispatch_update(*args)) - self._updates_queue.add(task) - task.add_done_callback(lambda _: self._updates_queue.discard(task)) - else: - self._updates_queue.put_nowait(args) - if not self._dispatching_updates_queue.is_set(): - self._dispatching_updates_queue.set() - self._loop.create_task(self._dispatch_queue_updates()) - - self._state_cache.update(update) - - async def _update_loop(self: 'TelegramClient'): - # Pings' ID don't really need to be secure, just "random" - rnd = lambda: random.randrange(-2**63, 2**63) - while self.is_connected(): - try: - await asyncio.wait_for( - self.disconnected, timeout=60, loop=self._loop - ) - continue # We actually just want to act upon timeout - except asyncio.TimeoutError: - pass - except asyncio.CancelledError: - return - except Exception: - continue # Any disconnected exception should be ignored - - # We also don't really care about their result. - # Just send them periodically. - try: - self._sender.send(functions.PingRequest(rnd())) - except (ConnectionError, asyncio.CancelledError): - return - - # Entities and cached files are not saved when they are - # inserted because this is a rather expensive operation - # (default's sqlite3 takes ~0.1s to commit changes). Do - # it every minute instead. No-op if there's nothing new. - self.session.save() - - # We need to send some content-related request at least hourly - # for Telegram to keep delivering updates, otherwise they will - # just stop even if we're connected. Do so every 30 minutes. - # - # TODO Call getDifference instead since it's more relevant - if time.time() - self._last_request > 30 * 60: - if not await self.is_user_authorized(): - # What can be the user doing for so - # long without being logged in...? - continue - - try: - await self(functions.updates.GetStateRequest()) - except (ConnectionError, asyncio.CancelledError): - return - - async def _dispatch_queue_updates(self: 'TelegramClient'): - while not self._updates_queue.empty(): - await self._dispatch_update(*self._updates_queue.get_nowait()) - - self._dispatching_updates_queue.clear() - - async def _dispatch_update(self: 'TelegramClient', update, channel_id, pts_date): - if not self._entity_cache.ensure_cached(update): - # We could add a lock to not fetch the same pts twice if we are - # already fetching it. However this does not happen in practice, - # which makes sense, because different updates have different pts. - if self._state_cache.update(update, check_only=True): - # If the update doesn't have pts, fetching won't do anything. - # For example, UpdateUserStatus or UpdateChatUserTyping. - await self._get_difference(update, channel_id, pts_date) - - built = EventBuilderDict(self, update) - for conv_set in self._conversations.values(): - for conv in conv_set: - ev = built[events.NewMessage] - if ev: - conv._on_new_message(ev) - - ev = built[events.MessageEdited] - if ev: - conv._on_edit(ev) - - ev = built[events.MessageRead] - if ev: - conv._on_read(ev) - - if conv._custom: - await conv._check_custom(built) - - for builder, callback in self._event_builders: - event = built[type(builder)] - if not event: - continue - - if not builder.resolved: - await builder.resolve(self) - - if not builder.filter(event): - continue - - try: - await callback(event) - except errors.AlreadyInConversationError: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" already has an open conversation, ' - 'ignoring new one', name) - except events.StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', - name) - - async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date): - """ - Get the difference for this `channel_id` if any, then load entities. - - Calls :tl:`updates.getDifference`, which fills the entities cache - (always done by `__call__`) and lets us know about the full entities. - """ - # Fetch since the last known pts/date before this update arrived, - # in order to fetch this update at full, including its entities. - self._log[__name__].debug('Getting difference for entities ' - 'for %r', update.__class__) - if channel_id: - try: - where = await self.get_input_entity(channel_id) - except ValueError: - # There's a high chance that this fails, since - # we are getting the difference to fetch entities. - return - - if not pts_date: - # First-time, can't get difference. Get pts instead. - result = await self(functions.messages.GetPeerDialogsRequest([ - utils.get_input_dialog(where) - ])) - self._state_cache[channel_id] = result.dialogs[0].pts - return - - result = await self(functions.updates.GetChannelDifferenceRequest( - channel=where, - filter=types.ChannelMessagesFilterEmpty(), - pts=pts_date, # just pts - limit=100, - force=True - )) - else: - if not pts_date[0]: - # First-time, can't get difference. Get pts instead. - result = await self(functions.updates.GetStateRequest()) - self._state_cache[None] = result.pts, result.date - return - - result = await self(functions.updates.GetDifferenceRequest( - pts=pts_date[0], - date=pts_date[1], - qts=0 - )) - - if isinstance(result, (types.updates.Difference, - types.updates.DifferenceSlice, - types.updates.ChannelDifference, - types.updates.ChannelDifferenceTooLong)): - update._entities.update({ - utils.get_peer_id(x): x for x in - itertools.chain(result.users, result.chats) - }) - - async def _handle_auto_reconnect(self: 'TelegramClient'): - # TODO Catch-up - return - try: - self._log[__name__].info( - 'Asking for the current state after reconnect...') - - # TODO consider: - # If there aren't many updates while the client is disconnected - # (I tried with up to 20), Telegram seems to send them without - # asking for them (via updates.getDifference). - # - # On disconnection, the library should probably set a "need - # difference" or "catching up" flag so that any new updates are - # ignored, and then the library should call updates.getDifference - # itself to fetch them. - # - # In any case (either there are too many updates and Telegram - # didn't send them, or there isn't a lot and Telegram sent them - # but we dropped them), we fetch the new difference to get all - # missed updates. I feel like this would be the best solution. - - # If a disconnection occurs, the old known state will be - # the latest one we were aware of, so we can catch up since - # the most recent state we were aware of. - await self.catch_up() - - self._log[__name__].info('Successfully fetched missed updates') - except errors.RPCError as e: - self._log[__name__].warning('Failed to get missed updates after ' - 'reconnect: %r', e) - except Exception: - self._log[__name__].exception('Unhandled exception while getting ' - 'update difference after reconnect') - - # endregion - - -class EventBuilderDict: - """ - Helper "dictionary" to return events from types and cache them. - """ - def __init__(self, client: 'TelegramClient', update): - self.client = client - self.update = update - - def __getitem__(self, builder): - try: - return self.__dict__[builder] - except KeyError: - event = self.__dict__[builder] = builder.build(self.update) - if isinstance(event, EventCommon): - event.original_update = self.update - event._set_client(self.client) - elif event: - event._client = self.client - - return event diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py index a405cccc..e69de29b 100644 --- a/telethon/client/uploads.py +++ b/telethon/client/uploads.py @@ -1,697 +0,0 @@ -import hashlib -import io -import itertools -import os -import pathlib -import re -import typing -from io import BytesIO - -from .. import utils, helpers, hints -from ..tl import types, functions, custom - -try: - import PIL - import PIL.Image -except ImportError: - PIL = None - - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class _CacheType: - """Like functools.partial but pretends to be the wrapped class.""" - def __init__(self, cls): - self._cls = cls - - def __call__(self, *args, **kwargs): - return self._cls(*args, file_reference=b'', **kwargs) - - def __eq__(self, other): - return self._cls == other - - -def _resize_photo_if_needed( - file, is_image, width=1280, height=1280, background=(255, 255, 255)): - - # https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254 - if (not is_image - or PIL is None - or (isinstance(file, io.IOBase) and not file.seekable())): - return file - - if isinstance(file, bytes): - file = io.BytesIO(file) - - before = file.tell() if isinstance(file, io.IOBase) else None - - try: - # Don't use a `with` block for `image`, or `file` would be closed. - # See https://github.com/LonamiWebs/Telethon/issues/1121 for more. - image = PIL.Image.open(file) - if image.width <= width and image.height <= height: - return file - - image.thumbnail((width, height), PIL.Image.ANTIALIAS) - - alpha_index = image.mode.find('A') - if alpha_index == -1: - # If the image mode doesn't have alpha - # channel then don't bother masking it away. - result = image - else: - # We could save the resized image with the original format, but - # JPEG often compresses better -> smaller size -> faster upload - # We need to mask away the alpha channel ([3]), since otherwise - # IOError is raised when trying to save alpha channels in JPEG. - result = PIL.Image.new('RGB', image.size, background) - result.paste(image, mask=image.split()[alpha_index]) - - buffer = io.BytesIO() - result.save(buffer, 'JPEG') - buffer.seek(0) - return buffer - - except IOError: - return file - finally: - if before is not None: - file.seek(before, io.SEEK_SET) - - -class UploadMethods: - - # region Public methods - - async def send_file( - self: 'TelegramClient', - entity: 'hints.EntityLike', - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]', - *, - caption: typing.Union[str, typing.Sequence[str]] = None, - force_document: bool = False, - progress_callback: 'hints.ProgressCallback' = None, - reply_to: 'hints.MessageIDLike' = None, - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - thumb: 'hints.FileLike' = None, - allow_cache: bool = True, - parse_mode: str = (), - voice_note: bool = False, - video_note: bool = False, - buttons: 'hints.MarkupLike' = None, - silent: bool = None, - supports_streaming: bool = False, - **kwargs) -> 'types.Message': - """ - Sends message with the given file to the specified entity. - - .. note:: - - If the ``hachoir3`` package (``hachoir`` module) is installed, - it will be used to determine metadata from audio and video files. - - If the ``pillow`` package is installed and you are sending a photo, - it will be resized to fit within the maximum dimensions allowed - by Telegram to avoid ``errors.PhotoInvalidDimensionsError``. This - cannot be done if you are sending :tl:`InputFile`, however. - - Arguments - entity (`entity`): - Who will receive the file. - - file (`str` | `bytes` | `file` | `media`): - The file to send, which can be one of: - - * A local file path to an in-disk file. The file name - will be the path's base name. - - * A `bytes` byte array with the file's data to send - (for example, by using ``text.encode('utf-8')``). - A default file name will be used. - - * A bytes `io.IOBase` stream over the file to send - (for example, by using ``open(file, 'rb')``). - Its ``.name`` property will be used for the file name, - or a default if it doesn't have one. - - * An external URL to a file over the internet. This will - send the file as "external" media, and Telegram is the - one that will fetch the media and send it. - - * A Bot API-like ``file_id``. You can convert previously - sent media to file IDs for later reusing with - `telethon.utils.pack_bot_file_id`. - - * A handle to an existing file (for example, if you sent a - message with media before, you can use its ``message.media`` - as a file here). - - * A handle to an uploaded file (from `upload_file`). - - To send an album, you should provide a list in this parameter. - - If a list or similar is provided, the files in it will be - sent as an album in the order in which they appear, sliced - in chunks of 10 if more than 10 are given. - - caption (`str`, optional): - Optional caption for the sent media message. When sending an - album, the caption may be a list of strings, which will be - assigned to the files pairwise. - - force_document (`bool`, optional): - If left to ``False`` and the file is a path that ends with - the extension of an image file or a video file, it will be - sent as such. Otherwise always as a document. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - reply_to (`int` | `Message `): - Same as `reply_to` from `send_message`. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - - The file must also be small in dimensions and in-disk size. - Successful thumbnails were files below 20kb and 200x200px. - Width/height and dimensions/size ratios may be important. - - allow_cache (`bool`, optional): - Whether to allow using the cached version stored in the - database or not. Defaults to ``True`` to avoid re-uploads. - Must be ``False`` if you wish to use different attributes - or thumb than those that were used when the file was cached. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - voice_note (`bool`, optional): - If ``True`` the audio will be sent as a voice note. - - Set `allow_cache` to ``False`` if you sent the same file - without this setting before for it to work. - - video_note (`bool`, optional): - If ``True`` the video will be sent as a video note, - also known as a round video message. - - Set `allow_cache` to ``False`` if you sent the same file - without this setting before for it to work. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - silent (`bool`, optional): - Whether the message should notify people in a broadcast - channel or not. Defaults to ``False``, which means it will - notify them. Set it to ``True`` to alter this behaviour. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - Returns - The `Message ` (or messages) - containing the sent file, or messages if a list of them was passed. - - Example - .. code-block:: python - - # Normal files like photos - client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") - # or - client.send_message(chat, "It's me!", file='/my/photos/me.jpg') - - # Voice notes or round videos - client.send_file(chat, '/my/songs/song.mp3', voice_note=True) - client.send_file(chat, '/my/videos/video.mp4', video_note=True) - - # Custom thumbnails - client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') - - # Only documents - client.send_file(chat, '/my/photos/photo.png', force_document=True) - - # Albums - client.send_file(chat, [ - '/my/photos/holiday1.jpg', - '/my/photos/holiday2.jpg', - '/my/drawings/portrait.png' - ]) - """ - # i.e. ``None`` was used - if not file: - raise TypeError('Cannot use {!r} as file'.format(file)) - - if not caption: - caption = '' - - # First check if the user passed an iterable, in which case - # we may want to send as an album if all are photo files. - if utils.is_list_like(file): - image_captions = [] - document_captions = [] - if utils.is_list_like(caption): - captions = caption - else: - captions = [caption] - - # TODO Fix progress_callback - images = [] - if force_document: - documents = file - else: - documents = [] - for doc, cap in itertools.zip_longest(file, captions): - if utils.is_image(doc): - images.append(doc) - image_captions.append(cap) - else: - documents.append(doc) - document_captions.append(cap) - - result = [] - while images: - result += await self._send_album( - entity, images[:10], caption=image_captions[:10], - progress_callback=progress_callback, reply_to=reply_to, - parse_mode=parse_mode, silent=silent - ) - images = images[10:] - image_captions = image_captions[10:] - - for doc, cap in zip(documents, captions): - result.append(await self.send_file( - entity, doc, allow_cache=allow_cache, - caption=cap, force_document=force_document, - progress_callback=progress_callback, reply_to=reply_to, - attributes=attributes, thumb=thumb, voice_note=voice_note, - video_note=video_note, buttons=buttons, silent=silent, - supports_streaming=supports_streaming, - **kwargs - )) - - return result - - entity = await self.get_input_entity(entity) - reply_to = utils.get_message_id(reply_to) - - # Not document since it's subject to change. - # Needed when a Message is passed to send_message and it has media. - if 'entities' in kwargs: - msg_entities = kwargs['entities'] - else: - caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) - - file_handle, media, image = await self._file_to_media( - file, force_document=force_document, - progress_callback=progress_callback, - attributes=attributes, allow_cache=allow_cache, thumb=thumb, - voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming - ) - - # e.g. invalid cast from :tl:`MessageMediaWebPage` - if not media: - raise TypeError('Cannot use {!r} as file'.format(file)) - - markup = self.build_reply_markup(buttons) - request = functions.messages.SendMediaRequest( - entity, media, reply_to_msg_id=reply_to, message=caption, - entities=msg_entities, reply_markup=markup, silent=silent - ) - msg = self._get_response_message(request, await self(request), entity) - await self._cache_media(msg, file, file_handle, image=image) - - return msg - - async def _send_album(self: 'TelegramClient', entity, files, caption='', - progress_callback=None, reply_to=None, - parse_mode=(), silent=None): - """Specialized version of .send_file for albums""" - # We don't care if the user wants to avoid cache, we will use it - # anyway. Why? The cached version will be exactly the same thing - # we need to produce right now to send albums (uploadMedia), and - # cache only makes a difference for documents where the user may - # want the attributes used on them to change. - # - # In theory documents can be sent inside the albums but they appear - # as different messages (not inside the album), and the logic to set - # the attributes/avoid cache is already written in .send_file(). - entity = await self.get_input_entity(entity) - if not utils.is_list_like(caption): - caption = (caption,) - - captions = [] - for c in reversed(caption): # Pop from the end (so reverse) - captions.append(await self._parse_message_text(c or '', parse_mode)) - - reply_to = utils.get_message_id(reply_to) - - # Need to upload the media first, but only if they're not cached yet - media = [] - for file in files: - # Albums want :tl:`InputMedia` which, in theory, includes - # :tl:`InputMediaUploadedPhoto`. However using that will - # make it `raise MediaInvalidError`, so we need to upload - # it as media and then convert that to :tl:`InputMediaPhoto`. - fh, fm, _ = await self._file_to_media(file) - if isinstance(fm, types.InputMediaUploadedPhoto): - r = await self(functions.messages.UploadMediaRequest( - entity, media=fm - )) - self.session.cache_file( - fh.md5, fh.size, utils.get_input_photo(r.photo)) - - fm = utils.get_input_media(r.photo) - - if captions: - caption, msg_entities = captions.pop() - else: - caption, msg_entities = '', None - media.append(types.InputSingleMedia( - fm, - message=caption, - entities=msg_entities - )) - - # Now we can construct the multi-media request - result = await self(functions.messages.SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=media, silent=silent - )) - - # We never sent a `random_id` for the messages that resulted from - # the request so we can't pair them up with the `Updates` that we - # get from Telegram. However, the sent messages have a photo and - # the photo IDs match with those we did send. - # - # Updates -> {_: message} - messages = self._get_response_message(None, result, entity) - # {_: message} -> {photo ID: message} - messages = {m.photo.id: m for m in messages.values()} - # Sent photo IDs -> messages - return [messages[m.media.id.id] for m in media] - - async def upload_file( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - part_size_kb: float = None, - file_name: str = None, - use_cache: type = None, - progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': - """ - Uploads a file to Telegram's servers, without sending it. - - This method returns a handle (an instance of :tl:`InputFile` or - :tl:`InputFileBig`, as required) which can be later used before - it expires (they are usable during less than a day). - - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will **not** upload the file to your own chat or any chat at all. - - Arguments - file (`str` | `bytes` | `file`): - The path of the file, byte array, or stream that will be sent. - Note that if a byte array or a stream is given, a filename - or its type won't be inferred, and it will be sent as an - "unnamed application/octet-stream". - - part_size_kb (`int`, optional): - Chunk size when uploading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_name (`str`, optional): - The file name which will be used on the resulting InputFile. - If not specified, the name will be taken from the ``file`` - and if this is not a ``str``, it will be ``"unnamed"``. - - use_cache (`type`, optional): - The type of cache to use (currently either :tl:`InputDocument` - or :tl:`InputPhoto`). If present and the file is small enough - to need the MD5, it will be checked against the database, - and if a match is found, the upload won't be made. Instead, - an instance of type ``use_cache`` will be returned. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - Returns - :tl:`InputFileBig` if the file size is larger than 10MB, - `InputSizedFile ` - (subclass of :tl:`InputFile`) otherwise. - - Example - .. code-block:: python - - # Photos as photo and document - file = client.upload_file('photo.jpg') - client.send_file(chat, file) # sends as photo - client.send_file(chat, file, force_document=True) # sends as document - - file.name = 'not a photo.jpg' - client.send_file(chat, file, force_document=True) # document, new name - - # As song or as voice note - file = client.upload_file('song.ogg') - client.send_file(chat, file) # sends as song - client.send_file(chat, file, voice_note=True) # sends as voice note - """ - if isinstance(file, (types.InputFile, types.InputFileBig)): - return file # Already uploaded - - if not file_name and getattr(file, 'name', None): - file_name = file.name - - if isinstance(file, str): - file_size = os.path.getsize(file) - elif isinstance(file, bytes): - file_size = len(file) - else: - if isinstance(file, io.IOBase) and file.seekable(): - pos = file.tell() - else: - pos = None - - # TODO Don't load the entire file in memory always - data = file.read() - if pos is not None: - file.seek(pos) - - file = data - file_size = len(file) - - # File will now either be a string or bytes - if not part_size_kb: - part_size_kb = utils.get_appropriated_part_size(file_size) - - if part_size_kb > 512: - raise ValueError('The part size must be less or equal to 512KB') - - part_size = int(part_size_kb * 1024) - if part_size % 1024 != 0: - raise ValueError( - 'The part size must be evenly divisible by 1024') - - # Set a default file name if None was specified - file_id = helpers.generate_random_long() - if not file_name: - if isinstance(file, str): - file_name = os.path.basename(file) - else: - file_name = str(file_id) - - # If the file name lacks extension, add it if possible. - # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` - # even if the uploaded image is indeed a photo. - if not os.path.splitext(file_name)[-1]: - file_name += utils._get_extension(file) - - # Determine whether the file is too big (over 10MB) or not - # Telegram does make a distinction between smaller or larger files - is_large = file_size > 10 * 1024 * 1024 - hash_md5 = hashlib.md5() - if not is_large: - # Calculate the MD5 hash before anything else. - # As this needs to be done always for small files, - # might as well do it before anything else and - # check the cache. - if isinstance(file, str): - with open(file, 'rb') as stream: - file = stream.read() - hash_md5.update(file) - if use_cache: - cached = self.session.get_file( - hash_md5.digest(), file_size, cls=_CacheType(use_cache) - ) - if cached: - return cached - - part_count = (file_size + part_size - 1) // part_size - self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', - file_size, part_count, part_size) - - with open(file, 'rb') if isinstance(file, str) else BytesIO(file)\ - as stream: - for part_index in range(part_count): - # Read the file by in chunks of size part_size - part = stream.read(part_size) - - # The SavePartRequest is different depending on whether - # the file is too large or not (over or less than 10MB) - if is_large: - request = functions.upload.SaveBigFilePartRequest( - file_id, part_index, part_count, part) - else: - request = functions.upload.SaveFilePartRequest( - file_id, part_index, part) - - result = await self(request) - if result: - self._log[__name__].debug('Uploaded %d/%d', - part_index + 1, part_count) - if progress_callback: - progress_callback(stream.tell(), file_size) - else: - raise RuntimeError( - 'Failed to upload file part {}.'.format(part_index)) - - if is_large: - return types.InputFileBig(file_id, part_count, file_name) - else: - return custom.InputSizedFile( - file_id, part_count, file_name, md5=hash_md5, size=file_size - ) - - # endregion - - async def _file_to_media( - self, file, force_document=False, - progress_callback=None, attributes=None, thumb=None, - allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None): - if not file: - return None, None, None - - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - if as_image is None: - as_image = utils.is_image(file) and not force_document - - if not isinstance(file, (str, bytes, io.IOBase)): - # The user may pass a Message containing media (or the media, - # or anything similar) that should be treated as a file. Try - # getting the input media for whatever they passed and send it. - # - # We pass all attributes since these will be used if the user - # passed :tl:`InputFile`, and all information may be relevant. - try: - return (None, utils.get_input_media( - file, - is_photo=as_image, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming - ), as_image) - except TypeError: - # Can't turn whatever was given into media - return None, None, as_image - - media = None - file_handle = None - use_cache = types.InputPhoto if as_image else types.InputDocument - if not isinstance(file, str) or os.path.isfile(file): - file_handle = await self.upload_file( - _resize_photo_if_needed(file, as_image), - progress_callback=progress_callback, - use_cache=use_cache if allow_cache else None - ) - elif re.match('https?://', file): - if as_image: - media = types.InputMediaPhotoExternal(file) - elif not force_document and utils.is_gif(file): - media = types.InputMediaGifExternal(file, '') - else: - media = types.InputMediaDocumentExternal(file) - else: - bot_file = utils.resolve_bot_file_id(file) - if bot_file: - media = utils.get_input_media(bot_file) - - if media: - pass # Already have media, don't check the rest - elif not file_handle: - raise ValueError( - 'Failed to convert {} to media. Not an existing file, ' - 'an HTTP URL or a valid bot-API-like file ID'.format(file) - ) - elif isinstance(file_handle, use_cache): - # File was cached, so an instance of use_cache was returned - if as_image: - media = types.InputMediaPhoto(file_handle) - else: - media = types.InputMediaDocument(file_handle) - elif as_image: - media = types.InputMediaUploadedPhoto(file_handle) - else: - attributes, mime_type = utils.get_attributes( - file, - mime_type=mime_type, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming - ) - - input_kw = {} - if thumb: - if isinstance(thumb, pathlib.Path): - thumb = str(thumb.absolute()) - input_kw['thumb'] = await self.upload_file(thumb) - - media = types.InputMediaUploadedDocument( - file=file_handle, - mime_type=mime_type, - attributes=attributes, - **input_kw - ) - return file_handle, media, as_image - - async def _cache_media(self: 'TelegramClient', msg, file, file_handle, image): - if file and msg and isinstance(file_handle, - custom.InputSizedFile): - # There was a response message and we didn't use cached - # version, so cache whatever we just sent to the database. - md5, size = file_handle.md5, file_handle.size - if image: - to_cache = utils.get_input_photo(msg.media.photo) - else: - to_cache = utils.get_input_document(msg.media.document) - self.session.cache_file(md5, size, to_cache) - - # endregion diff --git a/telethon/client/users.py b/telethon/client/users.py index fafff8cd..e69de29b 100644 --- a/telethon/client/users.py +++ b/telethon/client/users.py @@ -1,554 +0,0 @@ -import asyncio -import itertools -import time -import typing - -from .. import errors, utils, hints -from ..errors import MultiError, RPCError -from ..helpers import retry_range -from ..tl import TLRequest, types, functions - -_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class UserMethods: - async def __call__(self: 'TelegramClient', request, ordered=False): - requests = (request if utils.is_list_like(request) else (request,)) - for r in requests: - if not isinstance(r, TLRequest): - raise _NOT_A_REQUEST() - await r.resolve(self, utils) - - # Avoid making the request if it's already in a flood wait - if r.CONSTRUCTOR_ID in self._flood_waited_requests: - due = self._flood_waited_requests[r.CONSTRUCTOR_ID] - diff = round(due - time.time()) - if diff <= 3: # Flood waits below 3 seconds are "ignored" - self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - elif diff <= self.flood_sleep_threshold: - self._log[__name__].info( - 'Sleeping early for %ds on flood wait', diff) - await asyncio.sleep(diff, loop=self._loop) - self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - else: - raise errors.FloodWaitError(request=r, capture=diff) - - request_index = 0 - self._last_request = time.time() - for attempt in retry_range(self._request_retries): - try: - future = self._sender.send(request, ordered=ordered) - if isinstance(future, list): - results = [] - exceptions = [] - for f in future: - try: - result = await f - except RPCError as e: - exceptions.append(e) - results.append(None) - continue - self.session.process_entities(result) - self._entity_cache.add(result) - exceptions.append(None) - results.append(result) - request_index += 1 - if any(x is not None for x in exceptions): - raise MultiError(exceptions, results, requests) - else: - return results - else: - result = await future - self.session.process_entities(result) - self._entity_cache.add(result) - return result - except (errors.ServerError, errors.RpcCallFailError, - errors.RpcMcgetFailError) as e: - self._log[__name__].warning( - 'Telegram is having internal issues %s: %s', - e.__class__.__name__, e) - - await asyncio.sleep(2) - except (errors.FloodWaitError, errors.FloodTestPhoneWaitError) as e: - if utils.is_list_like(request): - request = request[request_index] - - self._flood_waited_requests\ - [request.CONSTRUCTOR_ID] = time.time() + e.seconds - - if e.seconds <= self.flood_sleep_threshold: - self._log[__name__].info('Sleeping for %ds on flood wait', - e.seconds) - await asyncio.sleep(e.seconds, loop=self._loop) - else: - raise - except (errors.PhoneMigrateError, errors.NetworkMigrateError, - errors.UserMigrateError) as e: - self._log[__name__].info('Phone migrated to %d', e.new_dc) - should_raise = isinstance(e, ( - errors.PhoneMigrateError, errors.NetworkMigrateError - )) - if should_raise and await self.is_user_authorized(): - raise - await self._switch_dc(e.new_dc) - - raise ValueError('Request was unsuccessful {} time(s)' - .format(attempt)) - - # region Public methods - - async def get_me(self: 'TelegramClient', input_peer: bool = False) \ - -> 'typing.Union[types.User, types.InputPeerUser]': - """ - Gets "me", the current :tl:`User` who is logged in. - - If the user has not logged in yet, this method returns ``None``. - - Arguments - input_peer (`bool`, optional): - Whether to return the :tl:`InputPeerUser` version or the normal - :tl:`User`. This can be useful if you just need to know the ID - of yourself. - - Returns - Your own :tl:`User`. - - Example - .. code-block:: python - - print(client.get_me().username) - """ - if input_peer and self._self_input_peer: - return self._self_input_peer - - try: - me = (await self( - functions.users.GetUsersRequest([types.InputUserSelf()])))[0] - - self._bot = me.bot - if not self._self_input_peer: - self._self_input_peer = utils.get_input_peer( - me, allow_self=False - ) - - return self._self_input_peer if input_peer else me - except errors.UnauthorizedError: - return None - - async def is_bot(self: 'TelegramClient') -> bool: - """ - Return ``True`` if the signed-in user is a bot, ``False`` otherwise. - - Example - .. code-block:: python - - if client.is_bot(): - print('Beep') - else: - print('Hello') - """ - if self._bot is None: - self._bot = (await self.get_me()).bot - - return self._bot - - async def is_user_authorized(self: 'TelegramClient') -> bool: - """ - Returns ``True`` if the user is authorized (i.e. has logged in). - - Example - .. code-block:: python - - if not client.is_user_authorized(): - client.send_code_request(phone) - code = input('enter code: ') - client.sign_in(phone, code) - """ - if self._authorized is None: - try: - # Any request that requires authorization will work - await self(functions.updates.GetStateRequest()) - self._authorized = True - except errors.RPCError: - self._authorized = False - - return self._authorized - - async def get_entity( - self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': - """ - Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` - or :tl:`Channel`. You can also pass a list or iterable of entities, - and they will be efficiently fetched from the network. - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username is given, **the username will be resolved** making - an API call every time. Resolving usernames is an expensive - operation and will start hitting flood waits around 50 usernames - in a short period of time. - - If you want to get the entity for a *cached* username, you should - first `get_input_entity(username) ` which will - use the cache), and then use `get_entity` with the result of the - previous call. - - Similar limits apply to invite links, and you should use their - ID instead. - - Using phone numbers (from people in your contact list), exact - names, integer IDs or :tl:`Peer` rely on a `get_input_entity` - first, which in turn needs the entity to be in cache, unless - a :tl:`InputPeer` was passed. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the - input entity. A list will be returned if more than one was given. - - Example - .. code-block:: python - - from telethon import utils - - me = client.get_entity('me') - print(utils.get_display_name(me)) - - chat = client.get_input_entity('username') - for message in client.iter_messages(chat): - ... - - # Note that you could have used the username directly, but it's - # good to use get_input_entity if you will reuse it a lot. - for message in client.iter_messages('username'): - ... - - # Note that for this to work the phone number must be in your contacts - some_id = client.get_peer_id('+34123456789') - """ - single = not utils.is_list_like(entity) - if single: - entity = (entity,) - - # Group input entities by string (resolve username), - # input users (get users), input chat (get chats) and - # input channels (get channels) to get the most entities - # in the less amount of calls possible. - inputs = [] - for x in entity: - if isinstance(x, str): - inputs.append(x) - else: - inputs.append(await self.get_input_entity(x)) - - users = [x for x in inputs - if isinstance(x, (types.InputPeerUser, types.InputPeerSelf))] - chats = [x.chat_id for x in inputs - if isinstance(x, types.InputPeerChat)] - channels = [x for x in inputs - if isinstance(x, types.InputPeerChannel)] - if users: - # GetUsersRequest has a limit of 200 per call - tmp = [] - while users: - curr, users = users[:200], users[200:] - tmp.extend(await self(functions.users.GetUsersRequest(curr))) - users = tmp - if chats: # TODO Handle chats slice? - chats = (await self( - functions.messages.GetChatsRequest(chats))).chats - if channels: - channels = (await self( - functions.channels.GetChannelsRequest(channels))).chats - - # Merge users, chats and channels into a single dictionary - id_entity = { - utils.get_peer_id(x): x - for x in itertools.chain(users, chats, channels) - } - - # We could check saved usernames and put them into the users, - # chats and channels list from before. While this would reduce - # the amount of ResolveUsername calls, it would fail to catch - # username changes. - result = [] - for x in inputs: - if isinstance(x, str): - result.append(await self._get_entity_from_string(x)) - elif not isinstance(x, types.InputPeerSelf): - result.append(id_entity[utils.get_peer_id(x)]) - else: - result.append(next( - u for u in id_entity.values() - if isinstance(u, types.User) and u.is_self - )) - - return result[0] if single else result - - async def get_input_entity( - self: 'TelegramClient', - peer: 'hints.EntityLike') -> 'types.TypeInputPeer': - """ - Turns the given entity into its input entity version. - - Most requests use this kind of :tl:`InputPeer`, so this is the most - suitable call to make for those cases. **Generally you should let the - library do its job** and don't worry about getting the input entity - first, but if you're going to use an entity often, consider making the - call: - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username or invite link is given, **the library will - use the cache**. This means that it's possible to be using - a username that *changed* or an old invite link (this only - happens if an invite link for a small group chat is used - after it was upgraded to a mega-group). - - If the username or ID from the invite link is not found in - the cache, it will be fetched. The same rules apply to phone - numbers (``'+34 123456789'``) from people in your contact list. - - If an exact name is given, it must be in the cache too. This - is not reliable as different people can share the same name - and which entity is returned is arbitrary, and should be used - only for quick tests. - - If a positive integer ID is given, the entity will be searched - in cached users, chats or channels, without making any call. - - If a negative integer ID is given, the entity will be searched - exactly as either a chat (prefixed with ``-``) or as a channel - (prefixed with ``-100``). - - If a :tl:`Peer` is given, it will be searched exactly in the - cache as either a user, chat or channel. - - If the given object can be turned into an input entity directly, - said operation will be done. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` - or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. - - If you need to get the ID of yourself, you should use - `get_me` with ``input_peer=True``) instead. - - Example - .. code-block:: python - - # If you're going to use "username" often in your code - # (make a lot of calls), consider getting its input entity - # once, and then using the "user" everywhere instead. - user = client.get_input_entity('username') - - # The same applies to IDs, chats or channels. - chat = client.get_input_entity(-123456789) - """ - # Short-circuit if the input parameter directly maps to an InputPeer - try: - return utils.get_input_peer(peer) - except TypeError: - pass - - # Next in priority is having a peer (or its ID) cached in-memory - try: - # 0x2d45687 == crc32(b'Peer') - if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: - return self._entity_cache[peer] - except (AttributeError, KeyError): - pass - - # Then come known strings that take precedence - if peer in ('me', 'self'): - return types.InputPeerSelf() - - # No InputPeer, cached peer, or known string. Fetch from disk cache - try: - return self.session.get_input_entity(peer) - except ValueError: - pass - - # Only network left to try - if isinstance(peer, str): - return utils.get_input_peer( - await self._get_entity_from_string(peer)) - - # If we're a bot and the user has messaged us privately users.getUsers - # will work with access_hash = 0. Similar for channels.getChannels. - # If we're not a bot but the user is in our contacts, it seems to work - # regardless. These are the only two special-cased requests. - peer = utils.get_peer(peer) - if isinstance(peer, types.PeerUser): - users = await self(functions.users.GetUsersRequest([ - types.InputUser(peer.user_id, access_hash=0)])) - if users and not isinstance(users[0], types.UserEmpty): - # If the user passed a valid ID they expect to work for - # channels but would be valid for users, we get UserEmpty. - # Avoid returning the invalid empty input peer for that. - # - # We *could* try to guess if it's a channel first, and if - # it's not, work as a chat and try to validate it through - # another request, but that becomes too much work. - return utils.get_input_peer(users[0]) - elif isinstance(peer, types.PeerChat): - return types.InputPeerChat(peer.chat_id) - elif isinstance(peer, types.PeerChannel): - try: - channels = await self(functions.channels.GetChannelsRequest([ - types.InputChannel(peer.channel_id, access_hash=0)])) - return utils.get_input_peer(channels.chats[0]) - except errors.ChannelInvalidError: - pass - - raise ValueError( - 'Could not find the input entity for {!r}. Please read https://' - 'docs.telethon.dev/en/latest/concepts/entities.html to' - ' find out more details.' - .format(peer) - ) - - async def get_peer_id( - self: 'TelegramClient', - peer: 'hints.EntityLike', - add_mark: bool = True) -> int: - """ - Gets the ID for the given entity. - - This method needs to be ``async`` because `peer` supports usernames, - invite-links, phone numbers (from people in your contact list), etc. - - If ``add_mark is False``, then a positive ID will be returned - instead. By default, bot-API style IDs (signed) are returned. - - Example - .. code-block:: python - - print(client.get_peer_id('me')) - """ - if isinstance(peer, int): - return utils.get_peer_id(peer, add_mark=add_mark) - - try: - if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): - # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' - peer = await self.get_input_entity(peer) - except AttributeError: - peer = await self.get_input_entity(peer) - - if isinstance(peer, types.InputPeerSelf): - peer = await self.get_me(input_peer=True) - - return utils.get_peer_id(peer, add_mark=add_mark) - - # endregion - - # region Private methods - - async def _get_entity_from_string(self: 'TelegramClient', string): - """ - Gets a full entity from the given string, which may be a phone or - a username, and processes all the found entities on the session. - The string may also be a user link, or a channel/chat invite link. - - This method has the side effect of adding the found users to the - session database, so it can be queried later without API calls, - if this option is enabled on the session. - - Returns the found entity, or raises TypeError if not found. - """ - phone = utils.parse_phone(string) - if phone: - try: - for user in (await self( - functions.contacts.GetContactsRequest(0))).users: - if user.phone == phone: - return user - except errors.BotMethodInvalidError: - raise ValueError('Cannot get entity by phone number as a ' - 'bot (try using integer IDs, not strings)') - elif string.lower() in ('me', 'self'): - return await self.get_me() - else: - username, is_join_chat = utils.parse_username(string) - if is_join_chat: - invite = await self( - functions.messages.CheckChatInviteRequest(username)) - - if isinstance(invite, types.ChatInvite): - raise ValueError( - 'Cannot get entity from a channel (or group) ' - 'that you are not part of. Join the group and retry' - ) - elif isinstance(invite, types.ChatInviteAlready): - return invite.chat - elif username: - try: - result = await self( - functions.contacts.ResolveUsernameRequest(username)) - except errors.UsernameNotOccupiedError as e: - raise ValueError('No user has "{}" as username' - .format(username)) from e - - try: - pid = utils.get_peer_id(result.peer, add_mark=False) - if isinstance(result.peer, types.PeerUser): - return next(x for x in result.users if x.id == pid) - else: - return next(x for x in result.chats if x.id == pid) - except StopIteration: - pass - try: - # Nobody with this username, maybe it's an exact name/title - return await self.get_entity( - self.session.get_input_entity(string)) - except ValueError: - pass - - raise ValueError( - 'Cannot find any entity corresponding to "{}"'.format(string) - ) - - async def _get_input_dialog(self: 'TelegramClient', dialog): - """ - Returns a :tl:`InputDialogPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - dialog.peer = await self.get_input_entity(dialog.peer) - return dialog - elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return types.InputDialogPeer(dialog) - except AttributeError: - pass - - return types.InputDialogPeer(await self.get_input_entity(dialog)) - - async def _get_input_notify(self: 'TelegramClient', notify): - """ - Returns a :tl:`InputNotifyPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if notify.SUBCLASS_OF_ID == 0x58981615: - if isinstance(notify, types.InputNotifyPeer): - notify.peer = await self.get_input_entity(notify.peer) - return notify - except AttributeError: - return types.InputNotifyPeer(await self.get_input_entity(notify)) - - # endregion diff --git a/telethon/crypto/__init__.py b/telethon/crypto/__init__.py index 69be1da8..e69de29b 100644 --- a/telethon/crypto/__init__.py +++ b/telethon/crypto/__init__.py @@ -1,10 +0,0 @@ -""" -This module contains several utilities regarding cryptographic purposes, -such as the AES IGE mode used by Telegram, the authorization key bound with -their data centers, and so on. -""" -from .aes import AES -from .aesctr import AESModeCTR -from .authkey import AuthKey -from .factorization import Factorization -from .cdndecrypter import CdnDecrypter diff --git a/telethon/crypto/aes.py b/telethon/crypto/aes.py index 3cfcc1af..e69de29b 100644 --- a/telethon/crypto/aes.py +++ b/telethon/crypto/aes.py @@ -1,111 +0,0 @@ -""" -AES IGE implementation in Python. - -If available, cryptg will be used instead, otherwise -if available, libssl will be used instead, otherwise -the Python implementation will be used. -""" -import os -import pyaes -import logging -from . import libssl - - -__log__ = logging.getLogger(__name__) - - -try: - import cryptg - __log__.info('cryptg detected, it will be used for encryption') -except ImportError: - cryptg = None - if libssl.encrypt_ige and libssl.decrypt_ige: - __log__.info('libssl detected, it will be used for encryption') - else: - __log__.info('cryptg module not installed and libssl not found, ' - 'falling back to (slower) Python encryption') - - -class AES: - """ - Class that servers as an interface to encrypt and decrypt - text through the AES IGE mode. - """ - @staticmethod - def decrypt_ige(cipher_text, key, iv): - """ - Decrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - if cryptg: - return cryptg.decrypt_ige(cipher_text, key, iv) - if libssl.decrypt_ige: - return libssl.decrypt_ige(cipher_text, key, iv) - - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] - - aes = pyaes.AES(key) - - plain_text = [] - blocks_count = len(cipher_text) // 16 - - cipher_text_block = [0] * 16 - for block_index in range(blocks_count): - for i in range(16): - cipher_text_block[i] = \ - cipher_text[block_index * 16 + i] ^ iv2[i] - - plain_text_block = aes.decrypt(cipher_text_block) - - for i in range(16): - plain_text_block[i] ^= iv1[i] - - iv1 = cipher_text[block_index * 16:block_index * 16 + 16] - iv2 = plain_text_block - - plain_text.extend(plain_text_block) - - return bytes(plain_text) - - @staticmethod - def encrypt_ige(plain_text, key, iv): - """ - Encrypts the given text in 16-bytes blocks by using the - given key and 32-bytes initialization vector. - """ - padding = len(plain_text) % 16 - if padding: - plain_text += os.urandom(16 - padding) - - if cryptg: - return cryptg.encrypt_ige(plain_text, key, iv) - if libssl.encrypt_ige: - return libssl.encrypt_ige(plain_text, key, iv) - - iv1 = iv[:len(iv) // 2] - iv2 = iv[len(iv) // 2:] - - aes = pyaes.AES(key) - - cipher_text = [] - blocks_count = len(plain_text) // 16 - - for block_index in range(blocks_count): - plain_text_block = list( - plain_text[block_index * 16:block_index * 16 + 16] - ) - for i in range(16): - plain_text_block[i] ^= iv1[i] - - cipher_text_block = aes.encrypt(plain_text_block) - - for i in range(16): - cipher_text_block[i] ^= iv2[i] - - iv1 = cipher_text_block - iv2 = plain_text[block_index * 16:block_index * 16 + 16] - - cipher_text.extend(cipher_text_block) - - return bytes(cipher_text) diff --git a/telethon/crypto/aesctr.py b/telethon/crypto/aesctr.py index 34422904..e69de29b 100644 --- a/telethon/crypto/aesctr.py +++ b/telethon/crypto/aesctr.py @@ -1,42 +0,0 @@ -""" -This module holds the AESModeCTR wrapper class. -""" -import pyaes - - -class AESModeCTR: - """Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV""" - # TODO Maybe make a pull request to pyaes to support iv on CTR - - def __init__(self, key, iv): - """ - Initializes the AES CTR mode with the given key/iv pair. - - :param key: the key to be used as bytes. - :param iv: the bytes initialization vector. Must have a length of 16. - """ - # TODO Use libssl if available - assert isinstance(key, bytes) - self._aes = pyaes.AESModeOfOperationCTR(key) - - assert isinstance(iv, bytes) - assert len(iv) == 16 - self._aes._counter._counter = list(iv) - - def encrypt(self, data): - """ - Encrypts the given plain text through AES CTR. - - :param data: the plain text to be encrypted. - :return: the encrypted cipher text. - """ - return self._aes.encrypt(data) - - def decrypt(self, data): - """ - Decrypts the given cipher text through AES CTR - - :param data: the cipher text to be decrypted. - :return: the decrypted plain text. - """ - return self._aes.decrypt(data) diff --git a/telethon/crypto/authkey.py b/telethon/crypto/authkey.py index 8475ec17..e69de29b 100644 --- a/telethon/crypto/authkey.py +++ b/telethon/crypto/authkey.py @@ -1,63 +0,0 @@ -""" -This module holds the AuthKey class. -""" -import struct -from hashlib import sha1 - -from ..extensions import BinaryReader - - -class AuthKey: - """ - Represents an authorization key, used to encrypt and decrypt - messages sent to Telegram's data centers. - """ - def __init__(self, data): - """ - Initializes a new authorization key. - - :param data: the data in bytes that represent this auth key. - """ - self.key = data - - @property - def key(self): - return self._key - - @key.setter - def key(self, value): - if not value: - self._key = self.aux_hash = self.key_id = None - return - - if isinstance(value, type(self)): - self._key, self.aux_hash, self.key_id = \ - value._key, value.aux_hash, value.key_id - return - - self._key = value - with BinaryReader(sha1(self._key).digest()) as reader: - self.aux_hash = reader.read_long(signed=False) - reader.read(4) - self.key_id = reader.read_long(signed=False) - - # TODO This doesn't really fit here, it's only used in authentication - def calc_new_nonce_hash(self, new_nonce, number): - """ - Calculates the new nonce hash based on the current attributes. - - :param new_nonce: the new nonce to be hashed. - :param number: number to prepend before the hash. - :return: the hash for the given new nonce. - """ - new_nonce = new_nonce.to_bytes(32, 'little', signed=True) - data = new_nonce + struct.pack(' 1: - break - - p, q = g, pq // g - return (p, q) if p < q else (q, p) - - @staticmethod - def gcd(a, b): - """ - Calculates the Greatest Common Divisor. - - :param a: the first number. - :param b: the second number. - :return: GCD(a, b) - """ - while b: - a, b = b, a % b - - return a diff --git a/telethon/crypto/libssl.py b/telethon/crypto/libssl.py index f1421e57..e69de29b 100644 --- a/telethon/crypto/libssl.py +++ b/telethon/crypto/libssl.py @@ -1,122 +0,0 @@ -""" -Helper module around the system's libssl library if available for IGE mode. -""" -import ctypes -import ctypes.util -try: - import ctypes.macholib.dyld -except ImportError: - pass -import logging -import os - -__log__ = logging.getLogger(__name__) - - -def _find_ssl_lib(): - lib = ctypes.util.find_library('ssl') - if not lib: - raise OSError('no library called "ssl" found') - - # First, let ctypes try to handle it itself. - try: - libssl = ctypes.cdll.LoadLibrary(lib) - except OSError: - pass - else: - return libssl - - # This is a best-effort attempt at finding the full real path of lib. - # - # Unfortunately ctypes doesn't tell us *where* it finds the library, - # so we have to do that ourselves. - try: - # This is not documented, so it could fail. Be on the safe side. - paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK - except AttributeError: - paths = [ - os.path.expanduser("~/lib"), - "/usr/local/lib", - "/lib", - "/usr/lib", - ] - - for path in paths: - if os.path.isdir(path): - for root, _, files in os.walk(path): - if lib in files: - # Manually follow symbolic links on *nix systems. - # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 - lib = os.path.realpath(os.path.join(root, lib)) - return ctypes.cdll.LoadLibrary(lib) - else: - raise OSError('no absolute path for "%s" and cannot load by name' % lib) - - -try: - _libssl = _find_ssl_lib() -except OSError as e: - # See https://github.com/LonamiWebs/Telethon/issues/1167 - # Sometimes `find_library` returns improper filenames. - __log__.info('Failed to load SSL library: %s (%s)', type(e), e) - _libssl = None - -if not _libssl: - decrypt_ige = None - encrypt_ige = None -else: - # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h - AES_ENCRYPT = ctypes.c_int(1) - AES_DECRYPT = ctypes.c_int(0) - AES_MAXNR = 14 - - class AES_KEY(ctypes.Structure): - """Helper class representing an AES key""" - _fields_ = [ - ('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))), - ('rounds', ctypes.c_uint), - ] - - def decrypt_ige(cipher_text, key, iv): - aes_key = AES_KEY() - key_len = ctypes.c_int(8 * len(key)) - key = (ctypes.c_ubyte * len(key))(*key) - iv = (ctypes.c_ubyte * len(iv))(*iv) - - in_len = ctypes.c_size_t(len(cipher_text)) - in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) - out_ptr = (ctypes.c_ubyte * len(cipher_text))() - - _libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key)) - _libssl.AES_ige_encrypt( - ctypes.byref(in_ptr), - ctypes.byref(out_ptr), - in_len, - ctypes.byref(aes_key), - ctypes.byref(iv), - AES_DECRYPT - ) - - return bytes(out_ptr) - - def encrypt_ige(plain_text, key, iv): - aes_key = AES_KEY() - key_len = ctypes.c_int(8 * len(key)) - key = (ctypes.c_ubyte * len(key))(*key) - iv = (ctypes.c_ubyte * len(iv))(*iv) - - in_len = ctypes.c_size_t(len(plain_text)) - in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text) - out_ptr = (ctypes.c_ubyte * len(plain_text))() - - _libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key)) - _libssl.AES_ige_encrypt( - ctypes.byref(in_ptr), - ctypes.byref(out_ptr), - in_len, - ctypes.byref(aes_key), - ctypes.byref(iv), - AES_ENCRYPT - ) - - return bytes(out_ptr) diff --git a/telethon/crypto/rsa.py b/telethon/crypto/rsa.py index e03ce188..e69de29b 100644 --- a/telethon/crypto/rsa.py +++ b/telethon/crypto/rsa.py @@ -1,122 +0,0 @@ -""" -This module holds several utilities regarding RSA and server fingerprints. -""" -import os -import struct -from hashlib import sha1 -try: - import rsa - import rsa.core -except ImportError: - rsa = None - raise ImportError('Missing module "rsa", please install via pip.') - -from ..tl import TLObject - - -# {fingerprint: Crypto.PublicKey.RSA._RSAobj} dictionary -_server_keys = {} - - -def get_byte_array(integer): - """Return the variable length bytes corresponding to the given int""" - # Operate in big endian (unlike most of Telegram API) since: - # > "...pq is a representation of a natural number - # (in binary *big endian* format)..." - # > "...current value of dh_prime equals - # (in *big-endian* byte order)..." - # Reference: https://core.telegram.org/mtproto/auth_key - return int.to_bytes( - integer, - (integer.bit_length() + 8 - 1) // 8, # 8 bits per byte, - byteorder='big', - signed=False - ) - - -def _compute_fingerprint(key): - """ - Given a RSA key, computes its fingerprint like Telegram does. - - :param key: the Crypto.RSA key. - :return: its 8-bytes-long fingerprint. - """ - n = TLObject.serialize_bytes(get_byte_array(key.n)) - e = TLObject.serialize_bytes(get_byte_array(key.e)) - # Telegram uses the last 8 bytes as the fingerprint - return struct.unpack('>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage) - ... async def delete(event): - ... await event.delete() - ... # No other event handler will have a chance to handle this event - ... raise StopPropagation - ... - >>> @client.on(events.NewMessage) - ... async def _(event): - ... # Will never be reached, because it is the second handler - ... pass - """ - # For some reason Sphinx wants the silly >>> or - # it will show warnings and look bad when generated. - pass - - -def register(event=None): - """ - Decorator method to *register* event handlers. This is the client-less - `add_event_handler() - ` variant. - - Note that this method only registers callbacks as handlers, - and does not attach them to any client. This is useful for - external modules that don't have access to the client, but - still want to define themselves as a handler. Example: - - >>> from telethon import events - >>> @events.register(events.NewMessage) - ... async def handler(event): - ... ... - ... - >>> # (somewhere else) - ... - >>> from telethon import TelegramClient - >>> client = TelegramClient(...) - >>> client.add_event_handler(handler) - - Remember that you can use this as a non-decorator - through ``register(event)(callback)``. - - Args: - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - """ - if isinstance(event, type): - event = event() - elif not event: - event = Raw() - - def decorator(callback): - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append(event) - setattr(callback, _HANDLERS_ATTRIBUTE, handlers) - return callback - - return decorator - - -def unregister(callback, event=None): - """ - Inverse operation of `register` (though not a decorator). Client-less - `remove_event_handler - ` - variant. **Note that this won't remove handlers from the client**, - because it simply can't, so you would generally use this before - adding the handlers to the client. - - This method is here for symmetry. You will rarely need to - unregister events, since you can simply just not add them - to any client. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append((event, callback)) - i = len(handlers) - while i: - i -= 1 - ev = handlers[i] - if not event or isinstance(ev, event): - del handlers[i] - found += 1 - - return found - - -def is_handler(callback): - """ - Returns ``True`` if the given callback is an - event handler (i.e. you used `register` on it). - """ - return hasattr(callback, _HANDLERS_ATTRIBUTE) - - -def list(callback): - """ - Returns a list containing the registered event - builders inside the specified callback handler. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] - - -def _get_handlers(callback): - """ - Like ``list`` but returns ``None`` if the callback was never registered. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, None) diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py index 99f0d147..e69de29b 100644 --- a/telethon/events/callbackquery.py +++ b/telethon/events/callbackquery.py @@ -1,297 +0,0 @@ -import re -import struct - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types, functions -from ..tl.custom.sendergetter import SenderGetter - - -@name_inner_event -class CallbackQuery(EventBuilder): - """ - Occurs whenever you sign in as a bot and a user - clicks one of the inline buttons on your messages. - - Note that the `chats` parameter will **not** work with normal - IDs or peers if the clicked inline button comes from a "via bot" - message. The `chats` parameter also supports checking against the - `chat_instance` which should be used for inline callbacks. - - Args: - data (`bytes` | `str` | `callable`, optional): - If set, the inline button payload data must match this data. - A UTF-8 string can also be given, a regex or a callable. For - instance, to check against ``'data_1'`` and ``'data_2'`` you - can use ``re.compile(b'data_')``. - """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, data=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - - if isinstance(data, bytes): - self.data = data - elif isinstance(data, str): - self.data = data.encode('utf-8') - elif not data or callable(data): - self.data = data - elif hasattr(data, 'match') and callable(data.match): - if not isinstance(getattr(data, 'pattern', b''), bytes): - data = re.compile(data.pattern.encode('utf-8'), - data.flags & (~re.UNICODE)) - - self.data = data.match - else: - raise TypeError('Invalid data type given') - - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateBotCallbackQuery): - event = cls.Event(update, update.peer, update.msg_id) - elif isinstance(update, types.UpdateInlineBotCallbackQuery): - # See https://github.com/LonamiWebs/Telethon/pull/1005 - # The long message ID is actually just msg_id + peer_id - mid, pid = struct.unpack('`, - since the message object is normally not present. - """ - self._client.loop.create_task(self.answer()) - if isinstance(self.query.msg_id, types.InputBotInlineMessageID): - return await self._client.edit_message( - self.query.msg_id, *args, **kwargs - ) - else: - return await self._client.edit_message( - await self.get_input_chat(), self.query.msg_id, - *args, **kwargs - ) - - async def delete(self, *args, **kwargs): - """ - Deletes the message. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - - If you need to delete more than one message at once, don't use - this `delete` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - - This method also creates a task to `answer` the callback. - - This method will likely fail if `via_inline` is ``True``. - """ - self._client.loop.create_task(self.answer()) - return await self._client.delete_messages( - await self.get_input_chat(), [self.query.msg_id], - *args, **kwargs - ) diff --git a/telethon/events/chataction.py b/telethon/events/chataction.py index 75734e05..e69de29b 100644 --- a/telethon/events/chataction.py +++ b/telethon/events/chataction.py @@ -1,390 +0,0 @@ -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types, functions - - -@name_inner_event -class ChatAction(EventBuilder): - """ - Occurs whenever a user joins or leaves a chat, or a message is pinned. - """ - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateChannelPinnedMessage) and update.id == 0: - # Telegram does not always send - # UpdateChannelPinnedMessage for new pins - # but always for unpin, with update.id = 0 - event = cls.Event(types.PeerChannel(update.channel_id), - unpin=True) - - elif isinstance(update, types.UpdateChatParticipantAdd): - event = cls.Event(types.PeerChat(update.chat_id), - added_by=update.inviter_id or True, - users=update.user_id) - - elif isinstance(update, types.UpdateChatParticipantDelete): - event = cls.Event(types.PeerChat(update.chat_id), - kicked_by=True, - users=update.user_id) - - elif (isinstance(update, ( - types.UpdateNewMessage, types.UpdateNewChannelMessage)) - and isinstance(update.message, types.MessageService)): - msg = update.message - action = update.message.action - if isinstance(action, types.MessageActionChatJoinedByLink): - event = cls.Event(msg, - added_by=True, - users=msg.from_id) - elif isinstance(action, types.MessageActionChatAddUser): - # If a user adds itself, it means they joined - added_by = ([msg.from_id] == action.users) or msg.from_id - event = cls.Event(msg, - added_by=added_by, - users=action.users) - elif isinstance(action, types.MessageActionChatDeleteUser): - event = cls.Event(msg, - kicked_by=msg.from_id or True, - users=action.user_id) - elif isinstance(action, types.MessageActionChatCreate): - event = cls.Event(msg, - users=action.users, - created=True, - new_title=action.title) - elif isinstance(action, types.MessageActionChannelCreate): - event = cls.Event(msg, - created=True, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditTitle): - event = cls.Event(msg, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditPhoto): - event = cls.Event(msg, - users=msg.from_id, - new_photo=action.photo) - elif isinstance(action, types.MessageActionChatDeletePhoto): - event = cls.Event(msg, - users=msg.from_id, - new_photo=True) - elif isinstance(action, types.MessageActionPinMessage): - # Telegram always sends this service message for new pins - event = cls.Event(msg, - users=msg.from_id, - new_pin=msg.reply_to_msg_id) - else: - return - else: - return - - event._entities = update._entities - return event - - class Event(EventCommon): - """ - Represents the event of a new chat action. - - Members: - action_message (`MessageAction `_): - The message invoked by this Chat Action. - - new_pin (`bool`): - ``True`` if there is a new pin. - - new_photo (`bool`): - ``True`` if there's a new chat photo (or it was removed). - - photo (:tl:`Photo`, optional): - The new photo (or ``None`` if it was removed). - - user_added (`bool`): - ``True`` if the user was added by some other. - - user_joined (`bool`): - ``True`` if the user joined on their own. - - user_left (`bool`): - ``True`` if the user left on their own. - - user_kicked (`bool`): - ``True`` if the user was kicked by some other. - - created (`bool`, optional): - ``True`` if this chat was just created. - - new_title (`str`, optional): - The new title string for the chat, if applicable. - - unpin (`bool`): - ``True`` if the existing pin gets unpinned. - """ - def __init__(self, where, new_pin=None, new_photo=None, - added_by=None, kicked_by=None, created=None, - users=None, new_title=None, unpin=None): - if isinstance(where, types.MessageService): - self.action_message = where - where = where.to_id - else: - self.action_message = None - - super().__init__(chat_peer=where, msg_id=new_pin) - - self.new_pin = isinstance(new_pin, int) - self._pinned_message = new_pin - - self.new_photo = new_photo is not None - self.photo = \ - new_photo if isinstance(new_photo, types.Photo) else None - - self._added_by = None - self._kicked_by = None - self.user_added = self.user_joined = self.user_left = \ - self.user_kicked = self.unpin = False - - if added_by is True: - self.user_joined = True - elif added_by: - self.user_added = True - self._added_by = added_by - - # If `from_id` was not present (it's ``True``) or the affected - # user was "kicked by itself", then it left. Else it was kicked. - if kicked_by is True or kicked_by == users: - self.user_left = True - elif kicked_by: - self.user_kicked = True - self._kicked_by = kicked_by - - self.created = bool(created) - self._user_peers = users if isinstance(users, list) else [users] - self._users = None - self._input_users = None - self.new_title = new_title - self.unpin = unpin - - def _set_client(self, client): - super()._set_client(client) - if self.action_message: - self.action_message._finish_init(client, self._entities, None) - - async def respond(self, *args, **kwargs): - """ - Responds to the chat action message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def reply(self, *args, **kwargs): - """ - Replies to the chat action message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. - - Has the same effect as `respond` if there is no message. - """ - if not self.action_message: - return await self.respond(*args, **kwargs) - - kwargs['reply_to'] = self.action_message.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def delete(self, *args, **kwargs): - """ - Deletes the chat action message. You're responsible for checking - whether you have the permission to do so, or to except the error - otherwise. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - - Does nothing if no message action triggered this event. - """ - if not self.action_message: - return - - return await self._client.delete_messages( - await self.get_input_chat(), [self.action_message], - *args, **kwargs - ) - - async def get_pinned_message(self): - """ - If ``new_pin`` is ``True``, this returns the `Message - ` object that was pinned. - """ - if self._pinned_message == 0: - return None - - if isinstance(self._pinned_message, int)\ - and await self.get_input_chat(): - r = await self._client(functions.channels.GetMessagesRequest( - self._input_chat, [self._pinned_message] - )) - try: - self._pinned_message = next( - x for x in r.messages - if isinstance(x, types.Message) - and x.id == self._pinned_message - ) - except StopIteration: - pass - - if isinstance(self._pinned_message, types.Message): - return self._pinned_message - - @property - def added_by(self): - """ - The user who added ``users``, if applicable (``None`` otherwise). - """ - if self._added_by and not isinstance(self._added_by, types.User): - aby = self._entities.get(utils.get_peer_id(self._added_by)) - if aby: - self._added_by = aby - - return self._added_by - - async def get_added_by(self): - """ - Returns `added_by` but will make an API call if necessary. - """ - if not self.added_by and self._added_by: - self._added_by = await self._client.get_entity(self._added_by) - - return self._added_by - - @property - def kicked_by(self): - """ - The user who kicked ``users``, if applicable (``None`` otherwise). - """ - if self._kicked_by and not isinstance(self._kicked_by, types.User): - kby = self._entities.get(utils.get_peer_id(self._kicked_by)) - if kby: - self._kicked_by = kby - - return self._kicked_by - - async def get_kicked_by(self): - """ - Returns `kicked_by` but will make an API call if necessary. - """ - if not self.kicked_by and self._kicked_by: - self._kicked_by = await self._client.get_entity(self._kicked_by) - - return self._kicked_by - - @property - def user(self): - """ - The first user that takes part in this action (e.g. joined). - - Might be ``None`` if the information can't be retrieved or - there is no user taking part. - """ - if self.users: - return self._users[0] - - async def get_user(self): - """ - Returns `user` but will make an API call if necessary. - """ - if self.users or await self.get_users(): - return self._users[0] - - @property - def input_user(self): - """ - Input version of the ``self.user`` property. - """ - if self.input_users: - return self._input_users[0] - - async def get_input_user(self): - """ - Returns `input_user` but will make an API call if necessary. - """ - if self.input_users or await self.get_input_users(): - return self._input_users[0] - - @property - def user_id(self): - """ - Returns the marked signed ID of the first user, if any. - """ - if self._user_peers: - return utils.get_peer_id(self._user_peers[0]) - - @property - def users(self): - """ - A list of users that take part in this action (e.g. joined). - - Might be empty if the information can't be retrieved or there - are no users taking part. - """ - if not self._user_peers: - return [] - - if self._users is None: - self._users = [ - self._entities[utils.get_peer_id(peer)] - for peer in self._user_peers - if utils.get_peer_id(peer) in self._entities - ] - - return self._users - - async def get_users(self): - """ - Returns `users` but will make an API call if necessary. - """ - if not self._user_peers: - return [] - - if self._users is None or len(self._users) != len(self._user_peers): - await self.action_message._reload_message() - self._users = [ - u for u in self.action_message.action_entities - if isinstance(u, (types.User, types.UserEmpty))] - - return self._users - - @property - def input_users(self): - """ - Input version of the ``self.users`` property. - """ - if self._input_users is None and self._user_peers: - self._input_users = [] - for peer in self._user_peers: - try: - self._input_users.append(self._client._entity_cache[peer]) - except KeyError: - pass - return self._input_users or [] - - async def get_input_users(self): - """ - Returns `input_users` but will make an API call if necessary. - """ - self._input_users = None - if self._input_users is None: - await self.action_message._reload_message() - self._input_users = [ - utils.get_input_peer(u) - for u in self.action_message.action_entities - if isinstance(u, (types.User, types.UserEmpty))] - - return self._input_users or [] - - @property - def user_ids(self): - """ - Returns the marked signed ID of the users, if any. - """ - if self._user_peers: - return [utils.get_peer_id(u) for u in self._user_peers] diff --git a/telethon/events/common.py b/telethon/events/common.py index eeb24c0c..e69de29b 100644 --- a/telethon/events/common.py +++ b/telethon/events/common.py @@ -1,179 +0,0 @@ -import abc -import asyncio -import itertools -import warnings - -from .. import utils -from ..tl import TLObject, types, functions -from ..tl.custom.chatgetter import ChatGetter - - -async def _into_id_set(client, chats): - """Helper util to turn the input chat or chats into a set of IDs.""" - if chats is None: - return None - - if not utils.is_list_like(chats): - chats = (chats,) - - result = set() - for chat in chats: - if isinstance(chat, int): - if chat < 0: - result.add(chat) # Explicitly marked IDs are negative - else: - result.update({ # Support all valid types of peers - utils.get_peer_id(types.PeerUser(chat)), - utils.get_peer_id(types.PeerChat(chat)), - utils.get_peer_id(types.PeerChannel(chat)), - }) - elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - result.add(utils.get_peer_id(chat)) - else: - chat = await client.get_input_entity(chat) - if isinstance(chat, types.InputPeerSelf): - chat = await client.get_me(input_peer=True) - result.add(utils.get_peer_id(chat)) - - return result - - -class EventBuilder(abc.ABC): - """ - The common event builder, with builtin support to filter per chat. - - Args: - chats (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only matching chats will be handled. - - blacklist_chats (`bool`, optional): - Whether to treat the chats as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``chats`` - which will be ignored if ``blacklist_chats=True``. - - func (`callable`, optional): - A callable function that should accept the event as input - parameter, and return a value indicating whether the event - should be dispatched or not (any truthy value will do, it - does not need to be a `bool`). It works like a custom filter: - - .. code-block:: python - - @client.on(events.NewMessage(func=lambda e: e.is_private)) - async def handler(event): - pass # code here - """ - self_id = None - - def __init__(self, chats=None, *, blacklist_chats=False, func=None): - self.chats = chats - self.blacklist_chats = bool(blacklist_chats) - self.resolved = False - self.func = func - self._resolve_lock = None - - @classmethod - @abc.abstractmethod - def build(cls, update): - """Builds an event for the given update if possible, or returns None""" - - async def resolve(self, client): - """Helper method to allow event builders to be resolved before usage""" - if self.resolved: - return - - if not self._resolve_lock: - self._resolve_lock = asyncio.Lock(loop=client.loop) - - async with self._resolve_lock: - if not self.resolved: - await self._resolve(client) - self.resolved = True - - async def _resolve(self, client): - self.chats = await _into_id_set(client, self.chats) - if not EventBuilder.self_id: - EventBuilder.self_id = await client.get_peer_id('me') - - def filter(self, event): - """ - If the ID of ``event._chat_peer`` isn't in the chats set (or it is - but the set is a blacklist) returns ``None``, otherwise the event. - - The events must have been resolved before this can be called. - """ - if not self.resolved: - return None - - if self.chats is not None: - # Note: the `event.chat_id` property checks if it's `None` for us - inside = event.chat_id in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return None - - if not self.func or self.func(event): - return event - - -class EventCommon(ChatGetter, abc.ABC): - """ - Intermediate class with common things to all events. - - Remember that this class implements `ChatGetter - ` which - means you have access to all chat properties and methods. - - In addition, you can access the `original_update` - field which contains the original :tl:`Update`. - """ - _event_name = 'Event' - - def __init__(self, chat_peer=None, msg_id=None, broadcast=None): - super().__init__(chat_peer, broadcast=broadcast) - self._entities = {} - self._client = None - self._message_id = msg_id - self.original_update = None - - def _set_client(self, client): - """ - Setter so subclasses can act accordingly when the client is set. - """ - self._client = client - if self._chat_peer: - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, self._entities, client._entity_cache) - else: - self._chat = self._input_chat = None - - @property - def client(self): - """ - The `telethon.TelegramClient` that created this event. - """ - return self._client - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) - - def to_dict(self): - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self._event_name - return d - - -def name_inner_event(cls): - """Decorator to rename cls.Event 'Event' as 'cls.Event'""" - if hasattr(cls, 'Event'): - cls.Event._event_name = '{}.Event'.format(cls.__name__) - else: - warnings.warn('Class {} does not have a inner Event'.format(cls)) - return cls diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py index 794ca857..e69de29b 100644 --- a/telethon/events/inlinequery.py +++ b/telethon/events/inlinequery.py @@ -1,235 +0,0 @@ -import inspect -import re - -import asyncio - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types, functions, custom -from ..tl.custom.sendergetter import SenderGetter - - -@name_inner_event -class InlineQuery(EventBuilder): - """ - Occurs whenever you sign in as a bot and a user - sends an inline query such as ``@bot query``. - - Args: - users (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only inline queries from these users will be handled. - - blacklist_users (`bool`, optional): - Whether to treat the users as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``users`` - which will be ignored if ``blacklist_users=True``. - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only queries matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns ``True`` - if a message is acceptable, or a compiled regex pattern. - """ - def __init__( - self, users=None, *, blacklist_users=False, func=None, pattern=None): - super().__init__(users, blacklist_chats=blacklist_users, func=func) - - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateBotInlineQuery): - event = cls.Event(update) - else: - return - - event._entities = update._entities - return event - - def filter(self, event): - if self.pattern: - match = self.pattern(event.text) - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon, SenderGetter): - """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotCallbackQuery`): - The original :tl:`UpdateBotCallbackQuery`. - - Make sure to access the `text` of the query if - that's what you want instead working with this. - - pattern_match (`obj`, optional): - The resulting object from calling the passed ``pattern`` - function, which is ``re.compile(...).match`` by default. - """ - def __init__(self, query): - super().__init__(chat_peer=types.PeerUser(query.user_id)) - SenderGetter.__init__(self, query.user_id) - self.query = query - self.pattern_match = None - self._answered = False - - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) - - @property - def id(self): - """ - Returns the unique identifier for the query ID. - """ - return self.query.query_id - - @property - def text(self): - """ - Returns the text the user used to make the inline query. - """ - return self.query.query - - @property - def offset(self): - """ - The string the user's client used as an offset for the query. - This will either be empty or equal to offsets passed to `answer`. - """ - return self.query.offset - - @property - def geo(self): - """ - If the user location is requested when using inline mode - and the user's device is able to send it, this will return - the :tl:`GeoPoint` with the position of the user. - """ - return - - @property - def builder(self): - """ - Returns a new `InlineBuilder - ` instance. - """ - return custom.InlineBuilder(self._client) - - async def answer( - self, results=None, cache_time=0, *, - gallery=False, next_offset=None, private=False, - switch_pm=None, switch_pm_param=''): - """ - Answers the inline query with the given results. - - Args: - results (`list`, optional): - A list of :tl:`InputBotInlineResult` to use. - You should use `builder` to create these: - - .. code-block:: python - - builder = inline.builder - r1 = builder.article('Be nice', text='Have a nice day') - r2 = builder.article('Be bad', text="I don't like you") - await inline.answer([r1, r2]) - - You can send up to 50 results as documented in - https://core.telegram.org/bots/api#answerinlinequery. - Sending more will raise ``ResultsTooMuchError``, - and you should consider using `next_offset` to - paginate them. - - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. - - gallery (`bool`, optional): - Whether the results should show as a gallery (grid) or not. - - next_offset (`str`, optional): - The offset the client will send when the user scrolls the - results and it repeats the request. - - private (`bool`, optional): - Whether the results should be cached by Telegram - (not private) or by the user's client (private). - - switch_pm (`str`, optional): - If set, this text will be shown in the results - to allow the user to switch to private messages. - - switch_pm_param (`str`, optional): - Optional parameter to start the bot with if - `switch_pm` was used. - - Example: - - .. code-block:: python - - @bot.on(events.InlineQuery) - async def handler(event): - builder = event.builder - - rev_text = event.text[::-1] - await event.answer([ - builder.article('Reverse text', text=rev_text), - builder.photo('/path/to/photo.jpg') - ]) - """ - if self._answered: - return - - if results: - futures = [self._as_future(x, self._client.loop) - for x in results] - - await asyncio.wait(futures, loop=self._client.loop) - - # All futures will be in the `done` *set* that `wait` returns. - # - # Precisely because it's a `set` and not a `list`, it - # will not preserve the order, but since all futures - # completed we can use our original, ordered `list`. - results = [x.result() for x in futures] - else: - results = [] - - if switch_pm: - switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param) - - return await self._client( - functions.messages.SetInlineBotResultsRequest( - query_id=self.query.query_id, - results=results, - cache_time=cache_time, - gallery=gallery, - next_offset=next_offset, - private=private, - switch_pm=switch_pm - ) - ) - - @staticmethod - def _as_future(obj, loop): - if inspect.isawaitable(obj): - return asyncio.ensure_future(obj, loop=loop) - - f = loop.create_future() - f.set_result(obj) - return f diff --git a/telethon/events/messagedeleted.py b/telethon/events/messagedeleted.py index dc86997d..e69de29b 100644 --- a/telethon/events/messagedeleted.py +++ b/telethon/events/messagedeleted.py @@ -1,51 +0,0 @@ -from .common import EventBuilder, EventCommon, name_inner_event -from ..tl import types - - -@name_inner_event -class MessageDeleted(EventBuilder): - """ - Occurs whenever a message is deleted. Note that this event isn't 100% - reliable, since Telegram doesn't always notify the clients that a message - was deleted. - - .. important:: - - Telegram **does not** send information about *where* a message - was deleted if it occurs in private conversations with other users - or in small group chats, because message IDs are *unique* and you - can identify the chat with the message ID alone if you saved it - previously. - - Telethon **does not** save information of where messages occur, - so it cannot know in which chat a message was deleted (this will - only work in channels, where the channel ID *is* present). - - This means that the ``chats=`` parameter will not work reliably, - unless you intend on working with channels and super-groups only. - """ - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateDeleteMessages): - event = cls.Event( - deleted_ids=update.messages, - peer=None - ) - elif isinstance(update, types.UpdateDeleteChannelMessages): - event = cls.Event( - deleted_ids=update.messages, - peer=types.PeerChannel(update.channel_id) - ) - else: - return - - event._entities = update._entities - return event - - class Event(EventCommon): - def __init__(self, deleted_ids, peer): - super().__init__( - chat_peer=peer, msg_id=(deleted_ids or [0])[0] - ) - self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = deleted_ids diff --git a/telethon/events/messageedited.py b/telethon/events/messageedited.py index 83a71dd6..e69de29b 100644 --- a/telethon/events/messageedited.py +++ b/telethon/events/messageedited.py @@ -1,47 +0,0 @@ -from .common import name_inner_event -from .newmessage import NewMessage -from ..tl import types - - -@name_inner_event -class MessageEdited(NewMessage): - """ - Occurs whenever a message is edited. Just like `NewMessage - `, you should treat - this event as a `Message `. - - .. warning:: - - On channels, `Message.out ` - will be ``True`` if you sent the message originally, **not if - you edited it**! This can be dangerous if you run outgoing - commands on edits. - - Some examples follow: - - * You send a message "A", ``out is True``. - * You edit "A" to "B", ``out is True``. - * Someone else edits "B" to "C", ``out is True`` (**be careful!**). - * Someone sends "X", ``out is False``. - * Someone edits "X" to "Y", ``out is False``. - * You edit "Y" to "Z", ``out is False``. - - Since there are useful cases where you need the right ``out`` - value, the library cannot do anything automatically to help you. - Instead, consider using ``from_users='me'`` (it won't work in - broadcast channels at all since the sender is the channel and - not you). - """ - @classmethod - def build(cls, update): - if isinstance(update, (types.UpdateEditMessage, - types.UpdateEditChannelMessage)): - event = cls.Event(update.message) - else: - return - - event._entities = update._entities - return event - - class Event(NewMessage.Event): - pass # Required if we want a different name for it diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py index 00ac6073..e69de29b 100644 --- a/telethon/events/messageread.py +++ b/telethon/events/messageread.py @@ -1,133 +0,0 @@ -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types - - -@name_inner_event -class MessageRead(EventBuilder): - """ - Occurs whenever one or more messages are read in a chat. - - Args: - inbox (`bool`, optional): - If this argument is ``True``, then when you read someone else's - messages the event will be fired. By default (``False``) only - when messages you sent are read by someone else will fire it. - """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, inbox=False): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.inbox = inbox - - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateReadHistoryInbox): - event = cls.Event(update.peer, update.max_id, False) - elif isinstance(update, types.UpdateReadHistoryOutbox): - event = cls.Event(update.peer, update.max_id, True) - elif isinstance(update, types.UpdateReadChannelInbox): - event = cls.Event(types.PeerChannel(update.channel_id), - update.max_id, False) - elif isinstance(update, types.UpdateReadChannelOutbox): - event = cls.Event(types.PeerChannel(update.channel_id), - update.max_id, True) - elif isinstance(update, types.UpdateReadMessagesContents): - event = cls.Event(message_ids=update.messages, - contents=True) - elif isinstance(update, types.UpdateChannelReadMessagesContents): - event = cls.Event(types.PeerChannel(update.channel_id), - message_ids=update.messages, - contents=True) - else: - return - - event._entities = update._entities - return event - - def filter(self, event): - if self.inbox == event.outbox: - return - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of one or more messages being read. - - Members: - max_id (`int`): - Up to which message ID has been read. Every message - with an ID equal or lower to it have been read. - - outbox (`bool`): - ``True`` if someone else has read your messages. - - contents (`bool`): - ``True`` if what was read were the contents of a message. - This will be the case when e.g. you play a voice note. - It may only be set on ``inbox`` events. - """ - def __init__(self, peer=None, max_id=None, out=False, contents=False, - message_ids=None): - self.outbox = out - self.contents = contents - self._message_ids = message_ids or [] - self._messages = None - self.max_id = max_id or max(message_ids or [], default=None) - super().__init__(peer, self.max_id) - - @property - def inbox(self): - """ - ``True`` if you have read someone else's messages. - """ - return not self.outbox - - @property - def message_ids(self): - """ - The IDs of the messages **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - return self._message_ids - - async def get_messages(self): - """ - Returns the list of `Message ` - **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - if self._messages is None: - chat = await self.get_input_chat() - if not chat: - self._messages = [] - else: - self._messages = await self._client.get_messages( - chat, ids=self._message_ids) - - return self._messages - - def is_read(self, message): - """ - Returns ``True`` if the given message (or its ID) has been read. - - If a list-like argument is provided, this method will return a - list of booleans indicating which messages have been read. - """ - if utils.is_list_like(message): - return [(m if isinstance(m, int) else m.id) <= self.max_id - for m in message] - else: - return (message if isinstance(message, int) - else message.id) <= self.max_id - - def __contains__(self, message): - """``True`` if the message(s) are read message.""" - if utils.is_list_like(message): - return all(self.is_read(message)) - else: - return self.is_read(message) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py index c1396f72..e69de29b 100644 --- a/telethon/events/newmessage.py +++ b/telethon/events/newmessage.py @@ -1,222 +0,0 @@ -import asyncio -import re - -from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set -from ..tl import types - - -@name_inner_event -class NewMessage(EventBuilder): - """ - Occurs whenever a new text message or a message with media arrives. - - Args: - incoming (`bool`, optional): - If set to ``True``, only **incoming** messages will be handled. - Mutually exclusive with ``outgoing`` (can only set one of either). - - outgoing (`bool`, optional): - If set to ``True``, only **outgoing** messages will be handled. - Mutually exclusive with ``incoming`` (can only set one of either). - - from_users (`entity`, optional): - Unlike `chats`, this parameter filters the *senders* of the - message. That is, only messages *sent by these users* will be - handled. Use `chats` if you want private messages with this/these - users. `from_users` lets you filter by messages sent by *one or - more* users across the desired chats (doesn't need a list). - - forwards (`bool`, optional): - Whether forwarded messages should be handled or not. By default, - both forwarded and normal messages are included. If it's ``True`` - *only* forwards will be handled. If it's ``False`` only messages - that are *not* forwards will be handled. - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only messages matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns ``True`` - if a message is acceptable, or a compiled regex pattern. - """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None, - incoming=None, outgoing=None, - from_users=None, forwards=None, pattern=None): - if incoming and outgoing: - incoming = outgoing = None # Same as no filter - elif incoming is not None and outgoing is None: - outgoing = not incoming - elif outgoing is not None and incoming is None: - incoming = not outgoing - elif all(x is not None and not x for x in (incoming, outgoing)): - raise ValueError("Don't create an event handler if you " - "don't want neither incoming nor outgoing!") - - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.incoming = incoming - self.outgoing = outgoing - self.from_users = from_users - self.forwards = forwards - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - # Should we short-circuit? E.g. perform no check at all - self._no_check = all(x is None for x in ( - self.chats, self.incoming, self.outgoing, self.pattern, - self.from_users, self.forwards, self.from_users, self.func - )) - - async def _resolve(self, client): - await super()._resolve(client) - self.from_users = await _into_id_set(client, self.from_users) - - @classmethod - def build(cls, update): - if isinstance(update, - (types.UpdateNewMessage, types.UpdateNewChannelMessage)): - if not isinstance(update.message, types.Message): - return # We don't care about MessageService's here - event = cls.Event(update.message) - elif isinstance(update, types.UpdateShortMessage): - event = cls.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - # Note that to_id/from_id complement each other in private - # messages, depending on whether the message was outgoing. - to_id=types.PeerUser( - update.user_id if update.out else cls.self_id - ), - from_id=cls.self_id if update.out else update.user_id, - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to_msg_id=update.reply_to_msg_id, - entities=update.entities - )) - elif isinstance(update, types.UpdateShortChatMessage): - event = cls.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - from_id=update.from_id, - to_id=types.PeerChat(update.chat_id), - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to_msg_id=update.reply_to_msg_id, - entities=update.entities - )) - else: - return - - # Make messages sent to ourselves outgoing unless they're forwarded. - # This makes it consistent with official client's appearance. - ori = event.message - if isinstance(ori.to_id, types.PeerUser): - if ori.from_id == ori.to_id.user_id and not ori.fwd_from: - event.message.out = True - - event._entities = update._entities - return event - - def filter(self, event): - if self._no_check: - return event - - if self.incoming and event.message.out: - return - if self.outgoing and not event.message.out: - return - if self.forwards is not None: - if bool(self.forwards) != bool(event.message.fwd_from): - return - - if self.from_users is not None: - if event.message.from_id not in self.from_users: - return - - if self.pattern: - match = self.pattern(event.message.message or '') - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of a new message. This event can be treated - to all effects as a `Message `, - so please **refer to its documentation** to know what you can do - with this event. - - Members: - message (`Message `): - This is the only difference with the received - `Message `, and will - return the `telethon.tl.custom.message.Message` itself, - not the text. - - See `Message ` for - the rest of available members and methods. - - pattern_match (`obj`): - The resulting object from calling the passed ``pattern`` function. - Here's an example using a string (defaults to regex match): - - >>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) - ... async def handler(event): - ... # In this case, the result is a ``Match`` object - ... # since the ``str`` pattern was converted into - ... # the ``re.compile(pattern).match`` function. - ... print('Welcomed', event.pattern_match.group(1)) - ... - >>> - """ - def __init__(self, message): - self.__dict__['_init'] = False - if not message.out and isinstance(message.to_id, types.PeerUser): - # Incoming message (e.g. from a bot) has to_id=us, and - # from_id=bot (the actual "chat" from a user's perspective). - chat_peer = types.PeerUser(message.from_id) - else: - chat_peer = message.to_id - - super().__init__(chat_peer=chat_peer, - msg_id=message.id, broadcast=bool(message.post)) - - self.pattern_match = None - self.message = message - - def _set_client(self, client): - super()._set_client(client) - m = self.message - m._finish_init(client, self._entities, None) - self.__dict__['_init'] = True # No new attributes can be set - - def __getattr__(self, item): - if item in self.__dict__: - return self.__dict__[item] - else: - return getattr(self.message, item) - - def __setattr__(self, name, value): - if not self.__dict__['_init'] or name in self.__dict__: - self.__dict__[name] = value - else: - setattr(self.message, name, value) diff --git a/telethon/events/raw.py b/telethon/events/raw.py index cd4c80a9..e69de29b 100644 --- a/telethon/events/raw.py +++ b/telethon/events/raw.py @@ -1,41 +0,0 @@ -from .common import EventBuilder -from .. import utils - - -class Raw(EventBuilder): - """ - Raw events are not actual events. Instead, they are the raw - :tl:`Update` object that Telegram sends. You normally shouldn't - need these. - - Args: - types (`list` | `tuple` | `type`, optional): - The type or types that the :tl:`Update` instance must be. - Equivalent to ``if not isinstance(update, types): return``. - """ - def __init__(self, types=None, *, func=None): - super().__init__(func=func) - if not types: - self.types = None - elif not utils.is_list_like(types): - if not isinstance(types, type): - raise TypeError('Invalid input type given %s', types) - - self.types = types - else: - if not all(isinstance(x, type) for x in types): - raise TypeError('Invalid input types given %s', types) - - self.types = tuple(types) - - async def resolve(self, client): - self.resolved = True - - @classmethod - def build(cls, update): - return update - - def filter(self, event): - if ((not self.types or isinstance(event, self.types)) - and (not self.func or self.func(event))): - return event diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py index 742c8034..e69de29b 100644 --- a/telethon/events/userupdate.py +++ b/telethon/events/userupdate.py @@ -1,211 +0,0 @@ -import datetime - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types -from ..tl.custom.sendergetter import SenderGetter - - -@name_inner_event -class UserUpdate(EventBuilder): - """ - Occurs whenever a user goes online, starts typing, etc. - """ - @classmethod - def build(cls, update): - if isinstance(update, types.UpdateUserStatus): - event = cls.Event(update.user_id, - status=update.status) - elif isinstance(update, types.UpdateChatUserTyping): - # Unfortunately, we can't know whether `chat_id`'s type - event = cls.Event(update.user_id, - chat_id=update.chat_id, - typing=update.action) - elif isinstance(update, types.UpdateUserTyping): - event = cls.Event(update.user_id, - typing=update.action) - else: - return - - event._entities = update._entities - return event - - class Event(EventCommon, SenderGetter): - """ - Represents the event of a user update - such as gone online, started typing, etc. - - Members: - online (`bool`, optional): - ``True`` if the user is currently online, ``False`` otherwise. - Might be ``None`` if this information is not present. - - last_seen (`datetime`, optional): - Exact date when the user was last seen if known. - - until (`datetime`, optional): - Until when will the user remain online. - - within_months (`bool`): - ``True`` if the user was seen within 30 days. - - within_weeks (`bool`): - ``True`` if the user was seen within 7 days. - - recently (`bool`): - ``True`` if the user was seen within a day. - - action (:tl:`SendMessageAction`, optional): - The "typing" action if any the user is performing if any. - - cancel (`bool`): - ``True`` if the action was cancelling other actions. - - typing (`bool`): - ``True`` if the action is typing a message. - - recording (`bool`): - ``True`` if the action is recording something. - - uploading (`bool`): - ``True`` if the action is uploading something. - - playing (`bool`): - ``True`` if the action is playing a game. - - audio (`bool`): - ``True`` if what's being recorded/uploaded is an audio. - - round (`bool`): - ``True`` if what's being recorded/uploaded is a round video. - - video (`bool`): - ``True`` if what's being recorded/uploaded is an video. - - document (`bool`): - ``True`` if what's being uploaded is document. - - geo (`bool`): - ``True`` if what's being uploaded is a geo. - - photo (`bool`): - ``True`` if what's being uploaded is a photo. - - contact (`bool`): - ``True`` if what's being uploaded (selected) is a contact. - """ - def __init__(self, user_id, *, status=None, chat_id=None, typing=None): - if chat_id is None: - super().__init__(types.PeerUser(user_id)) - else: - # Temporarily set the chat_peer to the ID until ._set_client. - # We need the client to actually figure out its type. - super().__init__(chat_id) - - SenderGetter.__init__(self, user_id) - - self.online = None if status is None else \ - isinstance(status, types.UserStatusOnline) - - self.last_seen = status.was_online if \ - isinstance(status, types.UserStatusOffline) else None - - self.until = status.expires if \ - isinstance(status, types.UserStatusOnline) else None - - if self.last_seen: - now = datetime.datetime.now(tz=datetime.timezone.utc) - diff = now - self.last_seen - if diff < datetime.timedelta(days=30): - self.within_months = True - if diff < datetime.timedelta(days=7): - self.within_weeks = True - if diff < datetime.timedelta(days=1): - self.recently = True - else: - self.within_months = self.within_weeks = self.recently = False - if isinstance(status, (types.UserStatusOnline, - types.UserStatusRecently)): - self.within_months = self.within_weeks = True - self.recently = True - elif isinstance(status, types.UserStatusLastWeek): - self.within_months = self.within_weeks = True - elif isinstance(status, types.UserStatusLastMonth): - self.within_months = True - - self.action = typing - if typing: - self.cancel = self.typing = self.recording = self.uploading = \ - self.playing = False - self.audio = self.round = self.video = self.document = \ - self.geo = self.photo = self.contact = False - - if isinstance(typing, types.SendMessageCancelAction): - self.cancel = True - elif isinstance(typing, types.SendMessageTypingAction): - self.typing = True - elif isinstance(typing, types.SendMessageGamePlayAction): - self.playing = True - elif isinstance(typing, types.SendMessageGeoLocationAction): - self.geo = True - elif isinstance(typing, types.SendMessageRecordAudioAction): - self.recording = self.audio = True - elif isinstance(typing, types.SendMessageRecordRoundAction): - self.recording = self.round = True - elif isinstance(typing, types.SendMessageRecordVideoAction): - self.recording = self.video = True - elif isinstance(typing, types.SendMessageChooseContactAction): - self.uploading = self.contact = True - elif isinstance(typing, types.SendMessageUploadAudioAction): - self.uploading = self.audio = True - elif isinstance(typing, types.SendMessageUploadDocumentAction): - self.uploading = self.document = True - elif isinstance(typing, types.SendMessageUploadPhotoAction): - self.uploading = self.photo = True - elif isinstance(typing, types.SendMessageUploadRoundAction): - self.uploading = self.round = True - elif isinstance(typing, types.SendMessageUploadVideoAction): - self.uploading = self.video = True - - def _set_client(self, client): - if isinstance(self._chat_peer, int): - try: - chat = client._entity_cache[self._chat_peer] - if isinstance(chat, types.InputPeerChat): - self._chat_peer = types.PeerChat(self._chat_peer) - elif isinstance(chat, types.InputPeerChannel): - self._chat_peer = types.PeerChannel(self._chat_peer) - else: - # Should not happen - self._chat_peer = types.PeerUser(self._chat_peer) - except KeyError: - # Hope for the best. We don't know where this event - # occurred but it was most likely in a channel. - self._chat_peer = types.PeerChannel(self._chat_peer) - - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._entity_cache) - - @property - def user(self): - """Alias for `sender `.""" - return self.sender - - async def get_user(self): - """Alias for `get_sender `.""" - return await self.get_sender() - - @property - def input_user(self): - """Alias for `input_sender `.""" - return self.input_sender - - async def get_input_user(self): - """Alias for `get_input_sender `.""" - return await self.get_input_sender() - - @property - def user_id(self): - """Alias for `sender_id `.""" - return self.sender_id diff --git a/telethon/extensions/__init__.py b/telethon/extensions/__init__.py index 903460b6..e69de29b 100644 --- a/telethon/extensions/__init__.py +++ b/telethon/extensions/__init__.py @@ -1,6 +0,0 @@ -""" -Several extensions Python is missing, such as a proper class to handle a TCP -communication with support for cancelling the operation, and an utility class -to read arbitrary binary data in a more comfortable way, with int/strings/etc. -""" -from .binaryreader import BinaryReader diff --git a/telethon/extensions/binaryreader.py b/telethon/extensions/binaryreader.py index 5382caaf..e69de29b 100644 --- a/telethon/extensions/binaryreader.py +++ b/telethon/extensions/binaryreader.py @@ -1,195 +0,0 @@ -""" -This module contains the BinaryReader utility class. -""" -import os -from datetime import datetime, timezone -from io import BufferedReader, BytesIO -from struct import unpack - -from ..errors import TypeNotFoundError -from ..tl.alltlobjects import tlobjects -from ..tl.core import core_objects - - -class BinaryReader: - """ - Small utility class to read binary data. - Also creates a "Memory Stream" if necessary - """ - - def __init__(self, data=None, stream=None): - if data: - self.stream = BytesIO(data) - elif stream: - self.stream = stream - else: - raise ValueError('Either bytes or a stream must be provided') - - self.reader = BufferedReader(self.stream) - self._last = None # Should come in handy to spot -404 errors - - # region Reading - - # "All numbers are written as little endian." - # https://core.telegram.org/mtproto - def read_byte(self): - """Reads a single byte value.""" - return self.read(1)[0] - - def read_int(self, signed=True): - """Reads an integer (4 bytes) value.""" - return int.from_bytes(self.read(4), byteorder='little', signed=signed) - - def read_long(self, signed=True): - """Reads a long integer (8 bytes) value.""" - return int.from_bytes(self.read(8), byteorder='little', signed=signed) - - def read_float(self): - """Reads a real floating point (4 bytes) value.""" - return unpack(' 0: - padding = 4 - padding - self.read(padding) - - return data - - def tgread_string(self): - """Reads a Telegram-encoded string.""" - return str(self.tgread_bytes(), encoding='utf-8', errors='replace') - - def tgread_bool(self): - """Reads a Telegram boolean value.""" - value = self.read_int(signed=False) - if value == 0x997275b5: # boolTrue - return True - elif value == 0xbc799737: # boolFalse - return False - else: - raise RuntimeError('Invalid boolean code {}'.format(hex(value))) - - def tgread_date(self): - """Reads and converts Unix time (used by Telegram) - into a Python datetime object. - """ - value = self.read_int() - if value == 0: - return None - else: - return datetime.fromtimestamp(value, tz=timezone.utc) - - def tgread_object(self): - """Reads a Telegram object.""" - constructor_id = self.read_int(signed=False) - clazz = tlobjects.get(constructor_id, None) - if clazz is None: - # The class was None, but there's still a - # chance of it being a manually parsed value like bool! - value = constructor_id - if value == 0x997275b5: # boolTrue - return True - elif value == 0xbc799737: # boolFalse - return False - elif value == 0x1cb5c415: # Vector - return [self.tgread_object() for _ in range(self.read_int())] - - clazz = core_objects.get(constructor_id, None) - if clazz is None: - # If there was still no luck, give up - self.seek(-4) # Go back - pos = self.tell_position() - error = TypeNotFoundError(constructor_id, self.read()) - self.set_position(pos) - raise error - - return clazz.from_reader(self) - - def tgread_vector(self): - """Reads a vector (a list) of Telegram objects.""" - if 0x1cb5c415 != self.read_int(signed=False): - raise RuntimeError('Invalid constructor code, vector was expected') - - count = self.read_int() - return [self.tgread_object() for _ in range(count)] - - # endregion - - def close(self): - """Closes the reader, freeing the BytesIO stream.""" - self.reader.close() - - # region Position related - - def tell_position(self): - """Tells the current position on the stream.""" - return self.reader.tell() - - def set_position(self, position): - """Sets the current position on the stream.""" - self.reader.seek(position) - - def seek(self, offset): - """ - Seeks the stream position given an offset from the current position. - The offset may be negative. - """ - self.reader.seek(offset, os.SEEK_CUR) - - # endregion - - # region with block - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - # endregion diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py index 99f10dee..e69de29b 100644 --- a/telethon/extensions/html.py +++ b/telethon/extensions/html.py @@ -1,215 +0,0 @@ -""" -Simple HTML -> Telegram entity parser. -""" -import struct -from collections import deque -from html import escape, unescape -from html.parser import HTMLParser -from typing import Iterable, Optional, Tuple, List - -from .. import helpers -from ..tl.types import ( - MessageEntityBold, MessageEntityItalic, MessageEntityCode, - MessageEntityPre, MessageEntityEmail, MessageEntityUrl, - MessageEntityTextUrl, MessageEntityMentionName, - MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote, - TypeMessageEntity -) - - -# Helpers from markdown.py -def _add_surrogate(text): - return ''.join( - ''.join(chr(y) for y in struct.unpack(' tag, this tag is - # probably intended for syntax highlighting. - # - # Syntax highlighting is set with - # codeblock - # inside
 tags
-                pre = self._building_entities['pre']
-                try:
-                    pre.language = attrs['class'][len('language-'):]
-                except KeyError:
-                    pass
-            except KeyError:
-                EntityType = MessageEntityCode
-        elif tag == 'pre':
-            EntityType = MessageEntityPre
-            args['language'] = ''
-        elif tag == 'a':
-            try:
-                url = attrs['href']
-            except KeyError:
-                return
-            if url.startswith('mailto:'):
-                url = url[len('mailto:'):]
-                EntityType = MessageEntityEmail
-            else:
-                if self.get_starttag_text() == url:
-                    EntityType = MessageEntityUrl
-                else:
-                    EntityType = MessageEntityTextUrl
-                    args['url'] = url
-                    url = None
-            self._open_tags_meta.popleft()
-            self._open_tags_meta.appendleft(url)
-
-        if EntityType and tag not in self._building_entities:
-            self._building_entities[tag] = EntityType(
-                offset=len(self.text),
-                # The length will be determined when closing the tag.
-                length=0,
-                **args)
-
-    def handle_data(self, text):
-        text = unescape(text)
-
-        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
-        if previous_tag == 'a':
-            url = self._open_tags_meta[0]
-            if url:
-                text = url
-
-        for tag, entity in self._building_entities.items():
-            entity.length += len(text)
-
-        self.text += text
-
-    def handle_endtag(self, tag):
-        try:
-            self._open_tags.popleft()
-            self._open_tags_meta.popleft()
-        except IndexError:
-            pass
-        entity = self._building_entities.pop(tag, None)
-        if entity:
-            self.entities.append(entity)
-
-
-def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
-    """
-    Parses the given HTML message and returns its stripped representation
-    plus a list of the MessageEntity's that were found.
-
-    :param message: the message with HTML to be parsed.
-    :return: a tuple consisting of (clean message, [message entities]).
-    """
-    if not html:
-        return html, []
-
-    parser = HTMLToTelegramParser()
-    parser.feed(_add_surrogate(html))
-    text = helpers.strip_text(parser.text, parser.entities)
-    return _del_surrogate(text), parser.entities
-
-
-def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
-            _length: Optional[int] = None) -> str:
-    """
-    Performs the reverse operation to .parse(), effectively returning HTML
-    given a normal text and its MessageEntity's.
-
-    :param text: the text to be reconverted into HTML.
-    :param entities: the MessageEntity's applied to the text.
-    :return: a HTML representation of the combination of both inputs.
-    """
-    if not text:
-        return text
-    elif not entities:
-        return escape(text)
-
-    text = _add_surrogate(text)
-    if _length is None:
-        _length = len(text)
-    html = []
-    last_offset = 0
-    for i, entity in enumerate(entities):
-        if entity.offset > _offset + _length:
-            break
-        relative_offset = entity.offset - _offset
-        if relative_offset > last_offset:
-            html.append(escape(text[last_offset:relative_offset]))
-        elif relative_offset < last_offset:
-            continue
-
-        skip_entity = False
-        entity_text = unparse(text=text[relative_offset:relative_offset + entity.length],
-                              entities=entities[i + 1:],
-                              _offset=entity.offset, _length=entity.length)
-        entity_type = type(entity)
-
-        if entity_type == MessageEntityBold:
-            html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityItalic:
-            html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityCode:
-            html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityUnderline:
-            html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityStrike:
-            html.append('{}'.format(entity_text))
-        elif entity_type == MessageEntityBlockquote:
-            html.append('
{}
'.format(entity_text)) - elif entity_type == MessageEntityPre: - if entity.language: - html.append( - "
\n"
-                    "    \n"
-                    "        {}\n"
-                    "    \n"
-                    "
".format(entity.language, entity_text)) - else: - html.append('
{}
' - .format(entity_text)) - elif entity_type == MessageEntityEmail: - html.append('{0}'.format(entity_text)) - elif entity_type == MessageEntityUrl: - html.append('{0}'.format(entity_text)) - elif entity_type == MessageEntityTextUrl: - html.append('{}' - .format(escape(entity.url), entity_text)) - elif entity_type == MessageEntityMentionName: - html.append('{}' - .format(entity.user_id, entity_text)) - else: - skip_entity = True - last_offset = relative_offset + (0 if skip_entity else entity.length) - html.append(escape(text[last_offset:])) - return _del_surrogate(''.join(html)) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py index 2bbf4121..e69de29b 100644 --- a/telethon/extensions/markdown.py +++ b/telethon/extensions/markdown.py @@ -1,182 +0,0 @@ -""" -Simple markdown parser which does not support nesting. Intended primarily -for use within the library, which attempts to handle emojies correctly, -since they seem to count as two characters and it's a bit strange. -""" -import re -import warnings - -from ..helpers import add_surrogate, del_surrogate, strip_text -from ..tl import TLObject -from ..tl.types import ( - MessageEntityBold, MessageEntityItalic, MessageEntityCode, - MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName, - MessageEntityStrike -) - -DEFAULT_DELIMITERS = { - '**': MessageEntityBold, - '__': MessageEntityItalic, - '~~': MessageEntityStrike, - '`': MessageEntityCode, - '```': MessageEntityPre -} - -DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)') -DEFAULT_URL_FORMAT = '[{0}]({1})' - - -def overlap(a, b, x, y): - return max(a, x) < min(b, y) - - -def parse(message, delimiters=None, url_re=None): - """ - Parses the given markdown message and returns its stripped representation - plus a list of the MessageEntity's that were found. - - :param message: the message with markdown-like syntax to be parsed. - :param delimiters: the delimiters to be used, {delimiter: type}. - :param url_re: the URL bytes regex to be used. Must have two groups. - :return: a tuple consisting of (clean message, [message entities]). - """ - if not message: - return message, [] - - if url_re is None: - url_re = DEFAULT_URL_RE - elif isinstance(url_re, str): - url_re = re.compile(url_re) - - if not delimiters: - if delimiters is not None: - return message, [] - delimiters = DEFAULT_DELIMITERS - - # Build a regex to efficiently test all delimiters at once - delim_re = re.compile('|'.join('({})'.format(re.escape(k)) for k in delimiters)) - - # Cannot use a for loop because we need to skip some indices - i = 0 - result = [] - - # Work on byte level with the utf-16le encoding to get the offsets right. - # The offset will just be half the index we're at. - message = add_surrogate(message) - while i < len(message): - m = delim_re.match(message, pos=i) - - # Did we find some delimiter here at `i`? - if m: - delim = next(filter(None, m.groups())) - - # +1 to avoid matching right after (e.g. "****") - end = message.find(delim, i + len(delim) + 1) - - # Did we find the earliest closing tag? - if end != -1: - - # Remove the delimiter from the string - message = ''.join(( - message[:i], - message[i + len(delim):end], - message[end + len(delim):] - )) - - # Check other affected entities - for ent in result: - # If the end is after our start, it is affected - if ent.offset + ent.length > i: - ent.length -= len(delim) - - # Append the found entity - ent = delimiters[delim] - if ent == MessageEntityPre: - result.append(ent(i, end - i - len(delim), '')) # has 'lang' - else: - result.append(ent(i, end - i - len(delim))) - - # No nested entities inside code blocks - if ent in (MessageEntityCode, MessageEntityPre): - i = end - - continue - - elif url_re: - m = url_re.match(message, pos=i) - if m: - # Replace the whole match with only the inline URL text. - message = ''.join(( - message[:m.start()], - m.group(1), - message[m.end():] - )) - - delim_size = m.end() - m.start() - len(m.group()) - for ent in result: - # If the end is after our start, it is affected - if ent.offset + ent.length > m.start(): - ent.length -= delim_size - - result.append(MessageEntityTextUrl( - offset=m.start(), length=len(m.group(1)), - url=del_surrogate(m.group(2)) - )) - i += len(m.group(1)) - continue - - i += 1 - - message = strip_text(message, result) - return del_surrogate(message), result - - -def unparse(text, entities, delimiters=None, url_fmt=None): - """ - Performs the reverse operation to .parse(), effectively returning - markdown-like syntax given a normal text and its MessageEntity's. - - :param text: the text to be reconverted into markdown. - :param entities: the MessageEntity's applied to the text. - :return: a markdown-like text representing the combination of both inputs. - """ - if not text or not entities: - return text - - if not delimiters: - if delimiters is not None: - return text - delimiters = DEFAULT_DELIMITERS - - if url_fmt is not None: - warnings.warn('url_fmt is deprecated') # since it complicates everything *a lot* - - if isinstance(entities, TLObject): - entities = (entities,) - - text = add_surrogate(text) - delimiters = {v: k for k, v in delimiters.items()} - insert_at = [] - for entity in entities: - s = entity.offset - e = entity.offset + entity.length - delimiter = delimiters.get(type(entity), None) - if delimiter: - insert_at.append((s, delimiter)) - insert_at.append((e, delimiter)) - else: - url = None - if isinstance(entity, MessageEntityTextUrl): - url = entity.url - elif isinstance(entity, MessageEntityMentionName): - url = 'tg://user?id={}'.format(entity.user_id) - if url: - insert_at.append((s, '[')) - insert_at.append((e, ']({})'.format(url))) - - insert_at.sort(key=lambda t: t[0]) - while insert_at: - at, what = insert_at.pop() - text = text[:at] + what + text[at:] - - return del_surrogate(text) diff --git a/telethon/extensions/messagepacker.py b/telethon/extensions/messagepacker.py index 443a5f3e..e69de29b 100644 --- a/telethon/extensions/messagepacker.py +++ b/telethon/extensions/messagepacker.py @@ -1,112 +0,0 @@ -import asyncio -import collections -import io -import struct - -from ..tl import TLRequest -from ..tl.core.messagecontainer import MessageContainer -from ..tl.core.tlmessage import TLMessage - - -class MessagePacker: - """ - This class packs `RequestState` as outgoing `TLMessages`. - - The purpose of this class is to support putting N `RequestState` into a - queue, and then awaiting for "packed" `TLMessage` in the other end. The - simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but - for efficiency purposes it's ``States -> Container`` (N-to-1). - - This addresses several needs: outgoing messages will be smaller, so the - encryption and network overhead also is smaller. It's also a central - point where outgoing requests are put, and where ready-messages are get. - """ - - def __init__(self, state, loop, loggers): - self._state = state - self._loop = loop - self._deque = collections.deque() - self._ready = asyncio.Event(loop=loop) - self._log = loggers[__name__] - - def append(self, state): - self._deque.append(state) - self._ready.set() - - def extend(self, states): - self._deque.extend(states) - self._ready.set() - - async def get(self): - """ - Returns (batch, data) if one or more items could be retrieved. - - If the cancellation occurs or only invalid items were in the - queue, (None, None) will be returned instead. - """ - if not self._deque: - self._ready.clear() - await self._ready.wait() - - buffer = io.BytesIO() - batch = [] - size = 0 - - # Fill a new batch to return while the size is small enough, - # as long as we don't exceed the maximum length of messages. - while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH: - state = self._deque.popleft() - size += len(state.data) + TLMessage.SIZE_OVERHEAD - - if size <= MessageContainer.MAXIMUM_SIZE: - state.msg_id = self._state.write_data_as_message( - buffer, state.data, isinstance(state.request, TLRequest), - after_id=state.after.msg_id if state.after else None - ) - batch.append(state) - self._log.debug('Assigned msg_id = %d to %s (%x)', - state.msg_id, state.request.__class__.__name__, - id(state.request)) - continue - - if batch: - # Put the item back since it can't be sent in this batch - self._deque.appendleft(state) - break - - # If a single message exceeds the maximum size, then the - # message payload cannot be sent. Telegram would forcibly - # close the connection; message would never be confirmed. - # - # We don't put the item back because it can never be sent. - # If we did, we would loop again and reach this same path. - # Setting the exception twice results in `InvalidStateError` - # and this method should never return with error, which we - # really want to avoid. - self._log.warning( - 'Message payload for %s is too long (%d) and cannot be sent', - state.request.__class__.__name__, len(state.data) - ) - state.future.set_exception( - ValueError('Request payload is too big')) - - size = 0 - continue - - if not batch: - return None, None - - if len(batch) > 1: - # Inlined code to pack several messages into a container - data = struct.pack( - ' Surrogate Pairs (Telegram offsets are calculated with these). - # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. - ''.join(chr(y) for y in struct.unpack('= (3, 7): - try: - await self._writer.wait_closed() - except Exception as e: - # Seen OSError: No route to host - # Disconnecting should never raise - self._log.warning('Unhandled %s on disconnect: %s', type(e), e) - - def send(self, data): - """ - Sends a packet of data through this connection mode. - - This method returns a coroutine. - """ - if not self._connected: - raise ConnectionError('Not connected') - - return self._send_queue.put(data) - - async def recv(self): - """ - Receives a packet of data through this connection mode. - - This method returns a coroutine. - """ - while self._connected: - result = await self._recv_queue.get() - if result: # None = sentinel value = keep trying - return result - - raise ConnectionError('Not connected') - - async def _send_loop(self): - """ - This loop is constantly popping items off the queue to send them. - """ - try: - while self._connected: - self._send(await self._send_queue.get()) - await self._writer.drain() - except asyncio.CancelledError: - pass - except Exception as e: - if isinstance(e, IOError): - self._log.info('The server closed the connection while sending') - else: - self._log.exception('Unexpected exception in the send loop') - - await self.disconnect() - - async def _recv_loop(self): - """ - This loop is constantly putting items on the queue as they're read. - """ - while self._connected: - try: - data = await self._recv() - except asyncio.CancelledError: - break - except Exception as e: - if isinstance(e, (IOError, asyncio.IncompleteReadError)): - msg = 'The server closed the connection' - self._log.info(msg) - elif isinstance(e, InvalidChecksumError): - msg = 'The server response had an invalid checksum' - self._log.info(msg) - else: - msg = 'Unexpected exception in the receive loop' - self._log.exception(msg) - - await self.disconnect() - - # Add a sentinel value to unstuck recv - if self._recv_queue.empty(): - self._recv_queue.put_nowait(None) - - break - - try: - await self._recv_queue.put(data) - except asyncio.CancelledError: - break - - def _init_conn(self): - """ - This method will be called after `connect` is called. - After this method finishes, the writer will be drained. - - Subclasses should make use of this if they need to send - data to Telegram to indicate which connection mode will - be used. - """ - if self._codec.tag: - self._writer.write(self._codec.tag) - - def _send(self, data): - self._writer.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._reader) - - def __str__(self): - return '{}:{}/{}'.format( - self._ip, self._port, - self.__class__.__name__.replace('Connection', '') - ) - - -class ObfuscatedConnection(Connection): - """ - Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") - """ - """ - This attribute should be redefined by subclasses - """ - obfuscated_io = None - - def _init_conn(self): - self._obfuscation = self.obfuscated_io(self) - self._writer.write(self._obfuscation.header) - - def _send(self, data): - self._obfuscation.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._obfuscation) - - -class PacketCodec(abc.ABC): - """ - Base class for packet codecs - """ - - """ - This attribute should be re-defined by subclass to define if some - "magic bytes" should be sent to server right after conection is made to - signal which protocol will be used - """ - tag = None - - def __init__(self, connection): - """ - Codec is created when connection is just made. - """ - self._conn = connection - - @abc.abstractmethod - def encode_packet(self, data): - """ - Encodes single packet and returns encoded bytes. - """ - raise NotImplementedError - - @abc.abstractmethod - async def read_packet(self, reader): - """ - Reads single packet from `reader` object that should have - `readexactly(n)` method. - """ - raise NotImplementedError diff --git a/telethon/network/connection/http.py b/telethon/network/connection/http.py index e2d976f7..e69de29b 100644 --- a/telethon/network/connection/http.py +++ b/telethon/network/connection/http.py @@ -1,39 +0,0 @@ -import asyncio - -from .connection import Connection, PacketCodec - - -SSL_PORT = 443 - - -class HttpPacketCodec(PacketCodec): - tag = None - obfuscate_tag = None - - def encode_packet(self, data): - return ('POST /api HTTP/1.1\r\n' - 'Host: {}:{}\r\n' - 'Content-Type: application/x-www-form-urlencoded\r\n' - 'Connection: keep-alive\r\n' - 'Keep-Alive: timeout=100000, max=10000000\r\n' - 'Content-Length: {}\r\n\r\n' - .format(self._conn._ip, self._conn._port, len(data)) - .encode('ascii') + data) - - async def read_packet(self, reader): - while True: - line = await reader.readline() - if not line or line[-1] != b'\n': - raise asyncio.IncompleteReadError(line, None) - - if line.lower().startswith(b'content-length: '): - await reader.readexactly(2) - length = int(line[16:-2]) - return await reader.readexactly(length) - - -class ConnectionHttp(Connection): - packet_codec = HttpPacketCodec - - async def connect(self, timeout=None, ssl=None): - await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) diff --git a/telethon/network/connection/tcpabridged.py b/telethon/network/connection/tcpabridged.py index 171b1d8c..e69de29b 100644 --- a/telethon/network/connection/tcpabridged.py +++ b/telethon/network/connection/tcpabridged.py @@ -1,33 +0,0 @@ -import struct - -from .connection import Connection, PacketCodec - - -class AbridgedPacketCodec(PacketCodec): - tag = b'\xef' - obfuscate_tag = b'\xef\xef\xef\xef' - - def encode_packet(self, data): - length = len(data) >> 2 - if length < 127: - length = struct.pack('B', length) - else: - length = b'\x7f' + int.to_bytes(length, 3, 'little') - return length + data - - async def read_packet(self, reader): - length = struct.unpack('= 127: - length = struct.unpack( - ' 0: - return packet_with_padding[:-pad_size] - return packet_with_padding - - -class ConnectionTcpIntermediate(Connection): - """ - Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. - Always sends 4 extra bytes for the packet length. - """ - packet_codec = IntermediatePacketCodec diff --git a/telethon/network/connection/tcpmtproxy.py b/telethon/network/connection/tcpmtproxy.py index 2a9438ab..e69de29b 100644 --- a/telethon/network/connection/tcpmtproxy.py +++ b/telethon/network/connection/tcpmtproxy.py @@ -1,152 +0,0 @@ -import asyncio -import hashlib -import os - -from .connection import ObfuscatedConnection -from .tcpabridged import AbridgedPacketCodec -from .tcpintermediate import ( - IntermediatePacketCodec, - RandomizedIntermediatePacketCodec -) - -from ...crypto import AESModeCTR - - -class MTProxyIO: - """ - It's very similar to tcpobfuscated.ObfuscatedIO, but the way - encryption keys, protocol tag and dc_id are encoded is different. - """ - header = None - - def __init__(self, connection): - self._reader = connection._reader - self._writer = connection._writer - - (self.header, - self._encrypt, - self._decrypt) = self.init_header( - connection._secret, connection._dc_id, connection.packet_codec) - - @staticmethod - def init_header(secret, dc_id, packet_codec): - # Validate - is_dd = (len(secret) == 17) and (secret[0] == 0xDD) - is_rand_codec = issubclass( - packet_codec, RandomizedIntermediatePacketCodec) - if is_dd and not is_rand_codec: - raise ValueError( - "Only RandomizedIntermediate can be used with dd-secrets") - secret = secret[1:] if is_dd else secret - if len(secret) != 16: - raise ValueError( - "MTProxy secret must be a hex-string representing 16 bytes") - - # Obfuscated messages secrets cannot start with any of these - keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') - while True: - random = os.urandom(64) - if (random[0] != 0xef and - random[:4] not in keywords and - random[4:4] != b'\0\0\0\0'): - break - - random = bytearray(random) - random_reversed = random[55:7:-1] # Reversed (8, len=48) - - # Encryption has "continuous buffer" enabled - encrypt_key = hashlib.sha256( - bytes(random[8:40]) + secret).digest() - encrypt_iv = bytes(random[40:56]) - decrypt_key = hashlib.sha256( - bytes(random_reversed[:32]) + secret).digest() - decrypt_iv = bytes(random_reversed[32:48]) - - encryptor = AESModeCTR(encrypt_key, encrypt_iv) - decryptor = AESModeCTR(decrypt_key, decrypt_iv) - - random[56:60] = packet_codec.obfuscate_tag - - dc_id_bytes = dc_id.to_bytes(2, "little", signed=True) - random = random[:60] + dc_id_bytes + random[62:] - random[56:64] = encryptor.encrypt(bytes(random))[56:64] - return (random, encryptor, decryptor) - - async def readexactly(self, n): - return self._decrypt.encrypt(await self._reader.readexactly(n)) - - def write(self, data): - self._writer.write(self._encrypt.encrypt(data)) - - -class TcpMTProxy(ObfuscatedConnection): - """ - Connector which allows user to connect to the Telegram via proxy servers - commonly known as MTProxy. - Implemented very ugly due to the leaky abstractions in Telethon networking - classes that should be refactored later (TODO). - - .. warning:: - - The support for TcpMTProxy classes is **EXPERIMENTAL** and prone to - be changed. You shouldn't be using this class yet. - """ - packet_codec = None - obfuscated_io = MTProxyIO - - # noinspection PyUnusedLocal - def __init__(self, ip, port, dc_id, *, loop, loggers, proxy=None): - # connect to proxy's host and port instead of telegram's ones - proxy_host, proxy_port = self.address_info(proxy) - self._secret = bytes.fromhex(proxy[2]) - super().__init__( - proxy_host, proxy_port, dc_id, loop=loop, loggers=loggers) - - async def _connect(self, timeout=None, ssl=None): - await super()._connect(timeout=timeout, ssl=ssl) - - # Wait for EOF for 2 seconds (or if _wait_for_data's definition - # is missing or different, just sleep for 2 seconds). This way - # we give the proxy a chance to close the connection if the current - # codec (which the proxy detects with the data we sent) cannot - # be used for this proxy. This is a work around for #1134. - # TODO Sleeping for N seconds may not be the best solution - # TODO This fix could be welcome for HTTP proxies as well - try: - await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2) - except asyncio.TimeoutError: - pass - except Exception: - await asyncio.sleep(2) - - if self._reader.at_eof(): - await self.disconnect() - raise ConnectionError( - 'Proxy closed the connection after sending initial payload') - - @staticmethod - def address_info(proxy_info): - if proxy_info is None: - raise ValueError("No proxy info specified for MTProxy connection") - return proxy_info[:2] - - -class ConnectionTcpMTProxyAbridged(TcpMTProxy): - """ - Connect to proxy using abridged protocol - """ - packet_codec = AbridgedPacketCodec - - -class ConnectionTcpMTProxyIntermediate(TcpMTProxy): - """ - Connect to proxy using intermediate protocol - """ - packet_codec = IntermediatePacketCodec - - -class ConnectionTcpMTProxyRandomizedIntermediate(TcpMTProxy): - """ - Connect to proxy using randomized intermediate protocol (dd-secrets) - """ - packet_codec = RandomizedIntermediatePacketCodec diff --git a/telethon/network/connection/tcpobfuscated.py b/telethon/network/connection/tcpobfuscated.py index 6cc10094..e69de29b 100644 --- a/telethon/network/connection/tcpobfuscated.py +++ b/telethon/network/connection/tcpobfuscated.py @@ -1,62 +0,0 @@ -import os - -from .tcpabridged import AbridgedPacketCodec -from .connection import ObfuscatedConnection - -from ...crypto import AESModeCTR - - -class ObfuscatedIO: - header = None - - def __init__(self, connection): - self._reader = connection._reader - self._writer = connection._writer - - (self.header, - self._encrypt, - self._decrypt) = self.init_header(connection.packet_codec) - - @staticmethod - def init_header(packet_codec): - # Obfuscated messages secrets cannot start with any of these - keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') - while True: - random = os.urandom(64) - if (random[0] != 0xef and - random[:4] not in keywords and - random[4:4] != b'\0\0\0\0'): - break - - random = bytearray(random) - random_reversed = random[55:7:-1] # Reversed (8, len=48) - - # Encryption has "continuous buffer" enabled - encrypt_key = bytes(random[8:40]) - encrypt_iv = bytes(random[40:56]) - decrypt_key = bytes(random_reversed[:32]) - decrypt_iv = bytes(random_reversed[32:48]) - - encryptor = AESModeCTR(encrypt_key, encrypt_iv) - decryptor = AESModeCTR(decrypt_key, decrypt_iv) - - random[56:60] = packet_codec.obfuscate_tag - random[56:64] = encryptor.encrypt(bytes(random))[56:64] - return (random, encryptor, decryptor) - - async def readexactly(self, n): - return self._decrypt.encrypt(await self._reader.readexactly(n)) - - def write(self, data): - self._writer.write(self._encrypt.encrypt(data)) - - -class ConnectionTcpObfuscated(ObfuscatedConnection): - """ - Mode that Telegram defines as "obfuscated2". Encodes the packet - just like `ConnectionTcpAbridged`, but encrypts every message with - a randomly generated key using the AES-CTR mode so the packets are - harder to discern. - """ - obfuscated_io = ObfuscatedIO - packet_codec = AbridgedPacketCodec diff --git a/telethon/network/mtprotoplainsender.py b/telethon/network/mtprotoplainsender.py index 563affd7..e69de29b 100644 --- a/telethon/network/mtprotoplainsender.py +++ b/telethon/network/mtprotoplainsender.py @@ -1,56 +0,0 @@ -""" -This module contains the class used to communicate with Telegram's servers -in plain text, when no authorization key has been created yet. -""" -import struct - -from .mtprotostate import MTProtoState -from ..errors import InvalidBufferError -from ..extensions import BinaryReader - - -class MTProtoPlainSender: - """ - MTProto Mobile Protocol plain sender - (https://core.telegram.org/mtproto/description#unencrypted-messages) - """ - def __init__(self, connection, *, loggers): - """ - Initializes the MTProto plain sender. - - :param connection: the Connection to be used. - """ - self._state = MTProtoState(auth_key=None, loggers=loggers) - self._connection = connection - - async def send(self, request): - """ - Sends and receives the result for the given request. - """ - body = bytes(request) - msg_id = self._state._get_new_msg_id() - await self._connection.send( - struct.pack(' 0, 'Bad length' - # We could read length bytes and use those in a new reader to read - # the next TLObject without including the padding, but since the - # reader isn't used for anything else after this, it's unnecessary. - return reader.tgread_object() diff --git a/telethon/network/mtprotosender.py b/telethon/network/mtprotosender.py index 6b5f6585..e69de29b 100644 --- a/telethon/network/mtprotosender.py +++ b/telethon/network/mtprotosender.py @@ -1,706 +0,0 @@ -import asyncio -import collections - -from . import authenticator -from ..extensions.messagepacker import MessagePacker -from .mtprotoplainsender import MTProtoPlainSender -from .requeststate import RequestState -from .mtprotostate import MTProtoState -from ..tl.tlobject import TLRequest -from .. import helpers, utils -from ..errors import ( - BadMessageError, InvalidBufferError, SecurityError, - TypeNotFoundError, rpc_message_to_error -) -from ..extensions import BinaryReader -from ..tl.core import RpcResult, MessageContainer, GzipPacked -from ..tl.functions.auth import LogOutRequest -from ..tl.types import ( - MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, - MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq, - MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload -) -from ..crypto import AuthKey -from ..helpers import retry_range - - -class MTProtoSender: - """ - MTProto Mobile Protocol sender - (https://core.telegram.org/mtproto/description). - - This class is responsible for wrapping requests into `TLMessage`'s, - sending them over the network and receiving them in a safe manner. - - Automatic reconnection due to temporary network issues is a concern - for this class as well, including retry of messages that could not - be sent successfully. - - A new authorization key will be generated on connection if no other - key exists yet. - """ - def __init__(self, auth_key, loop, *, loggers, - retries=5, delay=1, auto_reconnect=True, connect_timeout=None, - auth_key_callback=None, - update_callback=None, auto_reconnect_callback=None): - self._connection = None - self._loop = loop - self._loggers = loggers - self._log = loggers[__name__] - self._retries = retries - self._delay = delay - self._auto_reconnect = auto_reconnect - self._connect_timeout = connect_timeout - self._auth_key_callback = auth_key_callback - self._update_callback = update_callback - self._auto_reconnect_callback = auto_reconnect_callback - - # Whether the user has explicitly connected or disconnected. - # - # If a disconnection happens for any other reason and it - # was *not* user action then the pending messages won't - # be cleared but on explicit user disconnection all the - # pending futures should be cancelled. - self._user_connected = False - self._reconnecting = False - self._disconnected = self._loop.create_future() - self._disconnected.set_result(None) - - # We need to join the loops upon disconnection - self._send_loop_handle = None - self._recv_loop_handle = None - - # Preserving the references of the AuthKey and state is important - self.auth_key = auth_key or AuthKey(None) - self._state = MTProtoState(self.auth_key, loggers=self._loggers) - - # Outgoing messages are put in a queue and sent in a batch. - # Note that here we're also storing their ``_RequestState``. - self._send_queue = MessagePacker(self._state, self._loop, - loggers=self._loggers) - - # Sent states are remembered until a response is received. - self._pending_state = {} - - # Responses must be acknowledged, and we can also batch these. - self._pending_ack = set() - - # Similar to pending_messages but only for the last acknowledges. - # These can't go in pending_messages because no acknowledge for them - # is received, but we may still need to resend their state on bad salts. - self._last_acks = collections.deque(maxlen=10) - - # Jump table from response ID to method that handles it - self._handlers = { - RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, - MessageContainer.CONSTRUCTOR_ID: self._handle_container, - GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed, - Pong.CONSTRUCTOR_ID: self._handle_pong, - BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, - BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, - MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, - MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, - NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, - MsgsAck.CONSTRUCTOR_ID: self._handle_ack, - FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, - MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, - } - - # Public API - - async def connect(self, connection): - """ - Connects to the specified given connection using the given auth key. - """ - if self._user_connected: - self._log.info('User is already connected!') - return - - self._connection = connection - await self._connect() - self._user_connected = True - - def is_connected(self): - return self._user_connected - - async def disconnect(self): - """ - Cleanly disconnects the instance from the network, cancels - all pending requests, and closes the send and receive loops. - """ - await self._disconnect() - - def send(self, request, ordered=False): - """ - This method enqueues the given request to be sent. Its send - state will be saved until a response arrives, and a ``Future`` - that will be resolved when the response arrives will be returned: - - .. code-block:: python - - async def method(): - # Sending (enqueued for the send loop) - future = sender.send(request) - # Receiving (waits for the receive loop to read the result) - result = await future - - Designed like this because Telegram may send the response at - any point, and it can send other items while one waits for it. - Once the response for this future arrives, it is set with the - received result, quite similar to how a ``receive()`` call - would otherwise work. - - Since the receiving part is "built in" the future, it's - impossible to await receive a result that was never sent. - """ - if not self._user_connected: - raise ConnectionError('Cannot send requests while disconnected') - - if not utils.is_list_like(request): - state = RequestState(request, self._loop) - self._send_queue.append(state) - return state.future - else: - states = [] - futures = [] - state = None - for req in request: - state = RequestState(req, self._loop, after=ordered and state) - states.append(state) - futures.append(state.future) - - self._send_queue.extend(states) - return futures - - @property - def disconnected(self): - """ - Future that resolves when the connection to Telegram - ends, either by user action or in the background. - - Note that it may resolve in either a ``ConnectionError`` - or any other unexpected error that could not be handled. - """ - return asyncio.shield(self._disconnected, loop=self._loop) - - # Private methods - - async def _connect(self): - """ - Performs the actual connection, retrying, generating the - authorization key if necessary, and starting the send and - receive loops. - """ - self._log.info('Connecting to %s...', self._connection) - for attempt in retry_range(self._retries): - try: - self._log.debug('Connection attempt %d...', attempt) - await self._connection.connect(timeout=self._connect_timeout) - except (IOError, asyncio.TimeoutError) as e: - self._log.warning('Attempt %d at connecting failed: %s: %s', - attempt, type(e).__name__, e) - await asyncio.sleep(self._delay) - else: - break - else: - raise ConnectionError('Connection to Telegram failed %d time(s)', attempt) - - self._log.debug('Connection success!') - if not self.auth_key: - plain = MTProtoPlainSender(self._connection, loggers=self._loggers) - for attempt in retry_range(self._retries): - try: - self._log.debug('New auth_key attempt {}...' - .format(attempt)) - self.auth_key.key, self._state.time_offset =\ - await authenticator.do_authentication(plain) - - # This is *EXTREMELY* important since we don't control - # external references to the authorization key, we must - # notify whenever we change it. This is crucial when we - # switch to different data centers. - if self._auth_key_callback: - self._auth_key_callback(self.auth_key) - - break - except (SecurityError, AssertionError) as e: - self._log.warning('Attempt %d at new auth_key failed: %s', attempt, e) - await asyncio.sleep(self._delay) - else: - e = ConnectionError('auth_key generation failed %d time(s)', attempt) - await self._disconnect(error=e) - raise e - - self._log.debug('Starting send loop') - self._send_loop_handle = self._loop.create_task(self._send_loop()) - - self._log.debug('Starting receive loop') - self._recv_loop_handle = self._loop.create_task(self._recv_loop()) - - # _disconnected only completes after manual disconnection - # or errors after which the sender cannot continue such - # as failing to reconnect or any unexpected error. - if self._disconnected.done(): - self._disconnected = self._loop.create_future() - - self._log.info('Connection to %s complete!', self._connection) - - async def _disconnect(self, error=None): - if self._connection is None: - self._log.info('Not disconnecting (already have no connection)') - return - - self._log.info('Disconnecting from %s...', self._connection) - self._user_connected = False - try: - self._log.debug('Closing current connection...') - await self._connection.disconnect() - finally: - self._connection = None - self._log.debug('Cancelling %d pending message(s)...', len(self._pending_state)) - for state in self._pending_state.values(): - if error and not state.future.done(): - state.future.set_exception(error) - else: - state.future.cancel() - - self._pending_state.clear() - await helpers._cancel( - self._log, - send_loop_handle=self._send_loop_handle, - recv_loop_handle=self._recv_loop_handle - ) - - self._log.info('Disconnection from %s complete!', self._connection) - if self._disconnected and not self._disconnected.done(): - if error: - self._disconnected.set_exception(error) - else: - self._disconnected.set_result(None) - - async def _reconnect(self, last_error): - """ - Cleanly disconnects and then reconnects. - """ - self._log.debug('Closing current connection...') - await self._connection.disconnect() - - await helpers._cancel( - self._log, - send_loop_handle=self._send_loop_handle, - recv_loop_handle=self._recv_loop_handle - ) - - # TODO See comment in `_start_reconnect` - # Perhaps this should be the last thing to do? - # But _connect() creates tasks which may run and, - # if they see that reconnecting is True, they will end. - # Perhaps that task creation should not belong in connect? - self._reconnecting = False - - # Start with a clean state (and thus session ID) to avoid old msgs - self._state.reset() - - retries = self._retries if self._auto_reconnect else 0 - for attempt in retry_range(retries): - try: - await self._connect() - except (IOError, asyncio.TimeoutError) as e: - last_error = e - self._log.info('Failed reconnection attempt %d with %s', - attempt, e.__class__.__name__) - - await asyncio.sleep(self._delay) - except Exception as e: - last_error = e - self._log.exception('Unexpected exception reconnecting on ' - 'attempt %d', attempt) - - await asyncio.sleep(self._delay) - else: - self._send_queue.extend(self._pending_state.values()) - self._pending_state.clear() - - if self._auto_reconnect_callback: - self._loop.create_task(self._auto_reconnect_callback()) - - break - else: - self._log.error('Automatic reconnection failed %d time(s)', attempt) - await self._disconnect(error=last_error.with_traceback(None)) - - def _start_reconnect(self, error): - """Starts a reconnection in the background.""" - if self._user_connected and not self._reconnecting: - # We set reconnecting to True here and not inside the new task - # because it may happen that send/recv loop calls this again - # while the new task hasn't had a chance to run yet. This race - # condition puts `self.connection` in a bad state with two calls - # to its `connect` without disconnecting, so it creates a second - # receive loop. There can't be two tasks receiving data from - # the reader, since that causes an error, and the library just - # gets stuck. - # TODO It still gets stuck? Investigate where and why. - self._reconnecting = True - self._loop.create_task(self._reconnect(error)) - - # Loops - - async def _send_loop(self): - """ - This loop is responsible for popping items off the send - queue, encrypting them, and sending them over the network. - - Besides `connect`, only this method ever sends data. - """ - while self._user_connected and not self._reconnecting: - if self._pending_ack: - ack = RequestState(MsgsAck(list(self._pending_ack)), self._loop) - self._send_queue.append(ack) - self._last_acks.append(ack) - self._pending_ack.clear() - - self._log.debug('Waiting for messages to send...') - # TODO Wait for the connection send queue to be empty? - # This means that while it's not empty we can wait for - # more messages to be added to the send queue. - batch, data = await self._send_queue.get() - - if not data: - continue - - self._log.debug('Encrypting %d message(s) in %d bytes for sending', - len(batch), len(data)) - - data = self._state.encrypt_message_data(data) - try: - await self._connection.send(data) - except IOError as e: - self._log.info('Connection closed while sending data') - self._start_reconnect(e) - return - - for state in batch: - if not isinstance(state, list): - if isinstance(state.request, TLRequest): - self._pending_state[state.msg_id] = state - else: - for s in state: - if isinstance(s.request, TLRequest): - self._pending_state[s.msg_id] = s - - self._log.debug('Encrypted messages put in a queue to be sent') - - async def _recv_loop(self): - """ - This loop is responsible for reading all incoming responses - from the network, decrypting and handling or dispatching them. - - Besides `connect`, only this method ever receives data. - """ - while self._user_connected and not self._reconnecting: - self._log.debug('Receiving items from the network...') - try: - body = await self._connection.recv() - except IOError as e: - self._log.info('Connection closed while receiving data') - self._start_reconnect(e) - return - - try: - message = self._state.decrypt_message_data(body) - except TypeNotFoundError as e: - # Received object which we don't know how to deserialize - self._log.info('Type %08x not found, remaining data %r', - e.invalid_constructor_id, e.remaining) - continue - except SecurityError as e: - # A step while decoding had the incorrect data. This message - # should not be considered safe and it should be ignored. - self._log.warning('Security error while unpacking a ' - 'received message: %s', e) - continue - except BufferError as e: - if isinstance(e, InvalidBufferError) and e.code == 404: - self._log.info('Broken authorization key; resetting') - else: - self._log.warning('Invalid buffer %s', e) - - self.auth_key.key = None - if self._auth_key_callback: - self._auth_key_callback(None) - - self._start_reconnect(e) - return - except Exception as e: - self._log.exception('Unhandled error while receiving data') - self._start_reconnect(e) - return - - try: - await self._process_message(message) - except Exception: - self._log.exception('Unhandled error while processing msgs') - - # Response Handlers - - async def _process_message(self, message): - """ - Adds the given message to the list of messages that must be - acknowledged and dispatches control to different ``_handle_*`` - method based on its type. - """ - self._pending_ack.add(message.msg_id) - handler = self._handlers.get(message.obj.CONSTRUCTOR_ID, - self._handle_update) - await handler(message) - - def _pop_states(self, msg_id): - """ - Pops the states known to match the given ID from pending messages. - - This method should be used when the response isn't specific. - """ - state = self._pending_state.pop(msg_id, None) - if state: - return [state] - - to_pop = [] - for state in self._pending_state.values(): - if state.container_id == msg_id: - to_pop.append(state.msg_id) - - if to_pop: - return [self._pending_state.pop(x) for x in to_pop] - - for ack in self._last_acks: - if ack.msg_id == msg_id: - return [ack] - - return [] - - async def _handle_rpc_result(self, message): - """ - Handles the result for Remote Procedure Calls: - - rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; - - This is where the future results for sent requests are set. - """ - rpc_result = message.obj - state = self._pending_state.pop(rpc_result.req_msg_id, None) - self._log.debug('Handling RPC result for message %d', - rpc_result.req_msg_id) - - if not state: - # TODO We should not get responses to things we never sent - # However receiving a File() with empty bytes is "common". - # See #658, #759 and #958. They seem to happen in a container - # which contain the real response right after. - try: - with BinaryReader(rpc_result.body) as reader: - if not isinstance(reader.tgread_object(), upload.File): - raise ValueError('Not an upload.File') - except (TypeNotFoundError, ValueError): - self._log.info('Received response without parent request: %s', rpc_result.body) - return - - if rpc_result.error: - error = rpc_message_to_error(rpc_result.error, state.request) - self._send_queue.append( - RequestState(MsgsAck([state.msg_id]), loop=self._loop)) - - if not state.future.cancelled(): - state.future.set_exception(error) - else: - with BinaryReader(rpc_result.body) as reader: - result = state.request.read_result(reader) - - if not state.future.cancelled(): - state.future.set_result(result) - - async def _handle_container(self, message): - """ - Processes the inner messages of a container with many of them: - - msg_container#73f1f8dc messages:vector<%Message> = MessageContainer; - """ - self._log.debug('Handling container') - for inner_message in message.obj.messages: - await self._process_message(inner_message) - - async def _handle_gzip_packed(self, message): - """ - Unpacks the data from a gzipped object and processes it: - - gzip_packed#3072cfa1 packed_data:bytes = Object; - """ - self._log.debug('Handling gzipped data') - with BinaryReader(message.obj.data) as reader: - message.obj = reader.tgread_object() - await self._process_message(message) - - async def _handle_update(self, message): - try: - assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates') - except AssertionError: - self._log.warning('Note: %s is not an update, not dispatching it %s', message.obj) - return - - self._log.debug('Handling update %s', message.obj.__class__.__name__) - if self._update_callback: - self._update_callback(message.obj) - - async def _handle_pong(self, message): - """ - Handles pong results, which don't come inside a ``rpc_result`` - but are still sent through a request: - - pong#347773c5 msg_id:long ping_id:long = Pong; - """ - pong = message.obj - self._log.debug('Handling pong for message %d', pong.msg_id) - state = self._pending_state.pop(pong.msg_id, None) - if state: - state.future.set_result(pong) - - async def _handle_bad_server_salt(self, message): - """ - Corrects the currently used server salt to use the right value - before enqueuing the rejected message to be re-sent: - - bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int - error_code:int new_server_salt:long = BadMsgNotification; - """ - bad_salt = message.obj - self._log.debug('Handling bad salt for message %d', bad_salt.bad_msg_id) - self._state.salt = bad_salt.new_server_salt - states = self._pop_states(bad_salt.bad_msg_id) - self._send_queue.extend(states) - - self._log.debug('%d message(s) will be resent', len(states)) - - async def _handle_bad_notification(self, message): - """ - Adjusts the current state to be correct based on the - received bad message notification whenever possible: - - bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int - error_code:int = BadMsgNotification; - """ - bad_msg = message.obj - states = self._pop_states(bad_msg.bad_msg_id) - - self._log.debug('Handling bad msg %s', bad_msg) - if bad_msg.error_code in (16, 17): - # Sent msg_id too low or too high (respectively). - # Use the current msg_id to determine the right time offset. - to = self._state.update_time_offset( - correct_msg_id=message.msg_id) - self._log.info('System clock is wrong, set time offset to %ds', to) - elif bad_msg.error_code == 32: - # msg_seqno too low, so just pump it up by some "large" amount - # TODO A better fix would be to start with a new fresh session ID - self._state._sequence += 64 - elif bad_msg.error_code == 33: - # msg_seqno too high never seems to happen but just in case - self._state._sequence -= 16 - else: - for state in states: - state.future.set_exception( - BadMessageError(state.request, bad_msg.error_code)) - return - - # Messages are to be re-sent once we've corrected the issue - self._send_queue.extend(states) - self._log.debug('%d messages will be resent due to bad msg', - len(states)) - - async def _handle_detailed_info(self, message): - """ - Updates the current status with the received detailed information: - - msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long - bytes:int status:int = MsgDetailedInfo; - """ - # TODO https://goo.gl/VvpCC6 - msg_id = message.obj.answer_msg_id - self._log.debug('Handling detailed info for message %d', msg_id) - self._pending_ack.add(msg_id) - - async def _handle_new_detailed_info(self, message): - """ - Updates the current status with the received detailed information: - - msg_new_detailed_info#809db6df answer_msg_id:long - bytes:int status:int = MsgDetailedInfo; - """ - # TODO https://goo.gl/G7DPsR - msg_id = message.obj.answer_msg_id - self._log.debug('Handling new detailed info for message %d', msg_id) - self._pending_ack.add(msg_id) - - async def _handle_new_session_created(self, message): - """ - Updates the current status with the received session information: - - new_session_created#9ec20908 first_msg_id:long unique_id:long - server_salt:long = NewSession; - """ - # TODO https://goo.gl/LMyN7A - self._log.debug('Handling new session created') - self._state.salt = message.obj.server_salt - - async def _handle_ack(self, message): - """ - Handles a server acknowledge about our messages. Normally - these can be ignored except in the case of ``auth.logOut``: - - auth.logOut#5717da40 = Bool; - - Telegram doesn't seem to send its result so we need to confirm - it manually. No other request is known to have this behaviour. - - Since the ID of sent messages consisting of a container is - never returned (unless on a bad notification), this method - also removes containers messages when any of their inner - messages are acknowledged. - """ - ack = message.obj - self._log.debug('Handling acknowledge for %s', str(ack.msg_ids)) - for msg_id in ack.msg_ids: - state = self._pending_state.get(msg_id) - if state and isinstance(state.request, LogOutRequest): - del self._pending_state[msg_id] - state.future.set_result(True) - - async def _handle_future_salts(self, message): - """ - Handles future salt results, which don't come inside a - ``rpc_result`` but are still sent through a request: - - future_salts#ae500895 req_msg_id:long now:int - salts:vector = FutureSalts; - """ - # TODO save these salts and automatically adjust to the - # correct one whenever the salt in use expires. - self._log.debug('Handling future salts for message %d', message.msg_id) - state = self._pending_state.pop(message.msg_id, None) - if state: - state.future.set_result(message.obj) - - async def _handle_state_forgotten(self, message): - """ - Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by - enqueuing a :tl:`MsgsStateInfo` to be sent at a later point. - """ - self._send_queue.append(RequestState(MsgsStateInfo( - req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids)), - loop=self._loop)) - - async def _handle_msg_all(self, message): - """ - Handles :tl:`MsgsAllInfo` by doing nothing (yet). - """ diff --git a/telethon/network/mtprotostate.py b/telethon/network/mtprotostate.py index 05090260..e69de29b 100644 --- a/telethon/network/mtprotostate.py +++ b/telethon/network/mtprotostate.py @@ -1,203 +0,0 @@ -import os -import struct -import time -from hashlib import sha256 - -from ..crypto import AES -from ..errors import SecurityError, InvalidBufferError -from ..extensions import BinaryReader -from ..tl.core import TLMessage -from ..tl.functions import InvokeAfterMsgRequest -from ..tl.core.gzippacked import GzipPacked - - -class MTProtoState: - """ - `telethon.network.mtprotosender.MTProtoSender` needs to hold a state - in order to be able to encrypt and decrypt incoming/outgoing messages, - as well as generating the message IDs. Instances of this class hold - together all the required information. - - It doesn't make sense to use `telethon.sessions.abstract.Session` for - the sender because the sender should *not* be concerned about storing - this information to disk, as one may create as many senders as they - desire to any other data center, or some CDN. Using the same session - for all these is not a good idea as each need their own authkey, and - the concept of "copying" sessions with the unnecessary entities or - updates state for these connections doesn't make sense. - - While it would be possible to have a `MTProtoPlainState` that does no - encryption so that it was usable through the `MTProtoLayer` and thus - avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more - focused to efficiency and this state is also more advanced (since it - supports gzipping and invoking after other message IDs). There are too - many methods that would be needed to make it convenient to use for the - authentication process, at which point the `MTProtoPlainSender` is better. - """ - def __init__(self, auth_key, loggers): - self.auth_key = auth_key - self._log = loggers[__name__] - self.time_offset = 0 - self.salt = 0 - - self.id = self._sequence = self._last_msg_id = None - self.reset() - - def reset(self): - """ - Resets the state. - """ - # Session IDs can be random on every connection - self.id = struct.unpack('q', os.urandom(8))[0] - self._sequence = 0 - self._last_msg_id = 0 - - def update_message_id(self, message): - """ - Updates the message ID to a new one, - used when the time offset changed. - """ - message.msg_id = self._get_new_msg_id() - - @staticmethod - def _calc_key(auth_key, msg_key, client): - """ - Calculate the key based on Telegram guidelines for MTProto 2, - specifying whether it's the client or not. See - https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector - """ - x = 0 if client else 8 - sha256a = sha256(msg_key + auth_key[x: x + 36]).digest() - sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest() - - aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32] - aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32] - - return aes_key, aes_iv - - def write_data_as_message(self, buffer, data, content_related, - *, after_id=None): - """ - Writes a message containing the given data into buffer. - - Returns the message id. - """ - msg_id = self._get_new_msg_id() - seq_no = self._get_seq_no(content_related) - if after_id is None: - body = GzipPacked.gzip_if_smaller(content_related, data) - else: - body = GzipPacked.gzip_if_smaller(content_related, - bytes(InvokeAfterMsgRequest(after_id, data))) - - buffer.write(struct.pack('= new_msg_id: - new_msg_id = self._last_msg_id + 4 - - self._last_msg_id = new_msg_id - return new_msg_id - - def update_time_offset(self, correct_msg_id): - """ - Updates the time offset to the correct - one given a known valid message ID. - """ - bad = self._get_new_msg_id() - old = self.time_offset - - now = int(time.time()) - correct = correct_msg_id >> 32 - self.time_offset = correct - now - - if self.time_offset != old: - self._last_msg_id = 0 - self._log.debug( - 'Updated time offset (old offset %d, bad %d, good %d, new %d)', - old, bad, correct_msg_id, self.time_offset - ) - - return self.time_offset - - def _get_seq_no(self, content_related): - """ - Generates the next sequence number depending on whether - it should be for a content-related query or not. - """ - if content_related: - result = self._sequence * 2 + 1 - self._sequence += 1 - return result - else: - return self._sequence * 2 diff --git a/telethon/network/requeststate.py b/telethon/network/requeststate.py index eb598e24..e69de29b 100644 --- a/telethon/network/requeststate.py +++ b/telethon/network/requeststate.py @@ -1,19 +0,0 @@ -import asyncio - - -class RequestState: - """ - This request state holds several information relevant to sent messages, - in particular the message ID assigned to the request, the container ID - it belongs to, the request itself, the request as bytes, and the future - result that will eventually be resolved. - """ - __slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after') - - def __init__(self, request, loop, after=None): - self.container_id = None - self.msg_id = None - self.request = request - self.data = bytes(request) - self.future = asyncio.Future(loop=loop) - self.after = after diff --git a/telethon/password.py b/telethon/password.py index 1c4666b5..e69de29b 100644 --- a/telethon/password.py +++ b/telethon/password.py @@ -1,198 +0,0 @@ -import hashlib -import os - -from .crypto import factorization -from .tl import types - - -def check_prime_and_good_check(prime: int, g: int): - good_prime_bits_count = 2048 - if prime < 0 or prime.bit_length() != good_prime_bits_count: - raise ValueError('bad prime count {}, expected {}' - .format(prime.bit_length(), good_prime_bits_count)) - - # TODO This is awfully slow - if factorization.Factorization.factorize(prime)[0] != 1: - raise ValueError('given "prime" is not prime') - - if g == 2: - if prime % 8 != 7: - raise ValueError('bad g {}, mod8 {}'.format(g, prime % 8)) - elif g == 3: - if prime % 3 != 2: - raise ValueError('bad g {}, mod3 {}'.format(g, prime % 3)) - elif g == 4: - pass - elif g == 5: - if prime % 5 not in (1, 4): - raise ValueError('bad g {}, mod5 {}'.format(g, prime % 5)) - elif g == 6: - if prime % 24 not in (19, 23): - raise ValueError('bad g {}, mod24 {}'.format(g, prime % 24)) - elif g == 7: - if prime % 7 not in (3, 5, 6): - raise ValueError('bad g {}, mod7 {}'.format(g, prime % 7)) - else: - raise ValueError('bad g {}'.format(g)) - - prime_sub1_div2 = (prime - 1) // 2 - if factorization.Factorization.factorize(prime_sub1_div2)[0] != 1: - raise ValueError('(prime - 1) // 2 is not prime') - - # Else it's good - - -def check_prime_and_good(prime_bytes: bytes, g: int): - good_prime = bytes(( - 0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73, - 0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F, - 0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB, - 0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13, 0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37, - 0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A, 0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB, - 0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84, 0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64, - 0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D, 0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88, - 0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F, 0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4, - 0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E, 0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41, - 0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14, 0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54, - 0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4, 0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D, - 0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF, 0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4, - 0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0, 0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05, - 0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59, 0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F, - 0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE, 0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF, - 0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03, 0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B)) - - if good_prime == prime_bytes: - if g in (3, 4, 5, 7): - return # It's good - - check_prime_and_good_check(int.from_bytes(prime_bytes, 'big'), g) - - -def is_good_large(number: int, p: int) -> bool: - return number > 0 and p - number > 0 - - -SIZE_FOR_HASH = 256 - - -def num_bytes_for_hash(number: bytes) -> bytes: - return bytes(SIZE_FOR_HASH - len(number)) + number - - -def big_num_for_hash(g: int) -> bytes: - return g.to_bytes(SIZE_FOR_HASH, 'big') - - -def sha256(*p: bytes) -> bytes: - hash = hashlib.sha256() - for q in p: - hash.update(q) - return hash.digest() - - -def is_good_mod_exp_first(modexp, prime) -> bool: - diff = prime - modexp - min_diff_bits_count = 2048 - 64 - max_mod_exp_size = 256 - if diff < 0 or \ - diff.bit_length() < min_diff_bits_count or \ - modexp.bit_length() < min_diff_bits_count or \ - (modexp.bit_length() + 7) // 8 > max_mod_exp_size: - return False - return True - - -def xor(a: bytes, b: bytes) -> bytes: - return bytes(x ^ y for x, y in zip(a, b)) - - -def pbkdf2sha512(password: bytes, salt: bytes, iterations: int): - return hashlib.pbkdf2_hmac('sha512', password, salt, iterations) - - -def compute_hash(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, - password: str): - hash1 = sha256(algo.salt1, password.encode('utf-8'), algo.salt1) - hash2 = sha256(algo.salt2, hash1, algo.salt2) - hash3 = pbkdf2sha512(hash2, algo.salt1, 100000) - return sha256(algo.salt2, hash3, algo.salt2) - - -def compute_digest(algo: types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, - password: str): - try: - check_prime_and_good(algo.p, algo.g) - except ValueError: - raise ValueError('bad p/g in password') - - value = pow(algo.g, - int.from_bytes(compute_hash(algo, password), 'big'), - int.from_bytes(algo.p, 'big')) - - return big_num_for_hash(value) - - -# https://github.com/telegramdesktop/tdesktop/blob/18b74b90451a7db2379a9d753c9cbaf8734b4d5d/Telegram/SourceFiles/core/core_cloud_password.cpp -def compute_check(request: types.account.Password, password: str): - algo = request.current_algo - if not isinstance(algo, types.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow): - raise ValueError('unsupported password algorithm {}' - .format(algo.__class__.__name__)) - - pw_hash = compute_hash(algo, password) - - p = int.from_bytes(algo.p, 'big') - g = algo.g - B = int.from_bytes(request.srp_B, 'big') - try: - check_prime_and_good(algo.p, g) - except ValueError: - raise ValueError('bad p/g in password') - - if not is_good_large(B, p): - raise ValueError('bad b in check') - - x = int.from_bytes(pw_hash, 'big') - p_for_hash = num_bytes_for_hash(algo.p) - g_for_hash = big_num_for_hash(g) - b_for_hash = num_bytes_for_hash(request.srp_B) - g_x = pow(g, x, p) - k = int.from_bytes(sha256(p_for_hash, g_for_hash), 'big') - kg_x = (k * g_x) % p - - def generate_and_check_random(): - random_size = 256 - import time - while True: - random = os.urandom(random_size) - a = int.from_bytes(random, 'big') - A = pow(g, a, p) - if is_good_mod_exp_first(A, p): - a_for_hash = big_num_for_hash(A) - u = int.from_bytes(sha256(a_for_hash, b_for_hash), 'big') - if u > 0: - return (a, a_for_hash, u) - - print(A, 'bad for', p) - time.sleep(1) - - a, a_for_hash, u = generate_and_check_random() - g_b = (B - kg_x) % p - if not is_good_mod_exp_first(g_b, p): - raise ValueError('bad g_b') - - ux = u * x - a_ux = a + ux - S = pow(g_b, a_ux, p) - K = sha256(big_num_for_hash(S)) - M1 = sha256( - xor(sha256(p_for_hash), sha256(g_for_hash)), - sha256(algo.salt1), - sha256(algo.salt2), - a_for_hash, - b_for_hash, - K - ) - - return types.InputCheckPasswordSRP( - request.srp_id, bytes(a_for_hash), bytes(M1)) diff --git a/telethon/requestiter.py b/telethon/requestiter.py index dee55f5f..e69de29b 100644 --- a/telethon/requestiter.py +++ b/telethon/requestiter.py @@ -1,135 +0,0 @@ -import abc -import asyncio -import time - -from . import helpers - - -class RequestIter(abc.ABC): - """ - Helper class to deal with requests that need offsets to iterate. - - It has some facilities, such as automatically sleeping a desired - amount of time between requests if needed (but not more). - - Can be used synchronously if the event loop is not running and - as an asynchronous iterator otherwise. - - `limit` is the total amount of items that the iterator should return. - This is handled on this base class, and will be always ``>= 0``. - - `left` will be reset every time the iterator is used and will indicate - the amount of items that should be emitted left, so that subclasses can - be more efficient and fetch only as many items as they need. - - Iterators may be used with ``reversed``, and their `reverse` flag will - be set to ``True`` if that's the case. Note that if this flag is set, - `buffer` should be filled in reverse too. - """ - def __init__(self, client, limit, *, reverse=False, wait_time=None, **kwargs): - self.client = client - self.reverse = reverse - self.wait_time = wait_time - self.kwargs = kwargs - self.limit = max(float('inf') if limit is None else limit, 0) - self.left = self.limit - self.buffer = None - self.index = 0 - self.total = None - self.last_load = 0 - - async def _init(self, **kwargs): - """ - Called when asynchronous initialization is necessary. All keyword - arguments passed to `__init__` will be forwarded here, and it's - preferable to use named arguments in the subclasses without defaults - to avoid forgetting or misspelling any of them. - - This method may ``raise StopAsyncIteration`` if it cannot continue. - - This method may actually fill the initial buffer if it needs to, - and similarly to `_load_next_chunk`, ``return True`` to indicate - that this is the last iteration (just the initial load). - """ - - async def __anext__(self): - if self.buffer is None: - self.buffer = [] - if await self._init(**self.kwargs): - self.left = len(self.buffer) - - if self.left <= 0: # <= 0 because subclasses may change it - raise StopAsyncIteration - - if self.index == len(self.buffer): - # asyncio will handle times <= 0 to sleep 0 seconds - if self.wait_time: - await asyncio.sleep( - self.wait_time - (time.time() - self.last_load), - loop=self.client.loop - ) - self.last_load = time.time() - - self.index = 0 - self.buffer = [] - if await self._load_next_chunk(): - self.left = len(self.buffer) - - if not self.buffer: - raise StopAsyncIteration - - result = self.buffer[self.index] - self.left -= 1 - self.index += 1 - return result - - def __next__(self): - try: - return self.client.loop.run_until_complete(self.__anext__()) - except StopAsyncIteration: - raise StopIteration - - def __aiter__(self): - self.buffer = None - self.index = 0 - self.last_load = 0 - self.left = self.limit - return self - - def __iter__(self): - if self.client.loop.is_running(): - raise RuntimeError( - 'You must use "async for" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return self.__aiter__() - - async def collect(self): - """ - Create a `self` iterator and collect it into a `TotalList` - (a normal list with a `.total` attribute). - """ - result = helpers.TotalList() - async for message in self: - result.append(message) - - result.total = self.total - return result - - @abc.abstractmethod - async def _load_next_chunk(self): - """ - Called when the next chunk is necessary. - - It should extend the `buffer` with new items. - - It should return ``True`` if it's the last chunk, - after which moment the method won't be called again - during the same iteration. - """ - raise NotImplementedError - - def __reversed__(self): - self.reverse = not self.reverse - return self # __aiter__ will be called after, too diff --git a/telethon/sessions/__init__.py b/telethon/sessions/__init__.py index d10c76e6..e69de29b 100644 --- a/telethon/sessions/__init__.py +++ b/telethon/sessions/__init__.py @@ -1,4 +0,0 @@ -from .abstract import Session -from .memory import MemorySession -from .sqlite import SQLiteSession -from .string import StringSession diff --git a/telethon/sessions/abstract.py b/telethon/sessions/abstract.py index 7265b6a3..e69de29b 100644 --- a/telethon/sessions/abstract.py +++ b/telethon/sessions/abstract.py @@ -1,167 +0,0 @@ -from abc import ABC, abstractmethod - - -class Session(ABC): - def __init__(self): - pass - - def clone(self, to_instance=None): - """ - Creates a clone of this session file. - """ - return to_instance or self.__class__() - - @abstractmethod - def set_dc(self, dc_id, server_address, port): - """ - Sets the information of the data center address and port that - the library should connect to, as well as the data center ID, - which is currently unused. - """ - raise NotImplementedError - - @property - @abstractmethod - def dc_id(self): - """ - Returns the currently-used data center ID. - """ - raise NotImplementedError - - @property - @abstractmethod - def server_address(self): - """ - Returns the server address where the library should connect to. - """ - raise NotImplementedError - - @property - @abstractmethod - def port(self): - """ - Returns the port to which the library should connect to. - """ - raise NotImplementedError - - @property - @abstractmethod - def auth_key(self): - """ - Returns an ``AuthKey`` instance associated with the saved - data center, or ``None`` if a new one should be generated. - """ - raise NotImplementedError - - @auth_key.setter - @abstractmethod - def auth_key(self, value): - """ - Sets the ``AuthKey`` to be used for the saved data center. - """ - raise NotImplementedError - - @property - @abstractmethod - def takeout_id(self): - """ - Returns an ID of the takeout process initialized for this session, - or ``None`` if there's no were any unfinished takeout requests. - """ - raise NotImplementedError - - @takeout_id.setter - @abstractmethod - def takeout_id(self, value): - """ - Sets the ID of the unfinished takeout process for this session. - """ - raise NotImplementedError - - @abstractmethod - def get_update_state(self, entity_id): - """ - Returns the ``UpdateState`` associated with the given `entity_id`. - If the `entity_id` is 0, it should return the ``UpdateState`` for - no specific channel (the "general" state). If no state is known - it should ``return None``. - """ - raise NotImplementedError - - @abstractmethod - def set_update_state(self, entity_id, state): - """ - Sets the given ``UpdateState`` for the specified `entity_id`, which - should be 0 if the ``UpdateState`` is the "general" state (and not - for any specific channel). - """ - raise NotImplementedError - - @abstractmethod - def close(self): - """ - Called on client disconnection. Should be used to - free any used resources. Can be left empty if none. - """ - - @abstractmethod - def save(self): - """ - Called whenever important properties change. It should - make persist the relevant session information to disk. - """ - raise NotImplementedError - - @abstractmethod - def delete(self): - """ - Called upon client.log_out(). Should delete the stored - information from disk since it's not valid anymore. - """ - raise NotImplementedError - - @classmethod - def list_sessions(cls): - """ - Lists available sessions. Not used by the library itself. - """ - return [] - - @abstractmethod - def process_entities(self, tlo): - """ - Processes the input ``TLObject`` or ``list`` and saves - whatever information is relevant (e.g., ID or access hash). - """ - raise NotImplementedError - - @abstractmethod - def get_input_entity(self, key): - """ - Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``). - The library uses this method whenever an ``InputPeer`` is needed - to suit several purposes (e.g. user only provided its ID or wishes - to use a cached username to avoid extra RPC). - """ - raise NotImplementedError - - @abstractmethod - def cache_file(self, md5_digest, file_size, instance): - """ - Caches the given file information persistently, so that it - doesn't need to be re-uploaded in case the file is used again. - - The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, - both with an ``.id`` and ``.access_hash`` attributes. - """ - raise NotImplementedError - - @abstractmethod - def get_file(self, md5_digest, file_size, cls): - """ - Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` - match an existing saved record. The class will either be an - ``InputPhoto`` or ``InputDocument``, both with two parameters - ``id`` and ``access_hash`` in that order. - """ - raise NotImplementedError diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py index 02f07cff..e69de29b 100644 --- a/telethon/sessions/memory.py +++ b/telethon/sessions/memory.py @@ -1,244 +0,0 @@ -from enum import Enum - -from .abstract import Session -from .. import utils -from ..tl import TLObject -from ..tl.types import ( - PeerUser, PeerChat, PeerChannel, - InputPeerUser, InputPeerChat, InputPeerChannel, - InputPhoto, InputDocument -) - - -class _SentFileType(Enum): - DOCUMENT = 0 - PHOTO = 1 - - @staticmethod - def from_type(cls): - if cls == InputDocument: - return _SentFileType.DOCUMENT - elif cls == InputPhoto: - return _SentFileType.PHOTO - else: - raise ValueError('The cls must be either InputDocument/InputPhoto') - - -class MemorySession(Session): - def __init__(self): - super().__init__() - - self._dc_id = 0 - self._server_address = None - self._port = None - self._auth_key = None - self._takeout_id = None - - self._files = {} - self._entities = set() - self._update_states = {} - - def set_dc(self, dc_id, server_address, port): - self._dc_id = dc_id or 0 - self._server_address = server_address - self._port = port - - @property - def dc_id(self): - return self._dc_id - - @property - def server_address(self): - return self._server_address - - @property - def port(self): - return self._port - - @property - def auth_key(self): - return self._auth_key - - @auth_key.setter - def auth_key(self, value): - self._auth_key = value - - @property - def takeout_id(self): - return self._takeout_id - - @takeout_id.setter - def takeout_id(self, value): - self._takeout_id = value - - def get_update_state(self, entity_id): - return self._update_states.get(entity_id, None) - - def set_update_state(self, entity_id, state): - self._update_states[entity_id] = state - - def close(self): - pass - - def save(self): - pass - - def delete(self): - pass - - @staticmethod - def _entity_values_to_row(id, hash, username, phone, name): - # While this is a simple implementation it might be overrode by, - # other classes so they don't need to implement the plural form - # of the method. Don't remove. - return id, hash, username, phone, name - - def _entity_to_row(self, e): - if not isinstance(e, TLObject): - return - try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p) - except TypeError: - # Note: `get_input_peer` already checks for - # non-zero `access_hash`. See issues #354 and #392. - return - - if isinstance(p, (InputPeerUser, InputPeerChannel)): - p_hash = p.access_hash - elif isinstance(p, InputPeerChat): - p_hash = 0 - else: - return - - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - return self._entity_values_to_row( - marked_id, p_hash, username, phone, name - ) - - def _entities_to_rows(self, tlo): - if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): - # This may be a list of users already for instance - entities = tlo - else: - entities = [] - if hasattr(tlo, 'user'): - entities.append(tlo.user) - if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): - entities.extend(tlo.chats) - if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): - entities.extend(tlo.users) - - rows = [] # Rows to add (id, hash, username, phone, name) - for e in entities: - row = self._entity_to_row(e) - if row: - rows.append(row) - return rows - - def process_entities(self, tlo): - self._entities |= set(self._entities_to_rows(tlo)) - - def get_entity_rows_by_phone(self, phone): - try: - return next((id, hash) for id, hash, _, found_phone, _ - in self._entities if found_phone == phone) - except StopIteration: - pass - - def get_entity_rows_by_username(self, username): - try: - return next((id, hash) for id, hash, found_username, _, _ - in self._entities if found_username == username) - except StopIteration: - pass - - def get_entity_rows_by_name(self, name): - try: - return next((id, hash) for id, hash, _, _, found_name - in self._entities if found_name == name) - except StopIteration: - pass - - def get_entity_rows_by_id(self, id, exact=True): - try: - if exact: - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id == id) - else: - ids = ( - utils.get_peer_id(PeerUser(id)), - utils.get_peer_id(PeerChat(id)), - utils.get_peer_id(PeerChannel(id)) - ) - return next((id, hash) for found_id, hash, _, _, _ - in self._entities if found_id in ids) - except StopIteration: - pass - - def get_input_entity(self, key): - try: - if key.SUBCLASS_OF_ID in (0xc91c90b6, 0xe669bf46, 0x40f202fd): - # hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel')) - # We already have an Input version, so nothing else required - return key - # Try to early return if this key can be casted as input peer - return utils.get_input_peer(key) - except (AttributeError, TypeError): - # Not a TLObject or can't be cast into InputPeer - if isinstance(key, TLObject): - key = utils.get_peer_id(key) - exact = True - else: - exact = not isinstance(key, int) or key < 0 - - result = None - if isinstance(key, str): - phone = utils.parse_phone(key) - if phone: - result = self.get_entity_rows_by_phone(phone) - else: - username, invite = utils.parse_username(key) - if username and not invite: - result = self.get_entity_rows_by_username(username) - else: - tup = utils.resolve_invite_link(key)[1] - if tup: - result = self.get_entity_rows_by_id(tup, exact=False) - - elif isinstance(key, int): - result = self.get_entity_rows_by_id(key, exact) - - if not result and isinstance(key, str): - result = self.get_entity_rows_by_name(key) - - if result: - entity_id, entity_hash = result # unpack resulting tuple - entity_id, kind = utils.resolve_id(entity_id) - # removes the mark and returns type of entity - if kind == PeerUser: - return InputPeerUser(entity_id, entity_hash) - elif kind == PeerChat: - return InputPeerChat(entity_id) - elif kind == PeerChannel: - return InputPeerChannel(entity_id, entity_hash) - else: - raise ValueError('Could not find input entity with key ', key) - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (InputDocument, InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - key = (md5_digest, file_size, _SentFileType.from_type(type(instance))) - value = (instance.id, instance.access_hash) - self._files[key] = value - - def get_file(self, md5_digest, file_size, cls): - key = (md5_digest, file_size, _SentFileType.from_type(cls)) - try: - return cls(*self._files[key]) - except KeyError: - return None diff --git a/telethon/sessions/sqlite.py b/telethon/sessions/sqlite.py index 4295be11..e69de29b 100644 --- a/telethon/sessions/sqlite.py +++ b/telethon/sessions/sqlite.py @@ -1,325 +0,0 @@ -import datetime -import os - -from telethon.tl import types -from .memory import MemorySession, _SentFileType -from .. import utils -from ..crypto import AuthKey -from ..tl.types import ( - InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel -) - -try: - import sqlite3 - sqlite3_err = None -except ImportError as e: - sqlite3 = None - sqlite3_err = type(e) - -EXTENSION = '.session' -CURRENT_VERSION = 5 # database version - - -class SQLiteSession(MemorySession): - """This session contains the required information to login into your - Telegram account. NEVER give the saved session file to anyone, since - they would gain instant access to all your messages and contacts. - - If you think the session has been compromised, close all the sessions - through an official Telegram client to revoke the authorization. - """ - - def __init__(self, session_id=None): - if sqlite3 is None: - raise sqlite3_err - - super().__init__() - self.filename = ':memory:' - self.save_entities = True - - if session_id: - self.filename = session_id - if not self.filename.endswith(EXTENSION): - self.filename += EXTENSION - - self._conn = None - c = self._cursor() - c.execute("select name from sqlite_master " - "where type='table' and name='version'") - if c.fetchone(): - # Tables already exist, check for the version - c.execute("select version from version") - version = c.fetchone()[0] - if version < CURRENT_VERSION: - self._upgrade_database(old=version) - c.execute("delete from version") - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self.save() - - # These values will be saved - c.execute('select * from sessions') - tuple_ = c.fetchone() - if tuple_: - self._dc_id, self._server_address, self._port, key, \ - self._takeout_id = tuple_ - self._auth_key = AuthKey(data=key) - - c.close() - else: - # Tables don't exist, create new ones - self._create_table( - c, - "version (version integer primary key)" - , - """sessions ( - dc_id integer primary key, - server_address text, - port integer, - auth_key blob, - takeout_id integer - )""" - , - """entities ( - id integer primary key, - hash integer not null, - username text, - phone integer, - name text - )""" - , - """sent_files ( - md5_digest blob, - file_size integer, - type integer, - id integer, - hash integer, - primary key(md5_digest, file_size, type) - )""" - , - """update_state ( - id integer primary key, - pts integer, - qts integer, - date integer, - seq integer - )""" - ) - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self._update_session_table() - c.close() - self.save() - - def clone(self, to_instance=None): - cloned = super().clone(to_instance) - cloned.save_entities = self.save_entities - return cloned - - def _upgrade_database(self, old): - c = self._cursor() - if old == 1: - old += 1 - # old == 1 doesn't have the old sent_files so no need to drop - if old == 2: - old += 1 - # Old cache from old sent_files lasts then a day anyway, drop - c.execute('drop table sent_files') - self._create_table(c, """sent_files ( - md5_digest blob, - file_size integer, - type integer, - id integer, - hash integer, - primary key(md5_digest, file_size, type) - )""") - if old == 3: - old += 1 - self._create_table(c, """update_state ( - id integer primary key, - pts integer, - qts integer, - date integer, - seq integer - )""") - if old == 4: - old += 1 - c.execute("alter table sessions add column takeout_id integer") - c.close() - - @staticmethod - def _create_table(c, *definitions): - for definition in definitions: - c.execute('create table {}'.format(definition)) - - # Data from sessions should be kept as properties - # not to fetch the database every time we need it - def set_dc(self, dc_id, server_address, port): - super().set_dc(dc_id, server_address, port) - self._update_session_table() - - # Fetch the auth_key corresponding to this data center - row = self._execute('select auth_key from sessions') - if row and row[0]: - self._auth_key = AuthKey(data=row[0]) - else: - self._auth_key = None - - @MemorySession.auth_key.setter - def auth_key(self, value): - self._auth_key = value - self._update_session_table() - - @MemorySession.takeout_id.setter - def takeout_id(self, value): - self._takeout_id = value - self._update_session_table() - - def _update_session_table(self): - c = self._cursor() - # While we can save multiple rows into the sessions table - # currently we only want to keep ONE as the tables don't - # tell us which auth_key's are usable and will work. Needs - # some more work before being able to save auth_key's for - # multiple DCs. Probably done differently. - c.execute('delete from sessions') - c.execute('insert or replace into sessions values (?,?,?,?,?)', ( - self._dc_id, - self._server_address, - self._port, - self._auth_key.key if self._auth_key else b'', - self._takeout_id - )) - c.close() - - def get_update_state(self, entity_id): - row = self._execute('select pts, qts, date, seq from update_state ' - 'where id = ?', entity_id) - if row: - pts, qts, date, seq = row - date = datetime.datetime.fromtimestamp( - date, tz=datetime.timezone.utc) - return types.updates.State(pts, qts, date, seq, unread_count=0) - - def set_update_state(self, entity_id, state): - self._execute('insert or replace into update_state values (?,?,?,?,?)', - entity_id, state.pts, state.qts, - state.date.timestamp(), state.seq) - - def save(self): - """Saves the current session object as session_user_id.session""" - # This is a no-op if there are no changes to commit, so there's - # no need for us to keep track of an "unsaved changes" variable. - if self._conn is not None: - self._conn.commit() - - def _cursor(self): - """Asserts that the connection is open and returns a cursor""" - if self._conn is None: - self._conn = sqlite3.connect(self.filename, - check_same_thread=False) - return self._conn.cursor() - - def _execute(self, stmt, *values): - """ - Gets a cursor, executes `stmt` and closes the cursor, - fetching one row afterwards and returning its result. - """ - c = self._cursor() - try: - return c.execute(stmt, values).fetchone() - finally: - c.close() - - def close(self): - """Closes the connection unless we're working in-memory""" - if self.filename != ':memory:': - if self._conn is not None: - self._conn.commit() - self._conn.close() - self._conn = None - - def delete(self): - """Deletes the current session file""" - if self.filename == ':memory:': - return True - try: - os.remove(self.filename) - return True - except OSError: - return False - - @classmethod - def list_sessions(cls): - """Lists all the sessions of the users who have ever connected - using this client and never logged out - """ - return [os.path.splitext(os.path.basename(f))[0] - for f in os.listdir('.') if f.endswith(EXTENSION)] - - # Entity processing - - def process_entities(self, tlo): - """Processes all the found entities on the given TLObject, - unless .enabled is False. - - Returns True if new input entities were added. - """ - if not self.save_entities: - return - - rows = self._entities_to_rows(tlo) - if not rows: - return - - c = self._cursor() - try: - c.executemany( - 'insert or replace into entities values (?,?,?,?,?)', rows) - finally: - c.close() - - def get_entity_rows_by_phone(self, phone): - return self._execute( - 'select id, hash from entities where phone = ?', phone) - - def get_entity_rows_by_username(self, username): - return self._execute( - 'select id, hash from entities where username = ?', username) - - def get_entity_rows_by_name(self, name): - return self._execute( - 'select id, hash from entities where name = ?', name) - - def get_entity_rows_by_id(self, id, exact=True): - if exact: - return self._execute( - 'select id, hash from entities where id = ?', id) - else: - return self._execute( - 'select id, hash from entities where id in (?,?,?)', - utils.get_peer_id(PeerUser(id)), - utils.get_peer_id(PeerChat(id)), - utils.get_peer_id(PeerChannel(id)) - ) - - # File processing - - def get_file(self, md5_digest, file_size, cls): - row = self._execute( - 'select id, hash from sent_files ' - 'where md5_digest = ? and file_size = ? and type = ?', - md5_digest, file_size, _SentFileType.from_type(cls).value - ) - if row: - # Both allowed classes have (id, access_hash) as parameters - return cls(row[0], row[1]) - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (InputDocument, InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - - self._execute( - 'insert or replace into sent_files values (?,?,?,?,?)', - md5_digest, file_size, - _SentFileType.from_type(type(instance)).value, - instance.id, instance.access_hash - ) diff --git a/telethon/sessions/string.py b/telethon/sessions/string.py index fb971d82..e69de29b 100644 --- a/telethon/sessions/string.py +++ b/telethon/sessions/string.py @@ -1,63 +0,0 @@ -import base64 -import ipaddress -import struct - -from .abstract import Session -from .memory import MemorySession -from ..crypto import AuthKey - -_STRUCT_PREFORMAT = '>B{}sH256s' - -CURRENT_VERSION = '1' - - -class StringSession(MemorySession): - """ - This session file can be easily saved and loaded as a string. According - to the initial design, it contains only the data that is necessary for - successful connection and authentication, so takeout ID is not stored. - - It is thought to be used where you don't want to create any on-disk - files but would still like to be able to save and load existing sessions - by other means. - - You can use custom `encode` and `decode` functions, if present: - - * `encode` definition must be ``def encode(value: bytes) -> str:``. - * `decode` definition must be ``def decode(value: str) -> bytes:``. - """ - def __init__(self, string: str = None): - super().__init__() - if string: - if string[0] != CURRENT_VERSION: - raise ValueError('Not a valid string') - - string = string[1:] - ip_len = 4 if len(string) == 352 else 16 - self._dc_id, ip, self._port, key = struct.unpack( - _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string)) - - self._server_address = ipaddress.ip_address(ip).compressed - if any(key): - self._auth_key = AuthKey(key) - - @staticmethod - def encode(x: bytes) -> str: - return base64.urlsafe_b64encode(x).decode('ascii') - - @staticmethod - def decode(x: str) -> bytes: - return base64.urlsafe_b64decode(x) - - def save(self: Session): - if not self.auth_key: - return '' - - ip = ipaddress.ip_address(self.server_address).packed - return CURRENT_VERSION + StringSession.encode(struct.pack( - _STRUCT_PREFORMAT.format(len(ip)), - self.dc_id, - ip, - self.port, - self.auth_key.key - )) diff --git a/telethon/statecache.py b/telethon/statecache.py index 6125d101..e69de29b 100644 --- a/telethon/statecache.py +++ b/telethon/statecache.py @@ -1,162 +0,0 @@ -import datetime -import inspect - -from .tl import types - - -# Which updates have the following fields? -_has_channel_id = [] - - -# TODO EntityCache does the same. Reuse? -def _fill(): - for name in dir(types): - update = getattr(types, name) - if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e: - cid = update.CONSTRUCTOR_ID - sig = inspect.signature(update.__init__) - for param in sig.parameters.values(): - if param.name == 'channel_id' and param.annotation == int: - _has_channel_id.append(cid) - - if not _has_channel_id: - raise RuntimeError('FIXME: Did the init signature or updates change?') - - -# We use a function to avoid cluttering the globals (with name/update/cid/doc) -_fill() - - -class StateCache: - """ - In-memory update state cache, defaultdict-like behaviour. - """ - def __init__(self, initial, loggers): - # We only care about the pts and the date. By using a tuple which - # is lightweight and immutable we can easily copy them around to - # each update in case they need to fetch missing entities. - self._logger = loggers[__name__] - if initial: - self._pts_date = initial.pts, initial.date - else: - self._pts_date = None, None - - def reset(self): - self.__dict__.clear() - self._pts_date = None, None - - # TODO Call this when receiving responses too...? - def update( - self, - update, - *, - channel_id=None, - has_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateNewMessage, - types.UpdateDeleteMessages, - types.UpdateReadHistoryInbox, - types.UpdateReadHistoryOutbox, - types.UpdateWebPage, - types.UpdateReadMessagesContents, - types.UpdateEditMessage, - types.updates.State, - types.updates.DifferenceTooLong, - types.UpdateShortMessage, - types.UpdateShortChatMessage, - types.UpdateShortSentMessage - )), - has_date=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateUserPhoto, - types.UpdateEncryption, - types.UpdateEncryptedMessagesRead, - types.UpdateChatParticipantAdd, - types.updates.DifferenceEmpty, - types.UpdateShortMessage, - types.UpdateShortChatMessage, - types.UpdateShort, - types.UpdatesCombined, - types.Updates, - types.UpdateShortSentMessage, - )), - has_channel_pts=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateChannelTooLong, - types.UpdateNewChannelMessage, - types.UpdateDeleteChannelMessages, - types.UpdateEditChannelMessage, - types.UpdateChannelWebPage, - types.updates.ChannelDifferenceEmpty, - types.updates.ChannelDifferenceTooLong, - types.updates.ChannelDifference - )), - check_only=False - ): - """ - Update the state with the given update. - """ - cid = update.CONSTRUCTOR_ID - if check_only: - return cid in has_pts or cid in has_date or cid in has_channel_pts - - if cid in has_pts: - if cid in has_date: - self._pts_date = update.pts, update.date - else: - self._pts_date = update.pts, self._pts_date[1] - elif cid in has_date: - self._pts_date = self._pts_date[0], update.date - - if cid in has_channel_pts: - if channel_id is None: - channel_id = self.get_channel_id(update) - - if channel_id is None: - self._logger.info( - 'Failed to retrieve channel_id from %s', update) - else: - self.__dict__[channel_id] = update.pts - - def get_channel_id( - self, - update, - has_channel_id=frozenset(_has_channel_id), - # Hardcoded because only some with message are for channels - has_message=frozenset(x.CONSTRUCTOR_ID for x in ( - types.UpdateNewChannelMessage, - types.UpdateEditChannelMessage - )) - ): - """ - Gets the **unmarked** channel ID from this update, if it has any. - - Fails for ``*difference`` updates, where ``channel_id`` - is supposedly already known from the outside. - """ - cid = update.CONSTRUCTOR_ID - if cid in has_channel_id: - return update.channel_id - elif cid in has_message: - if update.message.to_id is None: - self._logger.info('Update has None to_id %s', update) - else: - return update.message.to_id.channel_id - - return None - - def __getitem__(self, item): - """ - If `item` is ``None``, returns the default ``(pts, date)``. - - If it's an **unmarked** channel ID, returns its ``pts``. - - If no information is known, ``pts`` will be ``None``. - """ - if item is None: - return self._pts_date - else: - return self.__dict__.get(item) - - def __setitem__(self, where, value): - if where is None: - self._pts_date = value - else: - self.__dict__[where] = value diff --git a/telethon/sync.py b/telethon/sync.py index 79058c95..e69de29b 100644 --- a/telethon/sync.py +++ b/telethon/sync.py @@ -1,70 +0,0 @@ -""" -This magical module will rewrite all public methods in the public interface -of the library so they can run the loop on their own if it's not already -running. This rewrite may not be desirable if the end user always uses the -methods they way they should be ran, but it's incredibly useful for quick -scripts and the runtime overhead is relatively low. - -Some really common methods which are hardly used offer this ability by -default, such as ``.start()`` and ``.run_until_disconnected()`` (since -you may want to start, and then run until disconnected while using async -event handlers). -""" -import asyncio -import functools -import inspect - -from . import connection -from .client.account import _TakeoutClient -from .client.telegramclient import TelegramClient -from .tl import types, functions, custom -from .tl.custom import ( - Draft, Dialog, MessageButton, Forward, Button, - Message, InlineResult, Conversation -) -from .tl.custom.chatgetter import ChatGetter -from .tl.custom.sendergetter import SenderGetter - - -def _syncify_wrap(t, method_name): - method = getattr(t, method_name) - - @functools.wraps(method) - def syncified(*args, **kwargs): - coro = method(*args, **kwargs) - loop = asyncio.get_event_loop() - if loop.is_running(): - return coro - else: - return loop.run_until_complete(coro) - - # Save an accessible reference to the original method - setattr(syncified, '__tl.sync', method) - setattr(t, method_name, syncified) - - -def syncify(*types): - """ - Converts all the methods in the given types (class definitions) - into synchronous, which return either the coroutine or the result - based on whether ``asyncio's`` event loop is running. - """ - # Our asynchronous generators all are `RequestIter`, which already - # provide a synchronous iterator variant, so we don't need to worry - # about asyncgenfunction's here. - for t in types: - for name in dir(t): - if not name.startswith('_') or name == '__call__': - if inspect.iscoroutinefunction(getattr(t, name)): - _syncify_wrap(t, name) - - -syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, - ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) - - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/tl/__init__.py b/telethon/tl/__init__.py index e187537f..e69de29b 100644 --- a/telethon/tl/__init__.py +++ b/telethon/tl/__init__.py @@ -1 +0,0 @@ -from .tlobject import TLObject, TLRequest diff --git a/telethon/tl/core/__init__.py b/telethon/tl/core/__init__.py index 3113196a..e69de29b 100644 --- a/telethon/tl/core/__init__.py +++ b/telethon/tl/core/__init__.py @@ -1,26 +0,0 @@ -""" -This module holds core "special" types, which are more convenient ways -to do stuff in a `telethon.network.mtprotosender.MTProtoSender` instance. - -Only special cases are gzip-packed data, the response message (not a -client message), the message container which references these messages -and would otherwise conflict with the rest, and finally the RpcResult: - - rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; - -Three things to note with this definition: -1. The constructor ID is actually ``42d36c2c``. -2. Those bytes are not read like the rest of bytes (length + payload). - They are actually the raw bytes of another object, which can't be - read directly because it depends on per-request information (since - some can return ``Vector`` and ``Vector``). -3. Those bytes may be gzipped data, which needs to be treated early. -""" -from .tlmessage import TLMessage -from .gzippacked import GzipPacked -from .messagecontainer import MessageContainer -from .rpcresult import RpcResult - -core_objects = {x.CONSTRUCTOR_ID: x for x in ( - GzipPacked, MessageContainer, RpcResult -)} diff --git a/telethon/tl/core/gzippacked.py b/telethon/tl/core/gzippacked.py index 906c2c67..e69de29b 100644 --- a/telethon/tl/core/gzippacked.py +++ b/telethon/tl/core/gzippacked.py @@ -1,45 +0,0 @@ -import gzip -import struct - -from .. import TLObject, TLRequest - - -class GzipPacked(TLObject): - CONSTRUCTOR_ID = 0x3072cfa1 - - def __init__(self, data): - self.data = data - - @staticmethod - def gzip_if_smaller(content_related, data): - """Calls bytes(request), and based on a certain threshold, - optionally gzips the resulting data. If the gzipped data is - smaller than the original byte array, this is returned instead. - - Note that this only applies to content related requests. - """ - if content_related and len(data) > 512: - gzipped = bytes(GzipPacked(data)) - return gzipped if len(gzipped) < len(data) else data - else: - return data - - def __bytes__(self): - return struct.pack('`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionEditMessage) - - @property - def deleted_message(self): - """ - Whether a message in this channel was deleted or not. - - If ``True``, `old` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionDeleteMessage) - - @property - def changed_admin(self): - """ - Whether the permissions for an admin in this channel - changed or not. - - If ``True``, `old` and `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance( - self.original.action, - types.ChannelAdminLogEventActionParticipantToggleAdmin) - - @property - def changed_restrictions(self): - """ - Whether a message in this channel was edited or not. - - If ``True``, `old` and `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance( - self.original.action, - types.ChannelAdminLogEventActionParticipantToggleBan) - - @property - def changed_invites(self): - """ - Whether the invites in the channel were toggled or not. - - If ``True``, `old` and `new` will be present as ``bool``. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleInvites) - - @property - def changed_location(self): - """ - Whether the location setting of the channel has changed or not. - - If ``True``, `old` and `new` will be present as :tl:`ChannelLocation`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeLocation) - - @property - def joined(self): - """ - Whether `user` joined through the channel's - public username or not. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantJoin) - - @property - def joined_invite(self): - """ - Whether a new user joined through an invite - link to the channel or not. - - If ``True``, `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantInvite) - - @property - def left(self): - """ - Whether `user` left the channel or not. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantLeave) - - @property - def changed_hide_history(self): - """ - Whether hiding the previous message history for new members - in the channel was toggled or not. - - If ``True``, `old` and `new` will be present as ``bool``. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionTogglePreHistoryHidden) - - @property - def changed_signatures(self): - """ - Whether the message signatures in the channel were toggled - or not. - - If ``True``, `old` and `new` will be present as ``bool``. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleSignatures) - - @property - def changed_pin(self): - """ - Whether a new message in this channel was pinned or not. - - If ``True``, `new` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionUpdatePinned) - - @property - def changed_default_banned_rights(self): - """ - Whether the default banned rights were changed or not. - - If ``True``, `old` and `new` will - be present as :tl:`ChatBannedRights`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionDefaultBannedRights) - - @property - def stopped_poll(self): - """ - Whether a poll was stopped or not. - - If ``True``, `new` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionStopPoll) - - def __str__(self): - return str(self.original) - - def stringify(self): - return self.original.stringify() diff --git a/telethon/tl/custom/button.py b/telethon/tl/custom/button.py index 2bfb4018..e69de29b 100644 --- a/telethon/tl/custom/button.py +++ b/telethon/tl/custom/button.py @@ -1,159 +0,0 @@ -from .. import types - - -class Button: - """ - .. note:: - - This class is used to **define** reply markups, e.g. when - sending a message or replying to events. When you access - `Message.buttons ` - they are actually `MessageButton - `, - so you might want to refer to that class instead. - - Helper class to allow defining ``reply_markup`` when - sending a message with inline or keyboard buttons. - - You should make use of the defined class methods to create button - instances instead making them yourself (i.e. don't do ``Button(...)`` - but instead use methods line `Button.inline(...) ` etc. - - You can use `inline`, `switch_inline` and `url` - together to create inline buttons (under the message). - - You can use `text`, `request_location` and `request_phone` - together to create a reply markup (replaces the user keyboard). - You can also configure the aspect of the reply with these. - - You **cannot** mix the two type of buttons together, - and it will error if you try to do so. - - The text for all buttons may be at most 142 characters. - If more characters are given, Telegram will cut the text - to 128 characters and add the ellipsis (…) character as - the 129. - """ - def __init__(self, button, *, resize, single_use, selective): - self.button = button - self.resize = resize - self.single_use = single_use - self.selective = selective - - @staticmethod - def _is_inline(button): - """ - Returns ``True`` if the button belongs to an inline keyboard. - """ - return isinstance(button, ( - types.KeyboardButtonCallback, - types.KeyboardButtonSwitchInline, - types.KeyboardButtonUrl - )) - - @staticmethod - def inline(text, data=None): - """ - Creates a new inline button with some payload data in it. - - If `data` is omitted, the given `text` will be used as `data`. - In any case `data` should be either ``bytes`` or ``str``. - - Note that the given `data` must be less or equal to 64 bytes. - If more than 64 bytes are passed as data, ``ValueError`` is raised. - """ - if not data: - data = text.encode('utf-8') - elif not isinstance(data, (bytes, bytearray, memoryview)): - data = str(data).encode('utf-8') - - if len(data) > 64: - raise ValueError('Too many bytes for the data') - - return types.KeyboardButtonCallback(text, data) - - @staticmethod - def switch_inline(text, query='', same_peer=False): - """ - Creates a new inline button to switch to inline query. - - If `query` is given, it will be the default text to be used - when making the inline query. - - If ``same_peer is True`` the inline query will directly be - set under the currently opened chat. Otherwise, the user will - have to select a different dialog to make the query. - """ - return types.KeyboardButtonSwitchInline(text, query, same_peer) - - @staticmethod - def url(text, url=None): - """ - Creates a new inline button to open the desired URL on click. - - If no `url` is given, the `text` will be used as said URL instead. - - You cannot detect that the user clicked this button directly. - """ - return types.KeyboardButtonUrl(text, url or text) - - @classmethod - def text(cls, text, *, resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button with the given text. - - Args: - resize (`bool`): - If present, the entire keyboard will be reconfigured to - be resized and be smaller if there are not many buttons. - - single_use (`bool`): - If present, the entire keyboard will be reconfigured to - be usable only once before it hides itself. - - selective (`bool`): - If present, the entire keyboard will be reconfigured to - be "selective". The keyboard will be shown only to specific - users. It will target users that are @mentioned in the text - of the message or to the sender of the message you reply to. - """ - return cls(types.KeyboardButton(text), - resize=resize, single_use=single_use, selective=selective) - - @classmethod - def request_location(cls, text, *, - resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button to request the user's location on click. - - ``resize``, ``single_use`` and ``selective`` are documented in `text`. - """ - return cls(types.KeyboardButtonRequestGeoLocation(text), - resize=resize, single_use=single_use, selective=selective) - - @classmethod - def request_phone(cls, text, *, - resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button to request the user's phone on click. - - ``resize``, ``single_use`` and ``selective`` are documented in `text`. - """ - return cls(types.KeyboardButtonRequestPhone(text), - resize=resize, single_use=single_use, selective=selective) - - @staticmethod - def clear(): - """ - Clears all keyboard buttons after sending a message with this markup. - When used, no other button should be present or it will be ignored. - """ - return types.ReplyKeyboardHide() - - @staticmethod - def force_reply(): - """ - Forces a reply to the message with this markup. If used, - no other button should be present or it will be ignored. - """ - return types.ReplyKeyboardForceReply() diff --git a/telethon/tl/custom/chatgetter.py b/telethon/tl/custom/chatgetter.py index e919e17b..e69de29b 100644 --- a/telethon/tl/custom/chatgetter.py +++ b/telethon/tl/custom/chatgetter.py @@ -1,149 +0,0 @@ -import abc - -from ... import errors, utils -from ...tl import types - - -class ChatGetter(abc.ABC): - """ - Helper base class that introduces the `chat`, `input_chat` - and `chat_id` properties and `get_chat` and `get_input_chat` - methods. - """ - def __init__(self, chat_peer=None, *, input_chat=None, chat=None, broadcast=None): - self._chat_peer = chat_peer - self._input_chat = input_chat - self._chat = chat - self._broadcast = broadcast - self._client = None - - @property - def chat(self): - """ - Returns the :tl:`User`, :tl:`Chat` or :tl:`Channel` where this object - belongs to. It may be ``None`` if Telegram didn't send the chat. - - If you only need the ID, use `chat_id` instead. - - If you need to call a method which needs - this chat, use `input_chat` instead. - - If you're using `telethon.events`, use `get_chat()` instead. - """ - return self._chat - - async def get_chat(self): - """ - Returns `chat`, but will make an API call to find the - chat unless it's already cached. - - If you only need the ID, use `chat_id` instead. - - If you need to call a method which needs - this chat, use `get_input_chat()` instead. - """ - # See `get_sender` for information about 'min'. - if (self._chat is None or getattr(self._chat, 'min', None))\ - and await self.get_input_chat(): - try: - self._chat =\ - await self._client.get_entity(self._input_chat) - except ValueError: - await self._refetch_chat() - return self._chat - - @property - def input_chat(self): - """ - This :tl:`InputPeer` is the input version of the chat where the - message was sent. Similarly to `input_sender - `, this - doesn't have things like username or similar, but still useful in - some cases. - - Note that this might not be available if the library doesn't - have enough information available. - """ - if self._input_chat is None and self._chat_peer and self._client: - try: - self._input_chat = self._client._entity_cache[self._chat_peer] - except KeyError: - pass - - return self._input_chat - - async def get_input_chat(self): - """ - Returns `input_chat`, but will make an API call to find the - input chat unless it's already cached. - """ - if self.input_chat is None and self.chat_id and self._client: - try: - # The chat may be recent, look in dialogs - target = self.chat_id - async for d in self._client.iter_dialogs(100): - if d.id == target: - self._chat = d.entity - self._input_chat = d.input_entity - break - except errors.RPCError: - pass - - return self._input_chat - - @property - def chat_id(self): - """ - Returns the marked chat integer ID. Note that this value **will - be different** from ``to_id`` for incoming private messages, since - the chat *to* which the messages go is to your own person, but - the *chat* itself is with the one who sent the message. - - TL;DR; this gets the ID that you expect. - - If there is a chat in the object, `chat_id` will *always* be set, - which is why you should use it instead of `chat.id `. - """ - return utils.get_peer_id(self._chat_peer) if self._chat_peer else None - - @property - def is_private(self): - """ - ``True`` if the message was sent as a private message. - - Returns ``None`` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None - - @property - def is_group(self): - """ - True if the message was sent on a group or megagroup. - - Returns ``None`` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - # TODO Cache could tell us more in the future - if self._broadcast is None and hasattr(self.chat, 'broadcast'): - self._broadcast = bool(self.chat.broadcast) - - if isinstance(self._chat_peer, types.PeerChannel): - if self._broadcast is None: - return None - else: - return not self._broadcast - - return isinstance(self._chat_peer, types.PeerChat) - - @property - def is_channel(self): - """``True`` if the message was sent on a megagroup or channel.""" - # The only case where chat peer could be none is in MessageDeleted, - # however those always have the peer in channels. - return isinstance(self._chat_peer, types.PeerChannel) - - async def _refetch_chat(self): - """ - Re-fetches chat information through other means. - """ diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py index 5ff92bb6..e69de29b 100644 --- a/telethon/tl/custom/conversation.py +++ b/telethon/tl/custom/conversation.py @@ -1,466 +0,0 @@ -import asyncio -import itertools -import time - -from .chatgetter import ChatGetter -from ... import helpers, utils, errors -from ...events.common import EventCommon - -# Sometimes the edits arrive very fast (within the same second). -# In that case we add a small delta so that the age is older, for -# comparision purposes. This value is enough for up to 1000 messages. -_EDIT_COLLISION_DELTA = 0.001 - - -class Conversation(ChatGetter): - """ - Represents a conversation inside an specific chat. - - A conversation keeps track of new messages since it was - created until its exit and easily lets you query the - current state. - - If you need a conversation across two or more chats, - you should use two conversations and synchronize them - as you better see fit. - """ - _id_counter = 0 - _custom_counter = 0 - - def __init__(self, client, input_chat, - *, timeout, total_timeout, max_messages, - exclusive, replies_are_responses): - # This call resets the client - ChatGetter.__init__(self, input_chat=input_chat) - - self._id = Conversation._id_counter - Conversation._id_counter += 1 - - self._client = client - self._timeout = timeout - self._total_timeout = total_timeout - self._total_due = None - - self._outgoing = set() - self._last_outgoing = 0 - self._incoming = [] - self._last_incoming = 0 - self._max_incoming = max_messages - self._last_read = None - self._custom = {} - - self._pending_responses = {} - self._pending_replies = {} - self._pending_edits = {} - self._pending_reads = {} - - self._exclusive = exclusive - self._cancelled = False - - # The user is able to expect two responses for the same message. - # {desired message ID: next incoming index} - self._response_indices = {} - if replies_are_responses: - self._reply_indices = self._response_indices - else: - self._reply_indices = {} - - self._edit_dates = {} - - async def send_message(self, *args, **kwargs): - """ - Sends a message in the context of this conversation. Shorthand - for `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - message = await self._client.send_message( - self._input_chat, *args, **kwargs) - - self._outgoing.add(message.id) - self._last_outgoing = message.id - return message - - async def send_file(self, *args, **kwargs): - """ - Sends a file in the context of this conversation. Shorthand - for `telethon.client.uploads.UploadMethods.send_file` with - ``entity`` already set. - """ - message = await self._client.send_file( - self._input_chat, *args, **kwargs) - - self._outgoing.add(message.id) - self._last_outgoing = message.id - return message - - def mark_read(self, message=None): - """ - Marks as read the latest received message if ``message is None``. - Otherwise, marks as read until the given message (or message ID). - - This is equivalent to calling `client.send_read_acknowledge - `. - """ - if message is None: - if self._incoming: - message = self._incoming[-1].id - else: - message = 0 - elif not isinstance(message, int): - message = message.id - - return self._client.send_read_acknowledge( - self._input_chat, max_id=message) - - async def get_response(self, message=None, *, timeout=None): - """ - Gets the next message that responds to a previous one. - - Args: - message (`Message ` | `int`, optional): - The message (or the message ID) for which a response - is expected. By default this is the last sent message. - - timeout (`int` | `float`, optional): - If present, this `timeout` (in seconds) will override the - per-action timeout defined for the conversation. - """ - return await self._get_message( - message, self._response_indices, self._pending_responses, timeout, - lambda x, y: True - ) - - async def get_reply(self, message=None, *, timeout=None): - """ - Gets the next message that explicitly replies to a previous one. - """ - return await self._get_message( - message, self._reply_indices, self._pending_replies, timeout, - lambda x, y: x.reply_to_msg_id == y - ) - - def _get_message( - self, target_message, indices, pending, timeout, condition): - """ - Gets the next desired message under the desired condition. - - Args: - target_message (`object`): - The target message for which we want to find another - response that applies based on `condition`. - - indices (`dict`): - This dictionary remembers the last ID chosen for the - input `target_message`. - - pending (`dict`): - This dictionary remembers {msg_id: Future} to be set - once `condition` is met. - - timeout (`int`): - The timeout (in seconds) override to use for this operation. - - condition (`callable`): - The condition callable that checks if an incoming - message is a valid response. - """ - start_time = time.time() - target_id = self._get_message_id(target_message) - - # If there is no last-chosen ID, make sure to pick one *after* - # the input message, since we don't want responses back in time - if target_id not in indices: - for i, incoming in enumerate(self._incoming): - if incoming.id > target_id: - indices[target_id] = i - break - else: - indices[target_id] = len(self._incoming) - - # We will always return a future from here, even if the result - # can be set immediately. Otherwise, needing to await only - # sometimes is an annoying edge case (i.e. we would return - # a `Message` but `get_response()` always `await`'s). - future = self._client.loop.create_future() - - # If there are enough responses saved return the next one - last_idx = indices[target_id] - if last_idx < len(self._incoming): - incoming = self._incoming[last_idx] - if condition(incoming, target_id): - indices[target_id] += 1 - future.set_result(incoming) - return future - - # Otherwise the next incoming response will be the one to use - # - # Note how we fill "pending" before giving control back to the - # event loop through "await". We want to register it as soon as - # possible, since any other task switch may arrive with the result. - pending[target_id] = future - return self._get_result(future, start_time, timeout, pending, target_id) - - async def get_edit(self, message=None, *, timeout=None): - """ - Awaits for an edit after the last message to arrive. - The arguments are the same as those for `get_response`. - """ - start_time = time.time() - target_id = self._get_message_id(message) - - target_date = self._edit_dates.get(target_id, 0) - earliest_edit = min( - (x for x in self._incoming - if x.edit_date - and x.id > target_id - and x.edit_date.timestamp() > target_date - ), - key=lambda x: x.edit_date.timestamp(), - default=None - ) - - if earliest_edit and earliest_edit.edit_date.timestamp() > target_date: - self._edit_dates[target_id] = earliest_edit.edit_date.timestamp() - return earliest_edit - - # Otherwise the next incoming response will be the one to use - future = self._client.loop.create_future() - self._pending_edits[target_id] = future - return await self._get_result(future, start_time, timeout, self._pending_edits, target_id) - - async def wait_read(self, message=None, *, timeout=None): - """ - Awaits for the sent message to be marked as read. Note that - receiving a response doesn't imply the message was read, and - this action will also trigger even without a response. - """ - start_time = time.time() - future = self._client.loop.create_future() - target_id = self._get_message_id(message) - - if self._last_read is None: - self._last_read = target_id - 1 - - if self._last_read >= target_id: - return - - self._pending_reads[target_id] = future - return await self._get_result(future, start_time, timeout, self._pending_reads, target_id) - - async def wait_event(self, event, *, timeout=None): - """ - Waits for a custom event to occur. Timeouts still apply. - - Unless you're certain that your code will run fast enough, - generally you should get a "handle" of this special coroutine - before acting. Generally, you should do this: - - >>> from telethon import TelegramClient, events - >>> - >>> client = TelegramClient(...) - >>> - >>> async def main(): - >>> async with client.conversation(...) as conv: - >>> response = conv.wait_event(events.NewMessage(incoming=True)) - >>> await conv.send_message('Hi') - >>> response = await response - - This way your event can be registered before acting, - since the response may arrive before your event was - registered. It depends on your use case since this - also means the event can arrive before you send - a previous action. - """ - start_time = time.time() - if isinstance(event, type): - event = event() - - await event.resolve(self._client) - - counter = Conversation._custom_counter - Conversation._custom_counter += 1 - - future = self._client.loop.create_future() - self._custom[counter] = (event, future) - return await self._get_result(future, start_time, timeout, self._custom, counter) - - async def _check_custom(self, built): - for key, (ev, fut) in list(self._custom.items()): - ev_type = type(ev) - inst = built[ev_type] - if inst and ev.filter(inst): - fut.set_result(inst) - del self._custom[key] - - def _on_new_message(self, response): - response = response.message - if response.chat_id != self.chat_id or response.out: - return - - if len(self._incoming) == self._max_incoming: - self._cancel_all(ValueError('Too many incoming messages')) - return - - self._incoming.append(response) - - # Most of the time, these dictionaries will contain just one item - # TODO In fact, why not make it be that way? Force one item only. - # How often will people want to wait for two responses at - # the same time? It's impossible, first one will arrive - # and then another, so they can do that. - for msg_id, future in list(self._pending_responses.items()): - self._response_indices[msg_id] = len(self._incoming) - future.set_result(response) - del self._pending_responses[msg_id] - - for msg_id, future in list(self._pending_replies.items()): - if msg_id == response.reply_to_msg_id: - self._reply_indices[msg_id] = len(self._incoming) - future.set_result(response) - del self._pending_replies[msg_id] - - def _on_edit(self, message): - message = message.message - if message.chat_id != self.chat_id or message.out: - return - - for msg_id, future in list(self._pending_edits.items()): - if msg_id < message.id: - edit_ts = message.edit_date.timestamp() - - # We compare <= because edit_ts resolution is always to - # seconds, but we may have increased _edit_dates before. - # Since the dates are ever growing this is not a problem. - if edit_ts <= self._edit_dates.get(msg_id, 0): - self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA - else: - self._edit_dates[msg_id] = message.edit_date.timestamp() - - future.set_result(message) - del self._pending_edits[msg_id] - - def _on_read(self, event): - if event.chat_id != self.chat_id or event.inbox: - return - - self._last_read = event.max_id - - remove_reads = [] - for msg_id, pending in list(self._pending_reads.items()): - if msg_id >= self._last_read: - remove_reads.append(msg_id) - pending.set_result(True) - del self._pending_reads[msg_id] - - for to_remove in remove_reads: - del self._pending_reads[to_remove] - - def _get_message_id(self, message): - if message is not None: # 0 is valid but false-y, check for None - return message if isinstance(message, int) else message.id - elif self._last_outgoing: - return self._last_outgoing - else: - raise ValueError('No message was sent previously') - - async def _get_result(self, future, start_time, timeout, pending, target_id): - if self._cancelled: - raise asyncio.CancelledError('The conversation was cancelled before') - - due = self._total_due - if timeout is None: - timeout = self._timeout - - if timeout is not None: - due = min(due, start_time + timeout) - - # NOTE: We can't try/finally to pop from pending here because - # the event loop needs to get back to us, but it might - # dispatch another update before, and in that case a - # response could be set twice. So responses must be - # cleared when their futures are set to a result. - return await asyncio.wait_for( - future, - timeout=None if due == float('inf') else due - time.time(), - loop=self._client.loop - ) - - def _cancel_all(self, exception=None): - self._cancelled = True - for pending in itertools.chain( - self._pending_responses.values(), - self._pending_replies.values(), - self._pending_edits.values()): - if exception: - pending.set_exception(exception) - else: - pending.cancel() - - for _, fut in self._custom.values(): - if exception: - fut.set_exception(exception) - else: - fut.cancel() - - async def __aenter__(self): - self._input_chat = \ - await self._client.get_input_entity(self._input_chat) - - self._chat_peer = utils.get_peer(self._input_chat) - - # Make sure we're the only conversation in this chat if it's exclusive - chat_id = utils.get_peer_id(self._chat_peer) - conv_set = self._client._conversations[chat_id] - if self._exclusive and conv_set: - raise errors.AlreadyInConversationError() - - conv_set.add(self) - self._cancelled = False - - self._last_outgoing = 0 - self._last_incoming = 0 - for d in ( - self._outgoing, self._incoming, - self._pending_responses, self._pending_replies, - self._pending_edits, self._response_indices, - self._reply_indices, self._edit_dates, self._custom): - d.clear() - - if self._total_timeout: - self._total_due = time.time() + self._total_timeout - else: - self._total_due = float('inf') - - return self - - def cancel(self): - """ - Cancels the current conversation. Pending responses and subsequent - calls to get a response will raise ``asyncio.CancelledError``. - - This method is synchronous and should not be awaited. - """ - self._cancel_all() - - async def cancel_all(self): - """ - Calls `cancel` on *all* conversations in this chat. - - Note that you should ``await`` this method, since it's meant to be - used outside of a context manager, and it needs to resolve the chat. - """ - chat_id = await self._client.get_peer_id(self._input_chat) - for conv in self._client._conversations[chat_id]: - conv.cancel() - - async def __aexit__(self, exc_type, exc_val, exc_tb): - chat_id = utils.get_peer_id(self._chat_peer) - conv_set = self._client._conversations[chat_id] - conv_set.discard(self) - if not conv_set: - del self._client._conversations[chat_id] - - self._cancel_all() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py index 8e82feaa..e69de29b 100644 --- a/telethon/tl/custom/dialog.py +++ b/telethon/tl/custom/dialog.py @@ -1,158 +0,0 @@ -from . import Draft -from .. import TLObject, types, functions -from ... import utils - - -class Dialog: - """ - Custom class that encapsulates a dialog (an open "conversation" with - someone, a group or a channel) providing an abstraction to easily - access the input version/normal entity/message etc. The library will - return instances of this class when calling :meth:`.get_dialogs()`. - - Args: - dialog (:tl:`Dialog`): - The original ``Dialog`` instance. - - pinned (`bool`): - Whether this dialog is pinned to the top or not. - - folder_id (`folder_id`): - The folder ID that this dialog belongs to. - - archived (`bool`): - Whether this dialog is archived or not (``folder_id is None``). - - message (`Message `): - The last message sent on this dialog. Note that this member - will not be updated when new messages arrive, it's only set - on creation of the instance. - - date (`datetime`): - The date of the last message sent on this dialog. - - entity (`entity`): - The entity that belongs to this dialog (user, chat or channel). - - input_entity (:tl:`InputPeer`): - Input version of the entity. - - id (`int`): - The marked ID of the entity, which is guaranteed to be unique. - - name (`str`): - Display name for this dialog. For chats and channels this is - their title, and for users it's "First-Name Last-Name". - - title (`str`): - Alias for `name`. - - unread_count (`int`): - How many messages are currently unread in this dialog. Note that - this value won't update when new messages arrive. - - unread_mentions_count (`int`): - How many mentions are currently unread in this dialog. Note that - this value won't update when new messages arrive. - - draft (`Draft `): - The draft object in this dialog. It will not be ``None``, - so you can call ``draft.set_message(...)``. - - is_user (`bool`): - ``True`` if the `entity` is a :tl:`User`. - - is_group (`bool`): - ``True`` if the `entity` is a :tl:`Chat` - or a :tl:`Channel` megagroup. - - is_channel (`bool`): - ``True`` if the `entity` is a :tl:`Channel`. - """ - def __init__(self, client, dialog, entities, messages): - # Both entities and messages being dicts {ID: item} - self._client = client - self.dialog = dialog - self.pinned = bool(dialog.pinned) - self.folder_id = dialog.folder_id - self.archived = dialog.folder_id is not None - self.message = messages.get(dialog.top_message, None) - self.date = getattr(self.message, 'date', None) - - self.entity = entities[utils.get_peer_id(dialog.peer)] - self.input_entity = utils.get_input_peer(self.entity) - self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf() - self.name = self.title = utils.get_display_name(self.entity) - - self.unread_count = dialog.unread_count - self.unread_mentions_count = dialog.unread_mentions_count - - self.draft = Draft._from_dialog(client, self) - - self.is_user = isinstance(self.entity, types.User) - self.is_group = ( - isinstance(self.entity, (types.Chat, types.ChatForbidden)) or - (isinstance(self.entity, types.Channel) and self.entity.megagroup) - ) - self.is_channel = isinstance(self.entity, types.Channel) - - async def send_message(self, *args, **kwargs): - """ - Sends a message to this dialog. This is just a wrapper around - ``client.send_message(dialog.input_entity, *args, **kwargs)``. - """ - return await self._client.send_message( - self.input_entity, *args, **kwargs) - - async def delete(self, revoke=False): - """ - Deletes the dialog from your dialog list. If you own the - channel this won't destroy it, only delete it from the list. - - Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog` - with ``entity`` already set. - """ - await self._client.delete_dialog(self.input_entity, revoke=revoke) - - async def archive(self, folder=1): - """ - Archives (or un-archives) this dialog. - - Args: - folder (`int`, optional): - The folder to which the dialog should be archived to. - - If you want to "un-archive" it, use ``folder=0``. - - Returns: - The :tl:`Updates` object that the request produces. - - Example: - - .. code-block:: python - - # Archiving - dialog.archive() - - # Un-archiving - dialog.archive(0) - """ - return await self._client(functions.folders.EditPeerFoldersRequest([ - types.InputFolderPeer(self.input_entity, folder_id=folder) - ])) - - def to_dict(self): - return { - '_': 'Dialog', - 'name': self.name, - 'date': self.date, - 'draft': self.draft, - 'message': self.message, - 'entity': self.entity, - } - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py index eb12ad3d..e69de29b 100644 --- a/telethon/tl/custom/draft.py +++ b/telethon/tl/custom/draft.py @@ -1,201 +0,0 @@ -import datetime - -from .. import TLObject -from ..functions.messages import SaveDraftRequest -from ..types import UpdateDraftMessage, DraftMessage -from ...errors import RPCError -from ...extensions import markdown -from ...utils import get_peer_id, get_input_peer - - -class Draft: - """ - Custom class that encapsulates a draft on the Telegram servers, providing - an abstraction to change the message conveniently. The library will return - instances of this class when calling :meth:`get_drafts()`. - - Args: - date (`datetime`): - The date of the draft. - - link_preview (`bool`): - Whether the link preview is enabled or not. - - reply_to_msg_id (`int`): - The message ID that the draft will reply to. - """ - def __init__(self, client, peer, draft, entity): - self._client = client - self._peer = peer - self._entity = entity - self._input_entity = get_input_peer(entity) if entity else None - - if not draft or not isinstance(draft, DraftMessage): - draft = DraftMessage('', None, None, None, None) - - self._text = markdown.unparse(draft.message, draft.entities) - self._raw_text = draft.message - self.date = draft.date - self.link_preview = not draft.no_webpage - self.reply_to_msg_id = draft.reply_to_msg_id - - @classmethod - def _from_dialog(cls, client, dialog): - return cls(client=client, peer=dialog.dialog.peer, - draft=dialog.dialog.draft, entity=dialog.entity) - - @classmethod - def _from_update(cls, client, update, entities=None): - assert isinstance(update, UpdateDraftMessage) - return cls(client=client, peer=update.peer, draft=update.draft, - entity=(entities or {}).get(get_peer_id(update.peer))) - - @property - def entity(self): - """ - The entity that belongs to this dialog (user, chat or channel). - """ - return self._entity - - @property - def input_entity(self): - """ - Input version of the entity. - """ - if not self._input_entity: - try: - self._input_entity = self._client._entity_cache[self._peer] - except KeyError: - pass - - return self._input_entity - - async def get_entity(self): - """ - Returns `entity` but will make an API call if necessary. - """ - if not self.entity and await self.get_input_entity(): - try: - self._entity =\ - await self._client.get_entity(self._input_entity) - except ValueError: - pass - - return self._entity - - async def get_input_entity(self): - """ - Returns `input_entity` but will make an API call if necessary. - """ - # We don't actually have an API call we can make yet - # to get more info, but keep this method for consistency. - return self.input_entity - - @property - def text(self): - """ - The markdown text contained in the draft. It will be - empty if there is no text (and hence no draft is set). - """ - return self._text - - @property - def raw_text(self): - """ - The raw (text without formatting) contained in the draft. - It will be empty if there is no text (thus draft not set). - """ - return self._raw_text - - @property - def is_empty(self): - """ - Convenience bool to determine if the draft is empty or not. - """ - return not self._text - - async def set_message( - self, text=None, reply_to=0, parse_mode=(), - link_preview=None): - """ - Changes the draft message on the Telegram servers. The changes are - reflected in this object. - - :param str text: New text of the draft. - Preserved if left as None. - - :param int reply_to: Message ID to reply to. - Preserved if left as 0, erased if set to None. - - :param bool link_preview: Whether to attach a web page preview. - Preserved if left as None. - - :param str parse_mode: The parse mode to be used for the text. - :return bool: ``True`` on success. - """ - if text is None: - text = self._text - - if reply_to == 0: - reply_to = self.reply_to_msg_id - - if link_preview is None: - link_preview = self.link_preview - - raw_text, entities =\ - await self._client._parse_message_text(text, parse_mode) - - result = await self._client(SaveDraftRequest( - peer=self._peer, - message=raw_text, - no_webpage=not link_preview, - reply_to_msg_id=reply_to, - entities=entities - )) - - if result: - self._text = text - self._raw_text = raw_text - self.link_preview = link_preview - self.reply_to_msg_id = reply_to - self.date = datetime.datetime.now(tz=datetime.timezone.utc) - - return result - - async def send(self, clear=True, parse_mode=()): - """ - Sends the contents of this draft to the dialog. This is just a - wrapper around ``send_message(dialog.input_entity, *args, **kwargs)``. - """ - await self._client.send_message( - self._peer, self.text, reply_to=self.reply_to_msg_id, - link_preview=self.link_preview, parse_mode=parse_mode, - clear_draft=clear - ) - - async def delete(self): - """ - Deletes this draft, and returns ``True`` on success. - """ - return await self.set_message(text='') - - def to_dict(self): - try: - entity = self.entity - except RPCError as e: - entity = e - - return { - '_': 'Draft', - 'text': self.text, - 'entity': entity, - 'date': self.date, - 'link_preview': self.link_preview, - 'reply_to_msg_id': self.reply_to_msg_id - } - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/tl/custom/file.py b/telethon/tl/custom/file.py index 1c17149a..e69de29b 100644 --- a/telethon/tl/custom/file.py +++ b/telethon/tl/custom/file.py @@ -1,136 +0,0 @@ -import mimetypes -import os - -from ... import utils -from ...tl import types - - -class File: - """ - Convenience class over media like photos or documents, which - supports accessing the attributes in a more convenient way. - - If any of the attributes are not present in the current media, - the properties will be ``None``. - - The original media is available through the ``media`` attribute. - """ - def __init__(self, media): - self.media = media - - @property - def id(self): - """ - The bot-API style ``file_id`` representing this file. - """ - return utils.pack_bot_file_id(self.media) - - @property - def name(self): - """ - The file name of this document. - """ - return self._from_attr(types.DocumentAttributeFilename, 'file_name') - - @property - def ext(self): - """ - The extension from the mime type of this file. - - If the mime type is unknown, the extension - from the file name (if any) will be used. - """ - return ( - mimetypes.guess_extension(self.mime_type) - or os.path.splitext(self.name or '')[-1] - or None - ) - - @property - def mime_type(self): - """ - The mime-type of this file. - """ - if isinstance(self.media, types.Photo): - return 'image/jpeg' - elif isinstance(self.media, types.Document): - return self.media.mime_type - - @property - def width(self): - """ - The width in pixels of this media if it's a photo or a video. - """ - return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') - - @property - def height(self): - """ - The height in pixels of this media if it's a photo or a video. - """ - return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') - - @property - def duration(self): - """ - The duration in seconds of the audio or video. - """ - return self._from_attr(( - types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') - - @property - def title(self): - """ - The title of the song. - """ - return self._from_attr(types.DocumentAttributeAudio, 'title') - - @property - def performer(self): - """ - The performer of the song. - """ - return self._from_attr(types.DocumentAttributeAudio, 'performer') - - @property - def emoji(self): - """ - A string with all emoji that represent the current sticker. - """ - return self._from_attr(types.DocumentAttributeSticker, 'alt') - - @property - def sticker_set(self): - """ - The :tl:`InputStickerSet` to which the sticker file belongs. - """ - return self._from_attr(types.DocumentAttributeSticker, 'stickerset') - - @property - def size(self): - """ - The size in bytes of this file. - """ - if isinstance(self.media, types.Photo): - return self._size_for(self.media.sizes[-1]) - elif isinstance(self.media, types.Document): - return self.media.size - - @staticmethod - def _size_for(kind): - if isinstance(kind, types.PhotoSize): - return kind.size - elif isinstance(kind, types.PhotoStrippedSize): - return utils._stripped_real_length(kind.bytes) - elif isinstance(kind, types.PhotoCachedSize): - return len(kind.bytes) - # elif isinstance(kind, types.PhotoSizeEmpty): - return 0 - - def _from_attr(self, cls, field): - if isinstance(self.media, types.Document): - for attr in self.media.attributes: - if isinstance(attr, cls): - return getattr(attr, field, None) diff --git a/telethon/tl/custom/forward.py b/telethon/tl/custom/forward.py index 7943effa..e69de29b 100644 --- a/telethon/tl/custom/forward.py +++ b/telethon/tl/custom/forward.py @@ -1,47 +0,0 @@ -from .chatgetter import ChatGetter -from .sendergetter import SenderGetter -from ... import utils -from ...tl import types - - -class Forward(ChatGetter, SenderGetter): - """ - Custom class that encapsulates a :tl:`MessageFwdHeader` providing an - abstraction to easily access information like the original sender. - - Remember that this class implements `ChatGetter - ` and `SenderGetter - ` which means you - have access to all their sender and chat properties and methods. - - Attributes: - - original_fwd (:tl:`MessageFwdHeader`): - The original :tl:`MessageFwdHeader` instance. - - Any other attribute: - Attributes not described here are the same as those available - in the original :tl:`MessageFwdHeader`. - """ - def __init__(self, client, original, entities): - # Copy all the fields, not reference! It would cause memory cycles: - # self.original_fwd.original_fwd.original_fwd.original_fwd - # ...would be valid if we referenced. - self.__dict__ = dict(original.__dict__) - self._client = client - self.original_fwd = original - - sender, input_sender = utils._get_entity_pair( - original.from_id, entities, client._entity_cache) - - if not original.channel_id: - peer = chat = input_chat = None - else: - peer = types.PeerChannel(original.channel_id) - chat, input_chat = utils._get_entity_pair( - utils.get_peer_id(peer), entities, client._entity_cache) - - ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) - SenderGetter.__init__(self, original.from_id, sender=sender, input_sender=input_sender) - - # TODO We could reload the message diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py index 2f4cc7d4..e69de29b 100644 --- a/telethon/tl/custom/inlinebuilder.py +++ b/telethon/tl/custom/inlinebuilder.py @@ -1,334 +0,0 @@ -import hashlib - -from .. import functions, types -from ... import utils - - -class InlineBuilder: - """ - Helper class to allow defining `InlineQuery - ` ``results``. - - Common arguments to all methods are - explained here to avoid repetition: - - text (`str`, optional): - If present, the user will send a text - message with this text upon being clicked. - - link_preview (`bool`, optional): - Whether to show a link preview in the sent - text message or not. - - geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional): - If present, it may either be a geo point or a venue. - - period (int, optional): - The period in seconds to be used for geo points. - - contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional): - If present, it must be the contact information to send. - - game (`bool`, optional): - May be ``True`` to indicate that the game will be sent. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, optional): - Same as ``buttons`` for `client.send_message() - `. - - parse_mode (`str`, optional): - Same as ``parse_mode`` for `client.send_message() - `. - - id (`str`, optional): - The string ID to use for this result. If not present, it - will be the SHA256 hexadecimal digest of converting the - created :tl:`InputBotInlineResult` with empty ID to ``bytes()``, - so that the ID will be deterministic for the same input. - - .. note:: - - If two inputs are exactly the same, their IDs will be the same - too. If you send two articles with the same ID, it will raise - ``ResultIdDuplicateError``. Consider giving them an explicit - ID if you need to send two results that are the same. - """ - def __init__(self, client): - self._client = client - - # noinspection PyIncorrectDocstring - async def article( - self, title, description=None, - *, url=None, thumb=None, content=None, - id=None, text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates new inline result of article type. - - Args: - title (`str`): - The title to be shown for this result. - - description (`str`, optional): - Further explanation of what this result means. - - url (`str`, optional): - The URL to be shown for this result. - - thumb (:tl:`InputWebDocument`, optional): - The thumbnail to be shown for this result. - For now it has to be a :tl:`InputWebDocument` if present. - - content (:tl:`InputWebDocument`, optional): - The content to be shown for this result. - For now it has to be a :tl:`InputWebDocument` if present. - """ - # TODO Does 'article' work always? - # article, photo, gif, mpeg4_gif, video, audio, - # voice, document, location, venue, contact, game - result = types.InputBotInlineResult( - id=id or '', - type='article', - send_message=await self._message( - text=text, parse_mode=parse_mode, link_preview=link_preview, - geo=geo, period=period, - contact=contact, - game=game, - buttons=buttons - ), - title=title, - description=description, - url=url, - thumb=thumb, - content=content - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def photo( - self, file, *, id=None, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates a new inline result of photo type. - - Args: - file (`obj`, optional): - Same as ``file`` for `client.send_file() - `. - """ - try: - fh = utils.get_input_photo(file) - except TypeError: - _, media, _ = await self._client._file_to_media( - file, allow_cache=True, as_image=True - ) - if isinstance(media, types.InputPhoto): - fh = media - else: - r = await self._client(functions.messages.UploadMediaRequest( - types.InputPeerSelf(), media=media - )) - fh = utils.get_input_photo(r.photo) - - result = types.InputBotInlineResultPhoto( - id=id or '', - type='photo', - photo=fh, - send_message=await self._message( - text=text or '', - parse_mode=parse_mode, - link_preview=link_preview, - geo=geo, - period=period, - contact=contact, - game=game, - buttons=buttons - ) - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def document( - self, file, title=None, *, description=None, type=None, - mime_type=None, attributes=None, force_document=False, - voice_note=False, video_note=False, use_cache=True, id=None, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates a new inline result of document type. - - `use_cache`, `mime_type`, `attributes`, `force_document`, - `voice_note` and `video_note` are described in `client.send_file - `. - - Args: - file (`obj`): - Same as ``file`` for `client.send_file() - `. - - title (`str`, optional): - The title to be shown for this result. - - description (`str`, optional): - Further explanation of what this result means. - - type (`str`, optional): - The type of the document. May be one of: photo, gif, - mpeg4_gif, video, audio, voice, document, sticker. - - See "Type of the result" in https://core.telegram.org/bots/api. - """ - if type is None: - if voice_note: - type = 'voice' - else: - type = 'document' - - try: - fh = utils.get_input_document(file) - except TypeError: - _, media, _ = await self._client._file_to_media( - file, - mime_type=mime_type, - attributes=attributes, - force_document=True, - voice_note=voice_note, - video_note=video_note, - allow_cache=use_cache - ) - if isinstance(media, types.InputDocument): - fh = media - else: - r = await self._client(functions.messages.UploadMediaRequest( - types.InputPeerSelf(), media=media - )) - fh = utils.get_input_document(r.document) - - result = types.InputBotInlineResultDocument( - id=id or '', - type=type, - document=fh, - send_message=await self._message( - # Empty string for text if there's media but text is None. - # We may want to display a document but send text; however - # default to sending the media (without text, i.e. stickers). - text=text or '', - parse_mode=parse_mode, - link_preview=link_preview, - geo=geo, - period=period, - contact=contact, - game=game, - buttons=buttons - ), - title=title, - description=description - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def game( - self, short_name, *, id=None, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates a new inline result of game type. - - Args: - short_name (`str`): - The short name of the game to use. - """ - result = types.InputBotInlineResultGame( - id=id or '', - short_name=short_name, - send_message=await self._message( - text=text, parse_mode=parse_mode, link_preview=link_preview, - geo=geo, period=period, - contact=contact, - game=game, - buttons=buttons - ) - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - async def _message( - self, *, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - # Empty strings are valid but false-y; if they're empty use dummy '\0' - args = ('\0' if text == '' else text, geo, contact, game) - if sum(1 for x in args if x is not None and x is not False) != 1: - raise ValueError( - 'Must set exactly one of text, geo, contact or game (set {})' - .format(', '.join(x[0] for x in zip( - 'text geo contact game'.split(), args) if x[1]) or 'none') - ) - - markup = self._client.build_reply_markup(buttons, inline_only=True) - if text is not None: - if not text: # Automatic media on empty string, like stickers - return types.InputBotInlineMessageMediaAuto('') - - text, msg_entities = await self._client._parse_message_text( - text, parse_mode - ) - return types.InputBotInlineMessageText( - message=text, - no_webpage=not link_preview, - entities=msg_entities, - reply_markup=markup - ) - elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): - return types.InputBotInlineMessageMediaGeo( - geo_point=utils.get_input_geo(geo), - period=period, - reply_markup=markup - ) - elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)): - if isinstance(geo, types.InputMediaVenue): - geo_point = geo.geo_point - else: - geo_point = geo.geo - - return types.InputBotInlineMessageMediaVenue( - geo_point=geo_point, - title=geo.title, - address=geo.address, - provider=geo.provider, - venue_id=geo.venue_id, - venue_type=geo.venue_type, - reply_markup=markup - ) - elif isinstance(contact, ( - types.InputMediaContact, types.MessageMediaContact)): - return types.InputBotInlineMessageMediaContact( - phone_number=contact.phone_number, - first_name=contact.first_name, - last_name=contact.last_name, - vcard=contact.vcard, - reply_markup=markup - ) - elif game: - return types.InputBotInlineMessageGame( - reply_markup=markup - ) - else: - raise ValueError('No text, game or valid geo or contact given') diff --git a/telethon/tl/custom/inlineresult.py b/telethon/tl/custom/inlineresult.py index 04e82cab..e69de29b 100644 --- a/telethon/tl/custom/inlineresult.py +++ b/telethon/tl/custom/inlineresult.py @@ -1,147 +0,0 @@ -from .. import types, functions -from ... import utils - - -class InlineResult: - """ - Custom class that encapsulates a bot inline result providing - an abstraction to easily access some commonly needed features - (such as clicking a result to select it). - - Attributes: - - result (:tl:`BotInlineResult`): - The original :tl:`BotInlineResult` object. - """ - ARTICLE = 'article' - PHOTO = 'photo' - GIF = 'gif' - VIDEO = 'video' - VIDEO_GIF = 'mpeg4_gif' - AUDIO = 'audio' - DOCUMENT = 'document' - LOCATION = 'location' - VENUE = 'venue' - CONTACT = 'contact' - GAME = 'game' - - def __init__(self, client, original, query_id=None): - self._client = client - self.result = original - self._query_id = query_id - - @property - def type(self): - """ - The always-present type of this result. It will be one of: - ``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``, - ``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``, - ``'contact'``, ``'game'``. - - You can access all of these constants through `InlineResult`, - such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc. - """ - return self.result.type - - @property - def message(self): - """ - The always-present :tl:`BotInlineMessage` that - will be sent if `click` is called on this result. - """ - return self.result.send_message - - @property - def title(self): - """ - The title for this inline result. It may be ``None``. - """ - return self.result.title - - @property - def description(self): - """ - The description for this inline result. It may be ``None``. - """ - return self.result.description - - @property - def url(self): - """ - The URL present in this inline results. If you want to "click" - this URL to open it in your browser, you should use Python's - `webbrowser.open(url)` for such task. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.url - - @property - def photo(self): - """ - Returns either the :tl:`WebDocument` thumbnail for - normal results or the :tl:`Photo` for media results. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.thumb - elif isinstance(self.result, types.BotInlineMediaResult): - return self.result.photo - - @property - def document(self): - """ - Returns either the :tl:`WebDocument` content for - normal results or the :tl:`Document` for media results. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.content - elif isinstance(self.result, types.BotInlineMediaResult): - return self.result.document - - async def click(self, entity, reply_to=None, - silent=False, clear_draft=False, hide_via=False): - """ - Clicks this result and sends the associated `message`. - - Args: - entity (`entity`): - The entity to which the message of this result should be sent. - - reply_to (`int` | `Message `, optional): - If present, the sent message will reply to this ID or message. - - silent (`bool`, optional): - If ``True``, the sent message will not notify the user(s). - - clear_draft (`bool`, optional): - Whether the draft should be removed after sending the - message from this result or not. Defaults to ``False``. - - hide_via (`bool`, optional): - Whether the "via @bot" should be hidden or not. - Only works with certain bots (like @bing or @gif). - """ - entity = await self._client.get_input_entity(entity) - reply_id = None if reply_to is None else utils.get_message_id(reply_to) - req = functions.messages.SendInlineBotResultRequest( - peer=entity, - query_id=self._query_id, - id=self.result.id, - silent=silent, - clear_draft=clear_draft, - hide_via=hide_via, - reply_to_msg_id=reply_id - ) - return self._client._get_response_message( - req, await self._client(req), entity) - - async def download_media(self, *args, **kwargs): - """ - Downloads the media in this result (if there is a document, the - document will be downloaded; otherwise, the photo will if present). - - This is a wrapper around `client.download_media - `. - """ - if self.document or self.photo: - return await self._client.download_media( - self.document or self.photo, *args, **kwargs) diff --git a/telethon/tl/custom/inlineresults.py b/telethon/tl/custom/inlineresults.py index 2ac5ebfa..e69de29b 100644 --- a/telethon/tl/custom/inlineresults.py +++ b/telethon/tl/custom/inlineresults.py @@ -1,83 +0,0 @@ -import time - -from .inlineresult import InlineResult - - -class InlineResults(list): - """ - Custom class that encapsulates :tl:`BotResults` providing - an abstraction to easily access some commonly needed features - (such as clicking one of the results to select it) - - Note that this is a list of `InlineResult - ` - so you can iterate over it or use indices to - access its elements. In addition, it has some - attributes. - - Attributes: - result (:tl:`BotResults`): - The original :tl:`BotResults` object. - - query_id (`int`): - The random ID that identifies this query. - - cache_time (`int`): - For how long the results should be considered - valid. You can call `results_valid` at any - moment to determine if the results are still - valid or not. - - users (:tl:`User`): - The users present in this inline query. - - gallery (`bool`): - Whether these results should be presented - in a grid (as a gallery of images) or not. - - next_offset (`str`, optional): - The string to be used as an offset to get - the next chunk of results, if any. - - switch_pm (:tl:`InlineBotSwitchPM`, optional): - If presents, the results should show a button to - switch to a private conversation with the bot using - the text in this object. - """ - def __init__(self, client, original): - super().__init__(InlineResult(client, x, original.query_id) - for x in original.results) - - self.result = original - self.query_id = original.query_id - self.cache_time = original.cache_time - self._valid_until = time.time() + self.cache_time - self.users = original.users - self.gallery = bool(original.gallery) - self.next_offset = original.next_offset - self.switch_pm = original.switch_pm - - def results_valid(self): - """ - Returns ``True`` if the cache time has not expired - yet and the results can still be considered valid. - """ - return time.time() < self._valid_until - - def _to_str(self, item_function): - return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, ' - 'next_offset={}, switch_pm={}]'.format( - ', '.join(item_function(x) for x in self), - self.query_id, - self.cache_time, - self.users, - self.gallery, - self.next_offset, - self.switch_pm - )) - - def __str__(self): - return self._to_str(str) - - def __repr__(self): - return self._to_str(repr) diff --git a/telethon/tl/custom/inputsizedfile.py b/telethon/tl/custom/inputsizedfile.py index fcb743f6..e69de29b 100644 --- a/telethon/tl/custom/inputsizedfile.py +++ b/telethon/tl/custom/inputsizedfile.py @@ -1,9 +0,0 @@ -from ..types import InputFile - - -class InputSizedFile(InputFile): - """InputFile class with two extra parameters: md5 (digest) and size""" - def __init__(self, id_, parts, name, md5, size): - super().__init__(id_, parts, name, md5.hexdigest()) - self.md5 = md5.digest() - self.size = size diff --git a/telethon/tl/custom/message.py b/telethon/tl/custom/message.py index 643c76f9..e69de29b 100644 --- a/telethon/tl/custom/message.py +++ b/telethon/tl/custom/message.py @@ -1,959 +0,0 @@ -import abc -from .chatgetter import ChatGetter -from .sendergetter import SenderGetter -from .messagebutton import MessageButton -from .forward import Forward -from .file import File -from .. import TLObject, types, functions -from ... import utils, errors - - -# TODO Figure out a way to have the code generator error on missing fields -# Maybe parsing the init function alone if that's possible. -class Message(ChatGetter, SenderGetter, TLObject, abc.ABC): - """ - This custom class aggregates both :tl:`Message` and - :tl:`MessageService` to ease accessing their members. - - Remember that this class implements `ChatGetter - ` and `SenderGetter - ` which means you - have access to all their sender and chat properties and methods. - - Members: - id (`int`): - The ID of this message. This field is *always* present. - Any other member is optional and may be ``None``. - - out (`bool`): - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). - - Note that messages in your own chat are always incoming, - but this member will be ``True`` if you send a message - to your own chat. Messages you forward to your chat are - *not* considered outgoing, just like official clients - display them. - - mentioned (`bool`): - Whether you were mentioned in this message or not. - Note that replies to your own messages also count - as mentions. - - media_unread (`bool`): - Whether you have read the media in this message - or not, e.g. listened to the voice note media. - - silent (`bool`): - Whether this message should notify or not, - used in channels. - - post (`bool`): - Whether this message is a post in a broadcast - channel or not. - - from_scheduled (`bool`): - Whether this message was originated from - a scheduled one or not. - - legacy (`bool`): - Whether this is a legacy message or not. - - to_id (:tl:`Peer`): - The peer to which this message was sent, which is either - :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This - will always be present except for empty messages. - - date (`datetime`): - The UTC+0 `datetime` object indicating when this message - was sent. This will always be present except for empty - messages. - - message (`str`): - The string text of the message for `Message - ` instances, - which will be ``None`` for other types of messages. - - action (:tl:`MessageAction`): - The message action object of the message for :tl:`MessageService` - instances, which will be ``None`` for other types of messages. - - from_id (`int`): - The ID of the user who sent this message. This will be - ``None`` if the message was sent in a broadcast channel. - - reply_to_msg_id (`int`): - The ID to which this message is replying to, if any. - - fwd_from (:tl:`MessageFwdHeader`): - The original forward header if this message is a forward. - You should probably use the `forward` property instead. - - via_bot_id (`int`): - The ID of the bot used to send this message - through its inline mode (e.g. "via @like"). - - media (:tl:`MessageMedia`): - The media sent with this message if any (such as - photos, videos, documents, gifs, stickers, etc.). - - You may want to access the `photo`, `document` - etc. properties instead. - - If the media was not present or it was :tl:`MessageMediaEmpty`, - this member will instead be ``None`` for convenience. - - reply_markup (:tl:`ReplyMarkup`): - The reply markup for this message (which was sent - either via a bot or by a bot). You probably want - to access `buttons` instead. - - entities (List[:tl:`MessageEntity`]): - The list of markup entities in this message, - such as bold, italics, code, hyperlinks, etc. - - views (`int`): - The number of views this message from a broadcast - channel has. This is also present in forwards. - - edit_date (`datetime`): - The date when this message was last edited. - - post_author (`str`): - The display name of the message sender to - show in messages sent to broadcast channels. - - grouped_id (`int`): - If this message belongs to a group of messages - (photo albums or video albums), all of them will - have the same value here. - """ - - # region Initialization - - def __init__( - # Common to all - self, id, - - # Common to Message and MessageService (mandatory) - to_id=None, date=None, - - # Common to Message and MessageService (flags) - out=None, mentioned=None, media_unread=None, silent=None, - post=None, from_id=None, reply_to_msg_id=None, - - # For Message (mandatory) - message=None, - - # For Message (flags) - fwd_from=None, via_bot_id=None, media=None, reply_markup=None, - entities=None, views=None, edit_date=None, post_author=None, - grouped_id=None, from_scheduled=None, legacy=None, - - # For MessageAction (mandatory) - action=None): - # Common properties to all messages - self.id = id - self.to_id = to_id - self.date = date - self.out = out - self.mentioned = mentioned - self.media_unread = media_unread - self.silent = silent - self.post = post - self.from_id = from_id - self.reply_to_msg_id = reply_to_msg_id - self.message = message - self.fwd_from = fwd_from - self.via_bot_id = via_bot_id - self.media = None if isinstance( - media, types.MessageMediaEmpty) else media - - self.reply_markup = reply_markup - self.entities = entities - self.views = views - self.edit_date = edit_date - self.post_author = post_author - self.grouped_id = grouped_id - self.from_scheduled = from_scheduled - self.legacy = legacy - self.action = action - - # Convenient storage for custom functions - # TODO This is becoming a bit of bloat - self._client = None - self._text = None - self._file = None - self._reply_message = None - self._buttons = None - self._buttons_flat = None - self._buttons_count = None - self._via_bot = None - self._via_input_bot = None - self._action_entities = None - - if not out and isinstance(to_id, types.PeerUser): - chat_peer = types.PeerUser(from_id) - if from_id == to_id.user_id: - self.out = not self.fwd_from # Patch out in our chat - else: - chat_peer = to_id - - # Note that these calls would reset the client - ChatGetter.__init__(self, chat_peer, broadcast=post) - SenderGetter.__init__(self, from_id) - - if post and not from_id and chat_peer: - # If the message comes from a Channel, let the sender be it - self._sender_id = utils.get_peer_id(chat_peer) - - self._forward = None - - def _finish_init(self, client, entities, input_chat): - """ - Finishes the initialization of this message by setting - the client that sent the message and making use of the - known entities. - """ - self._client = client - cache = client._entity_cache - - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, entities, cache) - - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, entities, cache) - - if input_chat: # This has priority - self._input_chat = input_chat - - if self.via_bot_id: - self._via_bot, self._via_input_bot = utils._get_entity_pair( - self.via_bot_id, entities, cache) - - if self.fwd_from: - self._forward = Forward(self._client, self.fwd_from, entities) - - if self.action: - if isinstance(self.action, (types.MessageActionChatAddUser, - types.MessageActionChatCreate)): - self._action_entities = [entities.get(i) - for i in self.action.users] - elif isinstance(self.action, types.MessageActionChatDeleteUser): - self._action_entities = [entities.get(self.action.user_id)] - elif isinstance(self.action, types.MessageActionChatJoinedByLink): - self._action_entities = [entities.get(self.action.inviter_id)] - elif isinstance(self.action, types.MessageActionChatMigrateTo): - self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChannel(self.action.channel_id)))] - elif isinstance( - self.action, types.MessageActionChannelMigrateFrom): - self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChat(self.action.chat_id)))] - - # endregion Initialization - - # region Public Properties - - @property - def client(self): - """ - Returns the `TelegramClient ` - that *patched* this message. This will only be present if you - **use the friendly methods**, it won't be there if you invoke - raw API methods manually, in which case you should only access - members, not properties. - """ - return self._client - - @property - def text(self): - """ - The message text, formatted using the client's default - parse mode. Will be ``None`` for :tl:`MessageService`. - """ - if self._text is None and self._client: - if not self._client.parse_mode: - self._text = self.message - else: - self._text = self._client.parse_mode.unparse( - self.message, self.entities) - - return self._text - - @text.setter - def text(self, value): - self._text = value - if self._client and self._client.parse_mode: - self.message, self.entities = self._client.parse_mode.parse(value) - else: - self.message, self.entities = value, [] - - @property - def raw_text(self): - """ - The raw message text, ignoring any formatting. - Will be ``None`` for :tl:`MessageService`. - - Setting a value to this field will erase the - `entities`, unlike changing the `message` member. - """ - return self.message - - @raw_text.setter - def raw_text(self, value): - self.message = value - self.entities = [] - self._text = None - - @property - def is_reply(self): - """ - ``True`` if the message is a reply to some other message. - - Remember that you can access the ID of the message - this one is replying to through `reply_to_msg_id`, - and the `Message` object with `get_reply_message()`. - """ - return bool(self.reply_to_msg_id) - - @property - def forward(self): - """ - The `Forward ` - information if this message is a forwarded message. - """ - return self._forward - - @property - def buttons(self): - """ - Returns a list of lists of `MessageButton - `, - if any. - - Otherwise, it returns ``None``. - """ - if self._buttons is None and self.reply_markup: - if not self.input_chat: - return - try: - bot = self._needed_markup_bot() - except ValueError: - return - else: - self._set_buttons(self._input_chat, bot) - - return self._buttons - - async def get_buttons(self): - """ - Returns `buttons` when that property fails (this is rarely needed). - """ - if not self.buttons and self.reply_markup: - chat = await self.get_input_chat() - if not chat: - return - try: - bot = self._needed_markup_bot() - except ValueError: - await self._reload_message() - bot = self._needed_markup_bot() # TODO use via_input_bot - - self._set_buttons(chat, bot) - - return self._buttons - - @property - def button_count(self): - """ - Returns the total button count (sum of all `buttons` rows). - """ - if self._buttons_count is None: - if isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - self._buttons_count = sum( - len(row.buttons) for row in self.reply_markup.rows) - else: - self._buttons_count = 0 - - return self._buttons_count - - @property - def file(self): - """ - Returns a `File ` wrapping the - `photo` or `document` in this message. If the media type is different - (polls, games, none, etc.), this property will be ``None``. - - This instance lets you easily access other properties, such as - `file.id `, - `file.name `, - etc., without having to manually inspect the ``document.attributes``. - """ - if not self._file: - media = self.photo or self.document - if media: - self._file = File(media) - - return self._file - - @property - def photo(self): - """ - The :tl:`Photo` media in this message, if any. - - This will also return the photo for :tl:`MessageService` if its - action is :tl:`MessageActionChatEditPhoto`, or if the message has - a web preview with a photo. - """ - if isinstance(self.media, types.MessageMediaPhoto): - if isinstance(self.media.photo, types.Photo): - return self.media.photo - elif isinstance(self.action, types.MessageActionChatEditPhoto): - return self.action.photo - else: - web = self.web_preview - if web and isinstance(web.photo, types.Photo): - return web.photo - - @property - def document(self): - """ - The :tl:`Document` media in this message, if any. - """ - if isinstance(self.media, types.MessageMediaDocument): - if isinstance(self.media.document, types.Document): - return self.media.document - else: - web = self.web_preview - if web and isinstance(web.photo, types.Document): - return web.photo - - @property - def web_preview(self): - """ - The :tl:`WebPage` media in this message, if any. - """ - if isinstance(self.media, types.MessageMediaWebPage): - if isinstance(self.media.webpage, types.WebPage): - return self.media.webpage - - @property - def audio(self): - """ - The :tl:`Document` media in this message, if it's an audio file. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: not attr.voice) - - @property - def voice(self): - """ - The :tl:`Document` media in this message, if it's a voice note. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: attr.voice) - - @property - def video(self): - """ - The :tl:`Document` media in this message, if it's a video. - """ - return self._document_by_attribute(types.DocumentAttributeVideo) - - @property - def video_note(self): - """ - The :tl:`Document` media in this message, if it's a video note. - """ - return self._document_by_attribute(types.DocumentAttributeVideo, - lambda attr: attr.round_message) - - @property - def gif(self): - """ - The :tl:`Document` media in this message, if it's a "gif". - - "Gif" files by Telegram are normally ``.mp4`` video files without - sound, the so called "animated" media. However, it may be the actual - gif format if the file is too large. - """ - return self._document_by_attribute(types.DocumentAttributeAnimated) - - @property - def sticker(self): - """ - The :tl:`Document` media in this message, if it's a sticker. - """ - return self._document_by_attribute(types.DocumentAttributeSticker) - - @property - def contact(self): - """ - The :tl:`MessageMediaContact` in this message, if it's a contact. - """ - if isinstance(self.media, types.MessageMediaContact): - return self.media - - @property - def game(self): - """ - The :tl:`Game` media in this message, if it's a game. - """ - if isinstance(self.media, types.MessageMediaGame): - return self.media.game - - @property - def geo(self): - """ - The :tl:`GeoPoint` media in this message, if it has a location. - """ - if isinstance(self.media, (types.MessageMediaGeo, - types.MessageMediaGeoLive, - types.MessageMediaVenue)): - return self.media.geo - - @property - def invoice(self): - """ - The :tl:`MessageMediaInvoice` in this message, if it's an invoice. - """ - if isinstance(self.media, types.MessageMediaInvoice): - return self.media - - @property - def poll(self): - """ - The :tl:`MessageMediaPoll` in this message, if it's a poll. - """ - if isinstance(self.media, types.MessageMediaPoll): - return self.media - - @property - def venue(self): - """ - The :tl:`MessageMediaVenue` in this message, if it's a venue. - """ - if isinstance(self.media, types.MessageMediaVenue): - return self.media - - @property - def action_entities(self): - """ - Returns a list of entities that took part in this action. - - Possible cases for this are :tl:`MessageActionChatAddUser`, - :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, - :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo` - and :tl:`MessageActionChannelMigrateFrom`. - - If the action is neither of those, the result will be ``None``. - If some entities could not be retrieved, the list may contain - some ``None`` items in it. - """ - return self._action_entities - - @property - def via_bot(self): - """ - The bot :tl:`User` if the message was sent via said bot. - - This will only be present if `via_bot_id` is not ``None`` and - the entity is known. - """ - return self._via_bot - - @property - def via_input_bot(self): - """ - Returns the input variant of `via_bot`. - """ - return self._via_input_bot - - # endregion Public Properties - - # region Public Methods - - def get_entities_text(self, cls=None): - """ - Returns a list of ``(markup entity, inner text)`` - (like bold or italics). - - The markup entity is a :tl:`MessageEntity` that represents bold, - italics, etc., and the inner text is the ``str`` inside that markup - entity. - - For example: - - .. code-block:: python - - print(repr(message.text)) # shows: 'Hello **world**!' - - for ent, txt in message.get_entities_text(): - print(ent) # shows: MessageEntityBold(offset=6, length=5) - print(txt) # shows: world - - Args: - cls (`type`): - Returns entities matching this type only. For example, - the following will print the text for all ``code`` entities: - - >>> from telethon.tl.types import MessageEntityCode - >>> - >>> m = ... # get the message - >>> for _, inner_text in m.get_entities_text(MessageEntityCode): - >>> print(inner_text) - """ - ent = self.entities - if not ent: - return [] - - if cls: - ent = [c for c in ent if isinstance(c, cls)] - - texts = utils.get_inner_text(self.message, ent) - return list(zip(ent, texts)) - - async def get_reply_message(self): - """ - The `Message` that this message is replying to, or ``None``. - - The result will be cached after its first use. - """ - if self._reply_message is None and self._client: - if not self.reply_to_msg_id: - return None - - # Bots cannot access other bots' messages by their ID. - # However they can access them through replies... - self._reply_message = await self._client.get_messages( - await self.get_input_chat() if self.is_channel else None, - ids=types.InputMessageReplyTo(self.id) - ) - if not self._reply_message: - # ...unless the current message got deleted. - # - # If that's the case, give it a second chance accessing - # directly by its ID. - self._reply_message = await self._client.get_messages( - self._input_chat if self.is_channel else None, - ids=self.reply_to_msg_id - ) - - return self._reply_message - - async def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with ``entity`` already set. - """ - if self._client: - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with both ``entity`` and ``reply_to`` already set. - """ - if self._client: - kwargs['reply_to'] = self.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def forward_to(self, *args, **kwargs): - """ - Forwards the message. Shorthand for - `telethon.client.messages.MessageMethods.forward_messages` - with both ``messages`` and ``from_peer`` already set. - - If you need to forward more than one message at once, don't use - this `forward_to` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - """ - if self._client: - kwargs['messages'] = self.id - kwargs['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) - - async def edit(self, *args, **kwargs): - """ - Edits the message iff it's outgoing. Shorthand for - `telethon.client.messages.MessageMethods.edit_message` - with both ``entity`` and ``message`` already set. - - Returns ``None`` if the message was incoming, - or the edited `Message` otherwise. - - .. note:: - - This is different from `client.edit_message - ` - and **will respect** the previous state of the message. - For example, if the message didn't have a link preview, - the edit won't add one by default, and you should force - it by setting it to ``True`` if you want it. - - This is generally the most desired and convenient behaviour, - and will work for link previews and message buttons. - """ - if self.fwd_from or not self.out or not self._client: - return None # We assume self.out was patched for our chat - - if 'link_preview' not in kwargs: - kwargs['link_preview'] = bool(self.web_preview) - - if 'buttons' not in kwargs: - kwargs['buttons'] = self.reply_markup - - return await self._client.edit_message( - await self.get_input_chat(), self.id, - *args, **kwargs - ) - - async def delete(self, *args, **kwargs): - """ - Deletes the message. You're responsible for checking whether you - have the permission to do so, or to except the error otherwise. - Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - - If you need to delete more than one message at once, don't use - this `delete` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - """ - if self._client: - return await self._client.delete_messages( - await self.get_input_chat(), [self.id], - *args, **kwargs - ) - - async def download_media(self, *args, **kwargs): - """ - Downloads the media contained in the message, if any. Shorthand - for `telethon.client.downloads.DownloadMethods.download_media` - with the ``message`` already set. - """ - if self._client: - return await self._client.download_media(self, *args, **kwargs) - - async def click(self, i=None, j=None, - *, text=None, filter=None, data=None): - """ - Calls `button.click ` - on the specified button. - - Does nothing if the message has no buttons. - - Args: - i (`int`): - Clicks the i'th button (starting from the index 0). - Will ``raise IndexError`` if out of bounds. Example: - - >>> message = ... # get the message somehow - >>> # Clicking the 3rd button - >>> # [button1] [button2] - >>> # [ button3 ] - >>> # [button4] [button5] - >>> message.click(2) # index - - j (`int`): - Clicks the button at position (i, j), these being the - indices for the (row, column) respectively. Example: - - >>> # Clicking the 2nd button on the 1st row. - >>> # [button1] [button2] - >>> # [ button3 ] - >>> # [button4] [button5] - >>> message.click(0, 1) # (row, column) - - This is equivalent to ``message.buttons[0][1].click()``. - - text (`str` | `callable`): - Clicks the first button with the text "text". This may - also be a callable, like a ``re.compile(...).match``, - and the text will be passed to it. - - filter (`callable`): - Clicks the first button for which the callable - returns ``True``. The callable should accept a single - `MessageButton ` - argument. - - data (`bytes`): - This argument overrides the rest and will not search any - buttons. Instead, it will directly send the request to - behave as if it clicked a button with said data. Note - that if the message does not have this data, it will - ``raise DataInvalidError``. - - Example: - - .. code-block:: python - - # Click the first button - message.click(0) - - # Click some row/column - message.click(row, column) - - # Click by text - message.click(text='👍') - - # Click by data - message.click(data=b'payload') - """ - if not self._client: - return - - if data: - if not await self.get_input_chat(): - return None - - try: - return await self._client( - functions.messages.GetBotCallbackAnswerRequest( - peer=self._input_chat, - msg_id=self.id, - data=data - ) - ) - except errors.BotTimeout: - return None - - if sum(int(x is not None) for x in (i, text, filter)) >= 2: - raise ValueError('You can only set either of i, text or filter') - - if not await self.get_buttons(): - return # Accessing the property sets self._buttons[_flat] - - if text is not None: - if callable(text): - for button in self._buttons_flat: - if text(button.text): - return await button.click() - else: - for button in self._buttons_flat: - if button.text == text: - return await button.click() - return - - if filter is not None: - for button in self._buttons_flat: - if filter(button): - return await button.click() - return - - if i is None: - i = 0 - if j is None: - return await self._buttons_flat[i].click() - else: - return await self._buttons[i][j].click() - - async def mark_read(self): - """ - Marks the message as read. Shorthand for - `client.send_read_acknowledge() - ` - with both ``entity`` and ``message`` already set. - """ - await self._client.send_read_acknowledge( - await self.get_input_chat(), max_id=self.id) - - async def pin(self, *, notify=False): - """ - Pins the message. Shorthand for - `telethon.client.messages.MessageMethods.pin_message` - with both ``entity`` and ``message`` already set. - """ - await self._client.pin_message( - await self.get_input_chat(), self.id, notify=notify) - - # endregion Public Methods - - # region Private Methods - - async def _reload_message(self): - """ - Re-fetches this message to reload the sender and chat entities, - along with their input versions. - """ - if not self._client: - return - - try: - chat = await self.get_input_chat() if self.is_channel else None - msg = await self._client.get_messages(chat, ids=self.id) - except ValueError: - return # We may not have the input chat/get message failed - if not msg: - return # The message may be deleted and it will be None - - self._sender = msg._sender - self._input_sender = msg._input_sender - self._chat = msg._chat - self._input_chat = msg._input_chat - self._via_bot = msg._via_bot - self._via_input_bot = msg._via_input_bot - self._forward = msg._forward - self._action_entities = msg._action_entities - - async def _refetch_sender(self): - await self._reload_message() - - def _set_buttons(self, chat, bot): - """ - Helper methods to set the buttons given the input sender and chat. - """ - if self._client and isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - self._buttons = [[ - MessageButton(self._client, button, chat, bot, self.id) - for button in row.buttons - ] for row in self.reply_markup.rows] - self._buttons_flat = [x for row in self._buttons for x in row] - - def _needed_markup_bot(self): - """ - Returns the input peer of the bot that's needed for the reply markup. - - This is necessary for :tl:`KeyboardButtonSwitchInline` since we need - to know what bot we want to start. Raises ``ValueError`` if the bot - cannot be found but is needed. Returns ``None`` if it's not needed. - """ - if self._client and not isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - return None - - for row in self.reply_markup.rows: - for button in row.buttons: - if isinstance(button, types.KeyboardButtonSwitchInline): - if button.same_peer: - bot = self.input_sender - if not bot: - raise ValueError('No input sender') - else: - try: - return self._client._entity_cache[self.via_bot_id] - except KeyError: - raise ValueError('No input sender') from None - - def _document_by_attribute(self, kind, condition=None): - """ - Helper method to return the document only if it has an attribute - that's an instance of the given kind, and passes the condition. - """ - doc = self.document - if doc: - for attr in doc.attributes: - if isinstance(attr, kind): - if not condition or condition(attr): - return doc - return None - - # endregion Private Methods diff --git a/telethon/tl/custom/messagebutton.py b/telethon/tl/custom/messagebutton.py index 9d18804c..e69de29b 100644 --- a/telethon/tl/custom/messagebutton.py +++ b/telethon/tl/custom/messagebutton.py @@ -1,103 +0,0 @@ -from .. import types, functions -from ...errors import BotTimeout -import webbrowser - - -class MessageButton: - """ - .. note:: - - `Message.buttons ` - are instances of this type. If you want to **define** a reply - markup for e.g. sending messages, refer to `Button - ` instead. - - Custom class that encapsulates a message button providing - an abstraction to easily access some commonly needed features - (such as clicking the button itself). - - Attributes: - - button (:tl:`KeyboardButton`): - The original :tl:`KeyboardButton` object. - """ - def __init__(self, client, original, chat, bot, msg_id): - self.button = original - self._bot = bot - self._chat = chat - self._msg_id = msg_id - self._client = client - - @property - def client(self): - """ - Returns the `telethon.client.telegramclient.TelegramClient` - instance that created this instance. - """ - return self._client - - @property - def text(self): - """The text string of the button.""" - return self.button.text - - @property - def data(self): - """The ``bytes`` data for :tl:`KeyboardButtonCallback` objects.""" - if isinstance(self.button, types.KeyboardButtonCallback): - return self.button.data - - @property - def inline_query(self): - """The query ``str`` for :tl:`KeyboardButtonSwitchInline` objects.""" - if isinstance(self.button, types.KeyboardButtonSwitchInline): - return self.button.query - - @property - def url(self): - """The url ``str`` for :tl:`KeyboardButtonUrl` objects.""" - if isinstance(self.button, types.KeyboardButtonUrl): - return self.button.url - - async def click(self): - """ - Emulates the behaviour of clicking this button. - - If it's a normal :tl:`KeyboardButton` with text, a message will be - sent, and the sent `Message ` returned. - - If it's an inline :tl:`KeyboardButtonCallback` with text and data, - it will be "clicked" and the :tl:`BotCallbackAnswer` returned. - - If it's an inline :tl:`KeyboardButtonSwitchInline` button, the - :tl:`StartBotRequest` will be invoked and the resulting updates - returned. - - If it's a :tl:`KeyboardButtonUrl`, the URL of the button will - be passed to ``webbrowser.open`` and return ``True`` on success. - """ - if isinstance(self.button, types.KeyboardButton): - return await self._client.send_message( - self._chat, self.button.text, reply_to=self._msg_id) - elif isinstance(self.button, types.KeyboardButtonCallback): - req = functions.messages.GetBotCallbackAnswerRequest( - peer=self._chat, msg_id=self._msg_id, data=self.button.data - ) - try: - return await self._client(req) - except BotTimeout: - return None - elif isinstance(self.button, types.KeyboardButtonSwitchInline): - return await self._client(functions.messages.StartBotRequest( - bot=self._bot, peer=self._chat, start_param=self.button.query - )) - elif isinstance(self.button, types.KeyboardButtonUrl): - return webbrowser.open(self.button.url) - elif isinstance(self.button, types.KeyboardButtonGame): - req = functions.messages.GetBotCallbackAnswerRequest( - peer=self._chat, msg_id=self._msg_id, game=True - ) - try: - return await self._client(req) - except BotTimeout: - return None diff --git a/telethon/tl/custom/sendergetter.py b/telethon/tl/custom/sendergetter.py index 37ea39fa..e69de29b 100644 --- a/telethon/tl/custom/sendergetter.py +++ b/telethon/tl/custom/sendergetter.py @@ -1,97 +0,0 @@ -import abc - - -class SenderGetter(abc.ABC): - """ - Helper base class that introduces the `sender`, `input_sender` - and `sender_id` properties and `get_sender` and `get_input_sender` - methods. - """ - def __init__(self, sender_id=None, *, sender=None, input_sender=None): - self._sender_id = sender_id - self._sender = sender - self._input_sender = input_sender - self._client = None - - @property - def sender(self): - """ - Returns the :tl:`User` or :tl:`Channel` that sent this object. - It may be ``None`` if Telegram didn't send the sender. - - If you only need the ID, use `sender_id` instead. - - If you need to call a method which needs - this chat, use `input_sender` instead. - - If you're using `telethon.events`, use `get_sender()` instead. - """ - return self._sender - - async def get_sender(self): - """ - Returns `sender`, but will make an API call to find the - sender unless it's already cached. - - If you only need the ID, use `sender_id` instead. - - If you need to call a method which needs - this sender, use `get_input_sender()` instead. - """ - # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. - # It's a flag that will be set if only minimal information is - # available (such as display name, but username may be missing), - # in which case we want to force fetch the entire thing because - # the user explicitly called a method. If the user is okay with - # cached information, they may use the property instead. - if (self._sender is None or self._sender.min) \ - and await self.get_input_sender(): - try: - self._sender =\ - await self._client.get_entity(self._input_sender) - except ValueError: - await self._refetch_sender() - return self._sender - - @property - def input_sender(self): - """ - This :tl:`InputPeer` is the input version of the user/channel who - sent the message. Similarly to `input_chat - `, this doesn't - have things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - if self._input_sender is None and self._sender_id and self._client: - try: - self._input_sender = \ - self._client._entity_cache[self._sender_id] - except KeyError: - pass - return self._input_sender - - async def get_input_sender(self): - """ - Returns `input_sender`, but will make an API call to find the - input sender unless it's already cached. - """ - if self.input_sender is None and self._sender_id and self._client: - await self._refetch_sender() - return self._input_sender - - @property - def sender_id(self): - """ - Returns the marked sender integer ID, if present. - - If there is a sender in the object, `sender_id` will *always* be set, - which is why you should use it instead of `sender.id `. - """ - return self._sender_id - - async def _refetch_sender(self): - """ - Re-fetches sender information through other means. - """ diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py index 49a11d3b..e69de29b 100644 --- a/telethon/tl/tlobject.py +++ b/telethon/tl/tlobject.py @@ -1,191 +0,0 @@ -import base64 -import json -import struct -from datetime import datetime, date, timedelta - - -def _json_default(value): - if isinstance(value, bytes): - return base64.b64encode(value).decode('ascii') - elif isinstance(value, datetime): - return value.isoformat() - else: - return repr(value) - - -class TLObject: - CONSTRUCTOR_ID = None - SUBCLASS_OF_ID = None - - @staticmethod - def pretty_format(obj, indent=None): - """ - Pretty formats the given object as a string which is returned. - If indent is None, a single line will be returned. - """ - if indent is None: - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - return '{}({})'.format(obj.get('_', 'dict'), ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() if k != '_' - )) - elif isinstance(obj, str) or isinstance(obj, bytes): - return repr(obj) - elif hasattr(obj, '__iter__'): - return '[{}]'.format( - ', '.join(TLObject.pretty_format(x) for x in obj) - ) - else: - return repr(obj) - else: - result = [] - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - result.append(obj.get('_', 'dict')) - result.append('(') - if obj: - result.append('\n') - indent += 1 - for k, v in obj.items(): - if k == '_': - continue - result.append('\t' * indent) - result.append(k) - result.append('=') - result.append(TLObject.pretty_format(v, indent)) - result.append(',\n') - result.pop() # last ',\n' - indent -= 1 - result.append('\n') - result.append('\t' * indent) - result.append(')') - - elif isinstance(obj, str) or isinstance(obj, bytes): - result.append(repr(obj)) - - elif hasattr(obj, '__iter__'): - result.append('[\n') - indent += 1 - for x in obj: - result.append('\t' * indent) - result.append(TLObject.pretty_format(x, indent)) - result.append(',\n') - indent -= 1 - result.append('\t' * indent) - result.append(']') - - else: - result.append(repr(obj)) - - return ''.join(result) - - @staticmethod - def serialize_bytes(data): - """Write bytes by using Telegram guidelines""" - if not isinstance(data, bytes): - if isinstance(data, str): - data = data.encode('utf-8') - else: - raise TypeError( - 'bytes or str expected, not {}'.format(type(data))) - - r = [] - if len(data) < 254: - padding = (len(data) + 1) % 4 - if padding != 0: - padding = 4 - padding - - r.append(bytes([len(data)])) - r.append(data) - - else: - padding = len(data) % 4 - if padding != 0: - padding = 4 - padding - - r.append(bytes([ - 254, - len(data) % 256, - (len(data) >> 8) % 256, - (len(data) >> 16) % 256 - ])) - r.append(data) - - r.append(bytes(padding)) - return b''.join(r) - - @staticmethod - def serialize_datetime(dt): - if not dt: - return b'\0\0\0\0' - - if isinstance(dt, datetime): - dt = int(dt.timestamp()) - elif isinstance(dt, date): - dt = int(datetime(dt.year, dt.month, dt.day).timestamp()) - elif isinstance(dt, float): - dt = int(dt) - elif isinstance(dt, timedelta): - # Timezones are tricky. datetime.now() + ... timestamp() works - dt = int((datetime.now() + dt).timestamp()) - - if isinstance(dt, int): - return struct.pack(' 'y!'. - - :param text: the original text. - :param entities: the entity or entities that must be matched. - :return: a single result or a list of the text surrounded by the entities. - """ - text = add_surrogate(text) - result = [] - for e in entities: - start = e.offset - end = e.offset + e.length - result.append(del_surrogate(text[start:end])) - - return result - - -def get_peer(peer): - try: - if isinstance(peer, int): - pid, cls = resolve_id(peer) - return cls(pid) - elif peer.SUBCLASS_OF_ID == 0x2d45687: - return peer - elif isinstance(peer, ( - types.contacts.ResolvedPeer, types.InputNotifyPeer, - types.TopPeer)): - return peer.peer - elif isinstance(peer, types.ChannelFull): - return types.PeerChannel(peer.id) - elif isinstance(peer, types.DialogPeer): - return peer.peer - - if peer.SUBCLASS_OF_ID in (0x7d7c6f86, 0xd9c7fc18): - # ChatParticipant, ChannelParticipant - return types.PeerUser(peer.user_id) - - peer = get_input_peer(peer, allow_self=False, check_hash=False) - if isinstance(peer, types.InputPeerUser): - return types.PeerUser(peer.user_id) - elif isinstance(peer, types.InputPeerChat): - return types.PeerChat(peer.chat_id) - elif isinstance(peer, types.InputPeerChannel): - return types.PeerChannel(peer.channel_id) - except (AttributeError, TypeError): - pass - _raise_cast_fail(peer, 'Peer') - - -def get_peer_id(peer, add_mark=True): - """ - Convert the given peer into its marked ID by default. - - This "mark" comes from the "bot api" format, and with it the peer type - can be identified back. User ID is left unmodified, chat ID is negated, - and channel ID is prefixed with -100: - - * ``user_id`` - * ``-chat_id`` - * ``-100channel_id`` - - The original ID and the peer type class can be returned with - a call to :meth:`resolve_id(marked_id)`. - """ - # First we assert it's a Peer TLObject, or early return for integers - if isinstance(peer, int): - return peer if add_mark else resolve_id(peer)[0] - - # Tell the user to use their client to resolve InputPeerSelf if we got one - if isinstance(peer, types.InputPeerSelf): - _raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)') - - try: - peer = get_peer(peer) - except TypeError: - _raise_cast_fail(peer, 'int') - - if isinstance(peer, types.PeerUser): - return peer.user_id - elif isinstance(peer, types.PeerChat): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.chat_id <= 0x7fffffff): - peer.chat_id = resolve_id(peer.chat_id)[0] - - return -peer.chat_id if add_mark else peer.chat_id - else: # if isinstance(peer, types.PeerChannel): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.channel_id <= 0x7fffffff): - peer.channel_id = resolve_id(peer.channel_id)[0] - - if not add_mark: - return peer.channel_id - - # Concat -100 through math tricks, .to_supergroup() on - # Madeline IDs will be strictly positive -> log works. - return -(peer.channel_id + pow( - 10, math.floor(math.log10(peer.channel_id) + 3))) - - -def resolve_id(marked_id): - """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" - if marked_id >= 0: - return marked_id, types.PeerUser - - # There have been report of chat IDs being 10000xyz, which means their - # marked version is -10000xyz, which in turn looks like a channel but - # it becomes 00xyz (= xyz). Hence, we must assert that there are only - # two zeroes. - m = re.match(r'-100([^0]\d*)', str(marked_id)) - if m: - return int(m.group(1)), types.PeerChannel - - return -marked_id, types.PeerChat - - -def _rle_decode(data): - """ - Decodes run-length-encoded `data`. - """ - if not data: - return data - - new = b'' - last = b'' - for cur in data: - if last == b'\0': - new += last * cur - last = b'' - else: - new += last - last = bytes([cur]) - - return new + last - - -def _rle_encode(string): - new = b'' - count = 0 - for cur in string: - if not cur: - count += 1 - else: - if count: - new += b'\0' + bytes([count]) - count = 0 - - new += bytes([cur]) - return new - - -def _decode_telegram_base64(string): - """ - Decodes an url-safe base64-encoded string into its bytes - by first adding the stripped necessary padding characters. - - This is the way Telegram shares binary data as strings, - such as Bot API-style file IDs or invite links. - - Returns ``None`` if the input string was not valid. - """ - try: - return base64.urlsafe_b64decode(string + '=' * (len(string) % 4)) - except (binascii.Error, ValueError, TypeError): - return None # not valid base64, not valid ascii, not a string - - -def _encode_telegram_base64(string): - """ - Inverse for `_decode_telegram_base64`. - """ - try: - return base64.urlsafe_b64encode(string).rstrip(b'=').decode('ascii') - except (binascii.Error, ValueError, TypeError): - return None # not valid base64, not valid ascii, not a string - - -def resolve_bot_file_id(file_id): - """ - Given a Bot API-style `file_id `, - returns the media it represents. If the `file_id ` - is not valid, ``None`` is returned instead. - - Note that the `file_id ` does not have information - such as image dimensions or file size, so these will be zero if present. - - For thumbnails, the photo ID and hash will always be zero. - """ - data = _rle_decode(_decode_telegram_base64(file_id)) - if not data or data[-1] != 2: - return None - - data = data[:-1] - if len(data) == 24: - file_type, dc_id, media_id, access_hash = struct.unpack('LLQ', payload) - except (struct.error, TypeError): - return None, None, None - - -def resolve_inline_message_id(inline_msg_id): - """ - Resolves an inline message ID. Returns a tuple of - ``(message id, peer, dc id, access hash)`` - - The ``peer`` may either be a :tl:`PeerUser` referencing - the user who sent the message via the bot in a private - conversation or small group chat, or a :tl:`PeerChannel` - if the message was sent in a channel. - - The ``access_hash`` does not have any use yet. - """ - try: - dc_id, message_id, pid, access_hash = \ - struct.unpack('> bit_shift) & 0b00011111 - - byte_index, bit_shift = divmod(value_count - 1, 8) - if byte_index == len(waveform) - 1: - value = waveform[byte_index] - else: - value = struct.unpack('> bit_shift) & 0b00011111 - return bytes(result) - - -class AsyncClassWrapper: - def __init__(self, wrapped): - self.wrapped = wrapped - - def __getattr__(self, item): - w = getattr(self.wrapped, item) - async def wrapper(*args, **kwargs): - val = w(*args, **kwargs) - return await val if inspect.isawaitable(val) else val - - if callable(w): - return wrapper - else: - return w - - -def stripped_photo_to_jpg(stripped): - """ - Adds the JPG header and footer to a stripped image. - - Ported from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225 - """ - # NOTE: Changes here should update _stripped_real_length - if len(stripped) < 3 or stripped[0] != 1: - return stripped - - header = bytearray(b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00(\x1c\x1e#\x1e\x19(#!#-+(0 diff --git a/telethon_examples/README.md b/telethon_examples/README.md index 29af0e4d..e69de29b 100644 --- a/telethon_examples/README.md +++ b/telethon_examples/README.md @@ -1,149 +0,0 @@ -# Examples - -This folder contains several single-file examples using [Telethon]. - -## Requisites - -You should have the `telethon` library installed with `pip`. -Run `python3 -m pip install --upgrade telethon --user` if you don't -have it installed yet (this is the most portable way to install it). - -The scripts will ask you for your API ID, hash, etc. through standard input. -You can also define the following environment variables to avoid doing so: - -* `TG_API_ID`, this is your API ID from https://my.telegram.org. -* `TG_API_HASH`, this is your API hash from https://my.telegram.org. -* `TG_TOKEN`, this is your bot token from [@BotFather] for bot examples. -* `TG_SESSION`, this is the name of the `*.session` file to use. - -## Downloading Examples - -You may download all and run any example by typing in a terminal: -```sh -git clone https://github.com/LonamiWebs/Telethon.git -cd Telethon -cd telethon_examples -python3 gui.py -``` - -You can also right-click the title of any example and use "Save Link As…" to -download only a particular example. - -All examples are licensed under the [CC0 License], so you can use -them as the base for your own code without worrying about copyright. - -## Available Examples - -### [`print_updates.py`] - -* Usable as: **user and bot**. -* Difficulty: **easy**. - -Trivial example that just prints all the updates Telegram originally -sends. Your terminal should support UTF-8, or Python may fail to print -some characters on screen. - -### [`print_messages.py`] - -* Usable as: **user and bot**. -* Difficulty: **easy**. - -This example uses the different `@client.on` syntax to register event -handlers, and uses the `pattern=` variable to filter only some messages. - -There are a lot other things you can do, but you should refer to the -documentation of [`events.NewMessage`] since this is only a simple example. - -### [`replier.py`] - -* Usable as: **user and bot**. -* Difficulty: **easy**. - -This example showcases a third way to add event handlers (using decorators -but without the client; you should use the one you prefer) and will also -reply to some messages with different reactions, or to your commands. - -It also shows how to enable `logging`, which you should always do, but was -not really needed for the previous two trivial examples. - -### [`assistant.py`] - -* Usable as a: **bot**. -* Difficulty: **medium**. - -This example is the actual bot account [@TelethonianBot] running in the -[official Telethon's chat] to help people out. The file is a bit big and -assumes some [`asyncio`] knowledge, but otherwise is easy to follow. - -In addition, it has optional plugins, which may be useful for your own code. -The plugins can be found at https://github.com/Lonami/TelethonianBotExt and -should be cloned into a `plugins` folder next to `assistant.py` for them to -work. - -### [`interactive_telegram_client.py`] - -* Usable as: **user**. -* Difficulty: **medium**. - -Interactive terminal client that you can use to list your dialogs, -send messages, delete them, and download media. The code is a bit -long which may make it harder to follow, and requires saving some -state in order for downloads to work later. - -### [`quart_login.py`] - -* Usable as: **user**. -* Difficulty: **medium**. - -Web-based application using [Quart](https://pgjones.gitlab.io/quart/index.html) -(an `asyncio` alternative to [Flask](http://flask.pocoo.org/)) and Telethon -together. - -The example should work as a base for Quart applications *with a single -global client*, and it should be easy to adapt for multiple clients by -following the comments in the code. - -It showcases how to login manually (ask for phone, code, and login), -and once the user is logged in, some messages and photos will be shown -in the page. - -There is nothing special about Quart. It was chosen because it's a -drop-in replacement for Flask, the most popular option for web-apps. -You can use any `asyncio` library with Telethon just as well, -like [Sanic](https://sanic.readthedocs.io/en/latest/index.html) or -[aiohttp](https://docs.aiohttp.org/en/stable/). You can even use Flask, -if you learn how to use `threading` and `asyncio` together. - -### [`gui.py`] - -* Usable as: **user and bot**. -* Difficulty: **high**. - -This is a simple GUI written with [`tkinter`] which becomes more complicated -when there's a need to use [`asyncio`] (although it's only a bit of additional -setup). The code to deal with the interface and the commands the GUI supports -also complicate the code further and require knowledge and careful reading. - -This example is the actual bot account [@TelethonianBot] running in the -[official Telethon's chat] to help people out. The file is a bit big and -assumes some [`asyncio`] knowledge, but otherwise is easy to follow. - -![Screenshot of the tkinter GUI][tkinter GUI] - - -[Telethon]: https://github.com/LonamiWebs/Telethon -[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE -[@BotFather]: https://t.me/BotFather -[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/assistant.py -[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/quart_login.py -[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/gui.py -[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/interactive_telegram_client.py -[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_messages.py -[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_updates.py -[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/replier.py -[@TelethonianBot]: https://t.me/TelethonianBot -[official Telethon's chat]: https://t.me/TelethonChat -[`asyncio`]: https://docs.python.org/3/library/asyncio.html -[`tkinter`]: https://docs.python.org/3/library/tkinter.html -[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/screenshot-gui.jpg -[`events.NewMessage`]: https://docs.telethon.dev/en/latest/modules/events.html#telethon.events.newmessage.NewMessage diff --git a/telethon_examples/assistant.py b/telethon_examples/assistant.py index 51597cc9..e69de29b 100644 --- a/telethon_examples/assistant.py +++ b/telethon_examples/assistant.py @@ -1,395 +0,0 @@ -import asyncio -import html -import logging -import os -import re -import sys -import time -import urllib.parse - -from telethon import TelegramClient, events, types, custom, utils, errors - -logging.basicConfig(level=logging.WARNING) -logging.getLogger('asyncio').setLevel(logging.ERROR) - -try: - import aiohttp -except ImportError: - aiohttp = None - logging.warning('aiohttp module not available; #haste command disabled') - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) -API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') -TOKEN = get_env('TG_TOKEN', 'Enter the bot token: ') -NAME = TOKEN.split(':')[0] -bot = TelegramClient(NAME, API_ID, API_HASH) - - -# ============================== Constants ============================== -WELCOME = { - -1001109500936: - 'Hi and welcome to the group. Before asking any questions, **please** ' - 'read [the docs](https://docs.telethon.dev/). Make sure you are ' - 'using the latest version with `pip3 install -U telethon`, since most ' - 'problems have already been fixed in newer versions.', - - -1001200633650: - 'Welcome to the off-topic group. Feel free to talk, ask or test anything ' - 'here, politely. Check the description if you need to test more spammy ' - '"features" of your or other people\'s bots (sed commands too).' -} - -READ_FULL = ( - 'Please read [Accessing the Full API](https://docs.telethon.dev' - '/en/latest/concepts/full-api.html)' -) - -SEARCH = ( - 'Remember [search is your friend]' - '(https://tl.telethon.dev/?q={}&redirect=no)' -) - -DOCS = 'TL Reference for [{}](https://tl.telethon.dev/?q={})' -RTD = '[Read The Docs!](https://docs.telethon.dev)' -RTFD = '[Read The F* Docs!](https://docs.telethon.dev)' -UPDATES = ( - 'Check out [Working with Updates](https://docs.telethon.dev' - '/en/latest/basic/updates.html) in the documentation.' -) - -SPAM = ( - "Telethon is free software. That means using it is a right: you are " - "free to use it for absolutely any purpose whatsoever. However, help " - "and support with using it is a privilege. If you misbehave or want " - "to do bad things, nobody is obligated to help you and you're not " - "welcome here." -) - -OFFTOPIC = { - -1001109500936: - 'That is not related to Telethon. ' - 'You may continue the conversation in @TelethonOffTopic', - -1001200633650: - 'That seems to be related to Telethon. Try asking in @TelethonChat' -} - -ASK = ( - "Hey, that's not how you ask a question! If you want helpful advice " - "(or any response at all) [read this first](https://stackoverflow.com" - "/help/how-to-ask) and then ask again. If you have the time, [How To " - "Ask Questions The Smart Way](catb.org/~esr/faqs/smart-questions.html)" - " is another wonderful resource worth reading." -) - -LOGGING = ''' -**Please enable logging:** -```import logging -logging.basicConfig(level=logging.WARNING)``` - -If you need more information, use `logging.DEBUG` instead. -''' - -ALREADY_FIXED = ( - "This issue has already been fixed, but it's not yet available in PyPi. " - "You can upgrade now with `pip3 install -U https://github.com/LonamiWebs" - "/Telethon/archive/master.zip`." -) - -GOOD_RESOURCES = ( - "Some good resources to learn Python:\n" - "• [Official Docs](https://docs.python.org/3/tutorial/index.html).\n" - "• [Dive Into Python 3](https://rawcdn.githack.com/diveintomark/" - "diveintopython3/master/table-of-contents.html).\n" - "• [Learn Python](https://www.learnpython.org/).\n" - "• [Project Python](http://projectpython.net/).\n" - "• [Computer Science Circles](https://cscircles.cemc.uwaterloo.ca/).\n" - "• [MIT OpenCourse](https://ocw.mit.edu/courses/electrical-engineering-" - "and-computer-science/6-0001-introduction-to-computer-science-and-progr" - "amming-in-python-fall-2016/).\n" - "• [Hitchhiker’s Guide to Python](https://docs.python-guide.org/).\n" - "• The @PythonRes Telegram Channel.\n" - "• Corey Schafer videos for [beginners](https://www.youtube.com/watch?v=" - "YYXdXT2l-Gg&list=PL-osiE80TeTskrapNbzXhwoFUiLCjGgY7) and in [general]" - "(https://www.youtube.com/watch?v=YYXdXT2l-Gg&list=PL-osiE80TeTt2d9bfV" - "yTiXJA-UTHn6WwU)." -) - -LEARN_PYTHON = ( - "That issue is no longer related with Telethon. You should learn more " - "Python before trying again. " + GOOD_RESOURCES -) - -# ============================== Constants ============================== -# ============================== Welcome ============================== -last_welcome = {} - - -@bot.on(events.ChatAction) -async def handler(event): - if event.user_joined: - if event.chat_id in last_welcome: - try: - await last_welcome[event.chat_id].delete() - except errors.MessageDeleteForbiddenError: - # We believe this happens when trying to delete old messages - pass - - last_welcome[event.chat_id] = await event.reply(WELCOME[event.chat_id]) - - -# ============================== Welcome ============================== -# ============================== Commands ============================== - - -@bot.on(events.NewMessage(pattern='#ping', forwards=False)) -async def handler(event): - s = time.time() - message = await event.reply('Pong!') - d = time.time() - s - await message.edit(f'Pong! __(reply took {d:.2f}s)__') - await asyncio.sleep(5) - await asyncio.wait([event.delete(), message.delete()]) - - -@bot.on(events.NewMessage(pattern='#full', forwards=False)) -async def handler(event): - """#full: Advises to read "Accessing the full API" in the docs.""" - await asyncio.wait([ - event.delete(), - event.respond(READ_FULL, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='#search (.+)', forwards=False)) -async def handler(event): - """#search query: Searches for "query" in the method reference.""" - query = urllib.parse.quote(event.pattern_match.group(1)) - await asyncio.wait([ - event.delete(), - event.respond(SEARCH.format(query), reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#(?:docs|ref) (.+)', forwards=False)) -async def handler(event): - """#docs or #ref query: Like #search but shows the query.""" - q1 = event.pattern_match.group(1) - q2 = urllib.parse.quote(q1) - await asyncio.wait([ - event.delete(), - event.respond(DOCS.format(q1, q2), reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='#rt(f)?d', forwards=False)) -async def handler(event): - """#rtd: Tells the user to please read the docs.""" - rtd = RTFD if event.pattern_match.group(1) else RTD - await asyncio.wait([ - event.delete(), - event.respond(rtd, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='#(updates|events?)', forwards=False)) -async def handler(event): - """#updates: Advices the user to read "Working with Updates".""" - await asyncio.wait([ - event.delete(), - event.respond(UPDATES, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#(ask|question)', forwards=False)) -async def handler(event): - """#ask or #question: Advices the user to ask a better question.""" - await asyncio.wait([ - event.delete(), - event.respond( - ASK, reply_to=event.reply_to_msg_id, link_preview=False) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#spam(mer|ming)?', forwards=False)) -async def handler(event): - """#spam, #spammer, #spamming: Informs spammers that they are not welcome here.""" - await asyncio.wait([ - event.delete(), - event.respond(SPAM, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#(ot|offtopic)', forwards=False)) -async def handler(event): - """#ot, #offtopic: Tells the user to move to @TelethonOffTopic.""" - await asyncio.wait([ - event.delete(), - event.respond(OFFTOPIC[event.chat_id], reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#log(s|ging)?', forwards=False)) -async def handler(event): - """#log, #logs or #logging: Explains how to enable logging.""" - await asyncio.wait([ - event.delete(), - event.respond(LOGGING, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#master', forwards=False)) -async def handler(event): - """#master: The bug has been fixed in the `master` branch.""" - await asyncio.wait([ - event.delete(), - event.respond(ALREADY_FIXED, reply_to=event.reply_to_msg_id) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#(learn|python)', forwards=False)) -async def handler(event): - """#learn or #python: Tells the user to learn some Python first.""" - await asyncio.wait([ - event.delete(), - event.respond( - LEARN_PYTHON, reply_to=event.reply_to_msg_id, link_preview=False) - ]) - - -@bot.on(events.NewMessage(pattern='(?i)#(list|help)', forwards=False)) -async def handler(event): - await event.delete() - text = 'Available commands:\n' - for callback, handler in bot.list_event_handlers(): - if isinstance(handler, events.NewMessage) and callback.__doc__: - text += f'\n{callback.__doc__.strip()}' - text += '\n\nYou can suggest new commands [here](https://docs.google.com/'\ - 'spreadsheets/d/12yWwixUu_vB426_toLBAiajXxYKvR2J1DD6yZtQz9l4/edit).' - - message = await event.respond(text, link_preview=False) - await asyncio.sleep(1 * text.count(' ')) # Sleep ~1 second per word - await message.delete() - - -if aiohttp: - @bot.on(events.NewMessage(pattern='(?i)#[hp]aste(bin)?', forwards=False)) - async def handler(event): - """ - #haste: Replaces the message you reply to with a hastebin link. - """ - await event.delete() - if not event.reply_to_msg_id: - return - - msg = await event.get_reply_message() - if len(msg.raw_text or '') < 200: - return - - sent = await event.respond( - 'Uploading paste…', reply_to=msg.reply_to_msg_id) - - name = html.escape( - utils.get_display_name(await msg.get_sender()) or 'A user') - - text = msg.raw_text - code = '' - for _, string in msg.get_entities_text(( - types.MessageEntityCode, types.MessageEntityPre)): - code += f'{string}\n' - text = text.replace(string, '') - - code = code.rstrip() - if code: - text = re.sub(r'\s+', ' ', text) - else: - code = msg.raw_text - text = '' - - async with aiohttp.ClientSession() as session: - async with session.post('https://hastebin.com/documents', - data=code.encode('utf-8')) as resp: - if resp.status >= 300: - await sent.edit("Hastebin seems to be down… ( ^^')") - return - - haste = (await resp.json())['key'] - - await asyncio.wait([ - msg.delete(), - sent.edit(f'{name} ' - f'said: {text} hastebin.com/{haste}.py' - .replace(' ', ' '), parse_mode='html') - ]) - - -# ============================== Commands ============================== -# ============================== Inline ============================== - - -@bot.on(events.InlineQuery) -async def handler(event): - builder = event.builder - result = None - query = event.text.lower() - if query == 'ping': - result = builder.article('Pong!', text='This bot works inline') - elif query == 'group': - result = builder.article( - 'Move to the right group!', - text='Try moving to the [right group](t.me/TelethonChat)', - buttons=custom.Button.url('Join the group!', 't.me/TelethonChat'), - link_preview=False - ) - elif query in ('python', 'learn'): - result = builder.article( - 'Resources to Learn Python', - text=GOOD_RESOURCES, - link_preview=False - ) - - # NOTE: You should always answer, but we want plugins to be able to answer - # too (and we can only answer once), so we don't always answer here. - if result: - await event.answer([result]) - - -# ============================== Inline ============================== - -bot.start(bot_token=TOKEN) - -# NOTE: This example has optional "plugins", which you can get by running: -# -# git clone https://github.com/Lonami/TelethonianBotExt plugins -# -# Into the same folder (so you would have `assistant.py` next to -# the now downloaded `plugins/` folder). We try importing them so -# that the example runs fine without them, but optionally load them. -try: - # Standalone script assistant.py with folder plugins/ - import plugins - plugins.init(bot) -except ImportError: - try: - # Running as a module with `python -m assistant` and structure: - # assistant/ - # __main__.py (this file) - # plugins/ (cloned) - from . import plugins - plugins.init(bot) - except ImportError: - plugins = None - -bot.run_until_disconnected() diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py index 949d1eb9..e69de29b 100644 --- a/telethon_examples/gui.py +++ b/telethon_examples/gui.py @@ -1,378 +0,0 @@ -import asyncio -import collections -import functools -import inspect -import os -import re -import sys -import time -import tkinter -import tkinter.constants -import tkinter.scrolledtext -import tkinter.ttk - -from telethon import TelegramClient, events, utils - -# Some configuration for the app -TITLE = 'Telethon GUI' -SIZE = '640x280' -REPLY = re.compile(r'\.r\s*(\d+)\s*(.+)', re.IGNORECASE) -DELETE = re.compile(r'\.d\s*(\d+)', re.IGNORECASE) -EDIT = re.compile(r'\.s(.+?[^\\])/(.*)', re.IGNORECASE) - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -# Session name, API ID and hash to use; loaded from environmental variables -SESSION = os.environ.get('TG_SESSION', 'gui') -API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) -API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - - -def sanitize_str(string): - return ''.join(x if ord(x) <= 0xffff else - '{{{:x}ū}}'.format(ord(x)) for x in string) - - -def callback(func): - """ - This decorator turns `func` into a callback for Tkinter - to be able to use, even if `func` is an awaitable coroutine. - """ - @functools.wraps(func) - def wrapped(*args, **kwargs): - result = func(*args, **kwargs) - if inspect.iscoroutine(result): - aio_loop.create_task(result) - - return wrapped - - -def allow_copy(widget): - """ - This helper makes `widget` readonly but allows copying with ``Ctrl+C``. - """ - widget.bind('', lambda e: None) - widget.bind('', lambda e: "break") - - -class App(tkinter.Tk): - """ - Our main GUI application; we subclass `tkinter.Tk` - so the `self` instance can be the root widget. - - One must be careful when assigning members or - defining methods since those may interfer with - the root widget. - - You may prefer to have ``App.root = tkinter.Tk()`` - and create widgets with ``self.root`` as parent. - """ - def __init__(self, client, *args, **kwargs): - super().__init__(*args, **kwargs) - self.cl = client - self.me = None - - self.title(TITLE) - self.geometry(SIZE) - - # Signing in row; the entry supports phone and bot token - self.sign_in_label = tkinter.Label(self, text='Loading...') - self.sign_in_label.grid(row=0, column=0) - self.sign_in_entry = tkinter.Entry(self) - self.sign_in_entry.grid(row=0, column=1, sticky=tkinter.EW) - self.sign_in_entry.bind('', self.sign_in) - self.sign_in_button = tkinter.Button(self, text='...', - command=self.sign_in) - self.sign_in_button.grid(row=0, column=2) - self.code = None - - # The chat where to send and show messages from - tkinter.Label(self, text='Target chat:').grid(row=1, column=0) - self.chat = tkinter.Entry(self) - self.chat.grid(row=1, column=1, columnspan=2, sticky=tkinter.EW) - self.columnconfigure(1, weight=1) - self.chat.bind('', self.check_chat) - self.chat.bind('', self.check_chat) - self.chat.focus() - self.chat_id = None - - # Message log (incoming and outgoing); we configure it as readonly - self.log = tkinter.scrolledtext.ScrolledText(self) - allow_copy(self.log) - self.log.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW) - self.rowconfigure(2, weight=1) - self.cl.add_event_handler(self.on_message, events.NewMessage) - - # Save shown message IDs to support replying with ".rN reply" - # For instance to reply to the last message ".r1 this is a reply" - # Deletion also works with ".dN". - self.message_ids = [] - - # Save the sent texts to allow editing with ".s text/replacement" - # For instance to edit the last "hello" with "bye" ".s hello/bye" - self.sent_text = collections.deque(maxlen=10) - - # Sending messages - tkinter.Label(self, text='Message:').grid(row=3, column=0) - self.message = tkinter.Entry(self) - self.message.grid(row=3, column=1, sticky=tkinter.EW) - self.message.bind('', self.send_message) - tkinter.Button(self, text='Send', - command=self.send_message).grid(row=3, column=2) - - # Post-init (async, connect client) - self.cl.loop.create_task(self.post_init()) - - async def post_init(self): - """ - Completes the initialization of our application. - Since `__init__` cannot be `async` we use this. - """ - if await self.cl.is_user_authorized(): - self.set_signed_in(await self.cl.get_me()) - else: - # User is not logged in, configure the button to ask them to login - self.sign_in_button.configure(text='Sign in') - self.sign_in_label.configure( - text='Sign in (phone/token):') - - async def on_message(self, event): - """ - Event handler that will add new messages to the message log. - """ - # We want to show only messages sent to this chat - if event.chat_id != self.chat_id: - return - - # Save the message ID so we know which to reply to - self.message_ids.append(event.id) - - # Decide a prefix (">> " for our messages, "" otherwise) - if event.out: - text = '>> ' - else: - sender = await event.get_sender() - text = '<{}> '.format(sanitize_str( - utils.get_display_name(sender))) - - # If the message has media show "(MediaType) " - if event.media: - text += '({}) '.format(event.media.__class__.__name__) - - text += sanitize_str(event.text) - text += '\n' - - # Append the text to the end with a newline, and scroll to the end - self.log.insert(tkinter.END, text) - self.log.yview(tkinter.END) - - # noinspection PyUnusedLocal - @callback - async def sign_in(self, event=None): - """ - Note the `event` argument. This is required since this callback - may be called from a ``widget.bind`` (such as ``''``), - which sends information about the event we don't care about. - - This callback logs out if authorized, signs in if a code was - sent or a bot token is input, or sends the code otherwise. - """ - self.sign_in_label.configure(text='Working...') - self.sign_in_entry.configure(state=tkinter.DISABLED) - if await self.cl.is_user_authorized(): - await self.cl.log_out() - self.destroy() - return - - value = self.sign_in_entry.get().strip() - if self.code: - self.set_signed_in(await self.cl.sign_in(code=value)) - elif ':' in value: - self.set_signed_in(await self.cl.sign_in(bot_token=value)) - else: - self.code = await self.cl.send_code_request(value) - self.sign_in_label.configure(text='Code:') - self.sign_in_entry.configure(state=tkinter.NORMAL) - self.sign_in_entry.delete(0, tkinter.END) - self.sign_in_entry.focus() - return - - def set_signed_in(self, me): - """ - Configures the application as "signed in" (displays user's - name and disables the entry to input phone/bot token/code). - """ - self.me = me - self.sign_in_label.configure(text='Signed in') - self.sign_in_entry.configure(state=tkinter.NORMAL) - self.sign_in_entry.delete(0, tkinter.END) - self.sign_in_entry.insert(tkinter.INSERT, utils.get_display_name(me)) - self.sign_in_entry.configure(state=tkinter.DISABLED) - self.sign_in_button.configure(text='Log out') - self.chat.focus() - - # noinspection PyUnusedLocal - @callback - async def send_message(self, event=None): - """ - Sends a message. Does nothing if the client is not connected. - """ - if not self.cl.is_connected(): - return - - # The user needs to configure a chat where the message should be sent. - # - # If the chat ID does not exist, it was not valid and the user must - # configure one; hint them by changing the background to red. - if not self.chat_id: - self.chat.configure(bg='red') - self.chat.focus() - return - - # Get the message, clear the text field and focus it again - text = self.message.get().strip() - self.message.delete(0, tkinter.END) - self.message.focus() - if not text: - return - - # NOTE: This part is optional but supports editing messages - # You can remove it if you find it too complicated. - # - # Check if the edit matches any text - m = EDIT.match(text) - if m: - find = re.compile(m.group(1).lstrip()) - # Cannot reversed(enumerate(...)), use index - for i in reversed(range(len(self.sent_text))): - msg_id, msg_text = self.sent_text[i] - if find.search(msg_text): - # Found text to replace, so replace it and edit - new = find.sub(m.group(2), msg_text) - self.sent_text[i] = (msg_id, new) - await self.cl.edit_message(self.chat_id, msg_id, new) - - # Notify that a replacement was made - self.log.insert(tkinter.END, '(message edited: {} -> {})\n' - .format(msg_text, new)) - self.log.yview(tkinter.END) - return - - # Check if we want to delete the message - m = DELETE.match(text) - if m: - try: - delete = self.message_ids.pop(-int(m.group(1))) - except IndexError: - pass - else: - await self.cl.delete_messages(self.chat_id, delete) - # Notify that a message was deleted - self.log.insert(tkinter.END, '(message deleted)\n') - self.log.yview(tkinter.END) - return - - # Check if we want to reply to some message - reply_to = None - m = REPLY.match(text) - if m: - text = m.group(2) - try: - reply_to = self.message_ids[-int(m.group(1))] - except IndexError: - pass - - # NOTE: This part is no longer optional. It sends the message. - # Send the message text and get back the sent message object - message = await self.cl.send_message(self.chat_id, text, - reply_to=reply_to) - - # Save the sent message ID and text to allow edits - self.sent_text.append((message.id, text)) - - # Process the sent message as if it were an event - await self.on_message(message) - - # noinspection PyUnusedLocal - @callback - async def check_chat(self, event=None): - """ - Checks the input chat where to send and listen messages from. - """ - if self.me is None: - return # Not logged in yet - - chat = self.chat.get().strip() - try: - chat = int(chat) - except ValueError: - pass - - try: - old = self.chat_id - # Valid chat ID, set it and configure the colour back to white - self.chat_id = await self.cl.get_peer_id(chat) - self.chat.configure(bg='white') - - # If the chat ID changed, clear the - # messages that we could edit or reply - if self.chat_id != old: - self.message_ids.clear() - self.sent_text.clear() - self.log.delete('1.0', tkinter.END) - if not self.me.bot: - for msg in reversed( - await self.cl.get_messages(self.chat_id, 100)): - await self.on_message(msg) - except ValueError: - # Invalid chat ID, let the user know with a yellow background - self.chat_id = None - self.chat.configure(bg='yellow') - - -async def main(loop, interval=0.05): - client = TelegramClient(SESSION, API_ID, API_HASH, loop=loop) - try: - await client.connect() - except Exception as e: - print('Failed to connect', e, file=sys.stderr) - return - - app = App(client) - try: - while True: - # We want to update the application but get back - # to asyncio's event loop. For this we sleep a - # short time so the event loop can run. - # - # https://www.reddit.com/r/Python/comments/33ecpl - app.update() - await asyncio.sleep(interval) - except KeyboardInterrupt: - pass - except tkinter.TclError as e: - if 'application has been destroyed' not in e.args[0]: - raise - finally: - await app.cl.disconnect() - - -if __name__ == "__main__": - # Some boilerplate code to set up the main method - aio_loop = asyncio.get_event_loop() - try: - aio_loop.run_until_complete(main(aio_loop)) - finally: - if not aio_loop.is_closed(): - aio_loop.close() diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py index 88f491de..e69de29b 100644 --- a/telethon_examples/interactive_telegram_client.py +++ b/telethon_examples/interactive_telegram_client.py @@ -1,405 +0,0 @@ -import asyncio -import os -import sys -import time -from getpass import getpass - -from telethon import TelegramClient, events -from telethon.errors import SessionPasswordNeededError -from telethon.network import ConnectionTcpAbridged -from telethon.utils import get_display_name - -# Create a global variable to hold the loop we will be using -loop = asyncio.get_event_loop() - - -def sprint(string, *args, **kwargs): - """Safe Print (handle UnicodeEncodeErrors on some terminals)""" - try: - print(string, *args, **kwargs) - except UnicodeEncodeError: - string = string.encode('utf-8', errors='ignore')\ - .decode('ascii', errors='ignore') - print(string, *args, **kwargs) - - -def print_title(title): - """Helper function to print titles to the console more nicely""" - sprint('\n') - sprint('=={}=='.format('=' * len(title))) - sprint('= {} ='.format(title)) - sprint('=={}=='.format('=' * len(title))) - - -def bytes_to_string(byte_count): - """Converts a byte count to a string (in KB, MB...)""" - suffix_index = 0 - while byte_count >= 1024: - byte_count /= 1024 - suffix_index += 1 - - return '{:.2f}{}'.format( - byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index] - ) - - -async def async_input(prompt): - """ - Python's ``input()`` is blocking, which means the event loop we set - above can't be running while we're blocking there. This method will - let the loop run while we wait for input. - """ - print(prompt, end='', flush=True) - return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip() - - -def get_env(name, message, cast=str): - """Helper to get environment variables interactively""" - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -class InteractiveTelegramClient(TelegramClient): - """Full featured Telegram client, meant to be used on an interactive - session to see what Telethon is capable off - - - This client allows the user to perform some basic interaction with - Telegram through Telethon, such as listing dialogs (open chats), - talking to people, downloading media, and receiving updates. - """ - - def __init__(self, session_user_id, api_id, api_hash, - proxy=None): - """ - Initializes the InteractiveTelegramClient. - :param session_user_id: Name of the *.session file. - :param api_id: Telegram's api_id acquired through my.telegram.org. - :param api_hash: Telegram's api_hash. - :param proxy: Optional proxy tuple/dictionary. - """ - print_title('Initialization') - - print('Initializing interactive example...') - - # The first step is to initialize the TelegramClient, as we are - # subclassing it, we need to call super().__init__(). On a more - # normal case you would want 'client = TelegramClient(...)' - super().__init__( - # These parameters should be passed always, session name and API - session_user_id, api_id, api_hash, - - # You can optionally change the connection mode by passing a - # type or an instance of it. This changes how the sent packets - # look (low-level concept you normally shouldn't worry about). - # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged. - connection=ConnectionTcpAbridged, - - # If you're using a proxy, set it here. - proxy=proxy - ) - - # Store {message.id: message} map here so that we can download - # media known the message ID, for every message having media. - self.found_media = {} - - # Calling .connect() may raise a connection error False, so you need - # to except those before continuing. Otherwise you may want to retry - # as done here. - print('Connecting to Telegram servers...') - try: - loop.run_until_complete(self.connect()) - except IOError: - # We handle IOError and not ConnectionError because - # PySocks' errors do not subclass ConnectionError - # (so this will work with and without proxies). - print('Initial connection failed. Retrying...') - loop.run_until_complete(self.connect()) - - # If the user hasn't called .sign_in() or .sign_up() yet, they won't - # be authorized. The first thing you must do is authorize. Calling - # .sign_in() should only be done once as the information is saved on - # the *.session file so you don't need to enter the code every time. - if not loop.run_until_complete(self.is_user_authorized()): - print('First run. Sending code request...') - user_phone = input('Enter your phone: ') - loop.run_until_complete(self.sign_in(user_phone)) - - self_user = None - while self_user is None: - code = input('Enter the code you just received: ') - try: - self_user =\ - loop.run_until_complete(self.sign_in(code=code)) - - # Two-step verification may be enabled, and .sign_in will - # raise this error. If that's the case ask for the password. - # Note that getpass() may not work on PyCharm due to a bug, - # if that's the case simply change it for input(). - except SessionPasswordNeededError: - pw = getpass('Two step verification is enabled. ' - 'Please enter your password: ') - - self_user =\ - loop.run_until_complete(self.sign_in(password=pw)) - - async def run(self): - """Main loop of the TelegramClient, will wait for user action""" - - # Once everything is ready, we can add an event handler. - # - # Events are an abstraction over Telegram's "Updates" and - # are much easier to use. - self.add_event_handler(self.message_handler, events.NewMessage) - - # Enter a while loop to chat as long as the user wants - while True: - # Retrieve the top dialogs. You can set the limit to None to - # retrieve all of them if you wish, but beware that may take - # a long time if you have hundreds of them. - dialog_count = 15 - - # Entities represent the user, chat or channel - # corresponding to the dialog on the same index. - dialogs = await self.get_dialogs(limit=dialog_count) - - i = None - while i is None: - print_title('Dialogs window') - - # Display them so the user can choose - for i, dialog in enumerate(dialogs, start=1): - sprint('{}. {}'.format(i, get_display_name(dialog.entity))) - - # Let the user decide who they want to talk to - print() - print('> Who do you want to send messages to?') - print('> Available commands:') - print(' !q: Quits the dialogs window and exits.') - print(' !l: Logs out, terminating this session.') - print() - i = await async_input('Enter dialog ID or a command: ') - if i == '!q': - return - if i == '!l': - # Logging out will cause the user to need to reenter the - # code next time they want to use the library, and will - # also delete the *.session file off the filesystem. - # - # This is not the same as simply calling .disconnect(), - # which simply shuts down everything gracefully. - await self.log_out() - return - - try: - i = int(i if i else 0) - 1 - # Ensure it is inside the bounds, otherwise retry - if not 0 <= i < dialog_count: - i = None - except ValueError: - i = None - - # Retrieve the selected user (or chat, or channel) - entity = dialogs[i].entity - - # Show some information - print_title('Chat with "{}"'.format(get_display_name(entity))) - print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') - print(' !dp: Downloads the current dialog Profile picture.') - print(' !i: Prints information about this chat..') - print() - - # And start a while loop to chat - while True: - msg = await async_input('Enter a message: ') - # Quit - if msg == '!q': - break - elif msg == '!Q': - return - - # History - elif msg == '!h': - # First retrieve the messages and some information - messages = await self.get_messages(entity, limit=10) - - # Iterate over all (in reverse order so the latest appear - # the last in the console) and print them with format: - # "[hh:mm] Sender: Message" - for msg in reversed(messages): - # Note how we access .sender here. Since we made an - # API call using the self client, it will always have - # information about the sender. This is different to - # events, where Telegram may not always send the user. - name = get_display_name(msg.sender) - - # Format the message content - if getattr(msg, 'media', None): - self.found_media[msg.id] = msg - content = '<{}> {}'.format( - type(msg.media).__name__, msg.message) - - elif hasattr(msg, 'message'): - content = msg.message - elif hasattr(msg, 'action'): - content = str(msg.action) - else: - # Unknown message, simply print its class name - content = type(msg).__name__ - - # And print it to the user - sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, content)) - - # Send photo - elif msg.startswith('!up '): - # Slice the message to get the path - path = msg[len('!up '):] - await self.send_photo(path=path, entity=entity) - - # Send file (document) - elif msg.startswith('!uf '): - # Slice the message to get the path - path = msg[len('!uf '):] - await self.send_document(path=path, entity=entity) - - # Delete messages - elif msg.startswith('!d '): - # Slice the message to get message ID - msg = msg[len('!d '):] - deleted_msg = await self.delete_messages(entity, msg) - print('Deleted {}'.format(deleted_msg)) - - # Download media - elif msg.startswith('!dm '): - # Slice the message to get message ID - await self.download_media_by_id(msg[len('!dm '):]) - - # Download profile photo - elif msg == '!dp': - print('Downloading profile picture to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = await self.download_profile_photo(entity, - 'usermedia') - if output: - print('Profile picture downloaded to', output) - else: - print('No profile picture found for this user!') - - elif msg == '!i': - attributes = list(entity.to_dict().items()) - pad = max(len(x) for x, _ in attributes) - for name, val in attributes: - print("{:<{width}} : {}".format(name, val, width=pad)) - - # Send chat message (if any) - elif msg: - await self.send_message(entity, msg, link_preview=False) - - async def send_photo(self, path, entity): - """Sends the file located at path to the desired entity as a photo""" - await self.send_file( - entity, path, - progress_callback=self.upload_progress_callback - ) - print('Photo sent!') - - async def send_document(self, path, entity): - """Sends the file located at path to the desired entity as a document""" - await self.send_file( - entity, path, - force_document=True, - progress_callback=self.upload_progress_callback - ) - print('Document sent!') - - async def download_media_by_id(self, media_id): - """Given a message ID, finds the media this message contained and - downloads it. - """ - try: - msg = self.found_media[int(media_id)] - except (ValueError, KeyError): - # ValueError when parsing, KeyError when accessing dictionary - print('Invalid media ID given or message not found!') - return - - print('Downloading media to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = await self.download_media( - msg.media, - file='usermedia/', - progress_callback=self.download_progress_callback - ) - print('Media downloaded to {}!'.format(output)) - - @staticmethod - def download_progress_callback(downloaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress( - 'Downloaded', downloaded_bytes, total_bytes - ) - - @staticmethod - def upload_progress_callback(uploaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress( - 'Uploaded', uploaded_bytes, total_bytes - ) - - @staticmethod - def print_progress(progress_type, downloaded_bytes, total_bytes): - print('{} {} out of {} ({:.2%})'.format( - progress_type, bytes_to_string(downloaded_bytes), - bytes_to_string(total_bytes), downloaded_bytes / total_bytes) - ) - - async def message_handler(self, event): - """Callback method for received events.NewMessage""" - - # Note that message_handler is called when a Telegram update occurs - # and an event is created. Telegram may not always send information - # about the ``.sender`` or the ``.chat``, so if you *really* want it - # you should use ``get_chat()`` and ``get_sender()`` while working - # with events. Since they are methods, you know they may make an API - # call, which can be expensive. - chat = await event.get_chat() - if event.is_group: - if event.out: - sprint('>> sent "{}" to chat {}'.format( - event.text, get_display_name(chat) - )) - else: - sprint('<< {} @ {} sent "{}"'.format( - get_display_name(await event.get_sender()), - get_display_name(chat), - event.text - )) - else: - if event.out: - sprint('>> "{}" to user {}'.format( - event.text, get_display_name(chat) - )) - else: - sprint('<< {} sent "{}"'.format( - get_display_name(chat), event.text - )) - - -if __name__ == '__main__': - SESSION = os.environ.get('TG_SESSION', 'interactive') - API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) - API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - client = InteractiveTelegramClient(SESSION, API_ID, API_HASH) - loop.run_until_complete(client.run()) diff --git a/telethon_examples/print_messages.py b/telethon_examples/print_messages.py index 21aafc59..e69de29b 100644 --- a/telethon_examples/print_messages.py +++ b/telethon_examples/print_messages.py @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# A simple script to print some messages. -import os -import sys -import time - -from telethon import TelegramClient, events, utils - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -session = os.environ.get('TG_SESSION', 'printer') -api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) -api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') -proxy = None # https://github.com/Anorov/PySocks - -# Create and start the client so we can make requests (we don't here) -client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() - - -# `pattern` is a regex, see https://docs.python.org/3/library/re.html -# Use https://regexone.com/ if you want a more interactive way of learning. -# -# "(?i)" makes it case-insensitive, and | separates "options". -@client.on(events.NewMessage(pattern=r'(?i).*\b(hello|hi)\b')) -async def handler(event): - sender = await event.get_sender() - name = utils.get_display_name(sender) - print(name, 'said', event.text, '!') - -try: - print('(Press Ctrl+C to stop this)') - client.run_until_disconnected() -finally: - client.disconnect() - -# Note: We used try/finally to show it can be done this way, but using: -# -# with client: -# client.run_until_disconnected() -# -# is almost always a better idea. diff --git a/telethon_examples/print_updates.py b/telethon_examples/print_updates.py index 48ade9d4..e69de29b 100755 --- a/telethon_examples/print_updates.py +++ b/telethon_examples/print_updates.py @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# A simple script to print all updates received. -# Import modules to access environment, sleep, write to stderr -import os -import sys -import time - -# Import the client -from telethon import TelegramClient - - -# This is a helper method to access environment variables or -# prompt the user to type them in the terminal if missing. -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -# Define some variables so the code reads easier -session = os.environ.get('TG_SESSION', 'printer') -api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) -api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') -proxy = None # https://github.com/Anorov/PySocks - - -# This is our update handler. It is called when a new update arrives. -async def handler(update): - print(update) - - -# Use the client in a `with` block. It calls `start/disconnect` automatically. -with TelegramClient(session, api_id, api_hash, proxy=proxy) as client: - # Register the update handler so that it gets called - client.add_event_handler(handler) - - # Run the client until Ctrl+C is pressed, or the client disconnects - print('(Press Ctrl+C to stop this)') - client.run_until_disconnected() diff --git a/telethon_examples/quart_login.py b/telethon_examples/quart_login.py index b4715c66..e69de29b 100644 --- a/telethon_examples/quart_login.py +++ b/telethon_examples/quart_login.py @@ -1,120 +0,0 @@ -import base64 -import os - -from quart import Quart, request - -from telethon import TelegramClient, utils - - -def get_env(name, message): - if name in os.environ: - return os.environ[name] - return input(message) - - -# Session name, API ID and hash to use; loaded from environmental variables -SESSION = os.environ.get('TG_SESSION', 'quart') -API_ID = int(get_env('TG_API_ID', 'Enter your API ID: ')) -API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - - -# Helper method to add the HTML head/body -def html(inner): - return ''' - - - - - Telethon + Quart - - {} - -'''.format(inner) - - -# Helper method to format messages nicely -async def format_message(message): - if message.photo: - content = '{}'.format( - base64.b64encode(await message.download_media(bytes)).decode(), - message.raw_text - ) - else: - # client.parse_mode = 'html', so bold etc. will work! - content = (message.text or '(action message)').replace('\n', '
') - - return '

{}: {}{}

'.format( - utils.get_display_name(message.sender), - content, - message.date - ) - - -# Define the global phone and Quart app variables -phone = None -app = Quart(__name__) - - -# Quart handlers -@app.route('/', methods=['GET', 'POST']) -async def root(): - # Connect if we aren't yet - if not client.is_connected(): - await client.connect() - - # We want to update the global phone variable to remember it - global phone - - # Check form parameters (phone/code) - form = await request.form - if 'phone' in form: - phone = form['phone'] - await client.send_code_request(phone) - - if 'code' in form: - await client.sign_in(code=form['code']) - - # If we're logged in, show them some messages from their first dialog - if await client.is_user_authorized(): - # They are logged in, show them some messages from their first dialog - dialog = (await client.get_dialogs())[0] - result = '

{}

'.format(dialog.title) - async for m in client.iter_messages(dialog, 10): - result += await(format_message(m)) - - return html(result) - - # Ask for the phone if we don't know it yet - if phone is None: - return html(''' -
- Phone (international format): - -
''') - - # We have the phone, but we're not logged in, so ask for the code - return html(''' -
- Telegram code: - -
''') - - -# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio -# event loop. If we create the `TelegramClient` before, `telethon` will -# use `asyncio.get_event_loop()`, which is the implicit loop in the main -# thread. These two loops are different, and it won't work. -# -# So, we have to manually pass the same `loop` to both applications to -# make 100% sure it works and to avoid headaches. -# -# Quart doesn't seem to offer a way to run inside `async def` -# (see https://gitlab.com/pgjones/quart/issues/146) so we must -# run and block on it last. -# -# This example creates a global client outside of Quart handlers. -# If you create the client inside the handlers (common case), you -# won't have to worry about any of this. -client = TelegramClient(SESSION, API_ID, API_HASH) -client.parse_mode = 'html' # <- render things nicely -app.run(loop=client.loop) # <- same event loop as telethon diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py index f11c39b4..e69de29b 100755 --- a/telethon_examples/replier.py +++ b/telethon_examples/replier.py @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -""" -A example script to automatically send messages based on certain triggers. - -This script assumes that you have certain files on the working directory, -such as "xfiles.m4a" or "anytime.png" for some of the automated replies. -""" -import os -import sys -import time -from collections import defaultdict - -from telethon import TelegramClient, events - -import logging -logging.basicConfig(level=logging.WARNING) - -# "When did we last react?" dictionary, 0.0 by default -recent_reacts = defaultdict(float) - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -def can_react(chat_id): - # Get the time when we last sent a reaction (or 0) - last = recent_reacts[chat_id] - - # Get the current time - now = time.time() - - # If 10 minutes as seconds have passed, we can react - if now - last < 10 * 60: - # Make sure we updated the last reaction time - recent_reacts[chat_id] = now - return True - else: - return False - - -# Register `events.NewMessage` before defining the client. -# Once you have a client, `add_event_handler` will use this event. -@events.register(events.NewMessage) -async def handler(event): - # There are better ways to do this, but this is simple. - # If the message is not outgoing (i.e. someone else sent it) - if not event.out: - if 'emacs' in event.raw_text: - if can_react(event.chat_id): - await event.reply('> emacs\nneeds more vim') - - elif 'vim' in event.raw_text: - if can_react(event.chat_id): - await event.reply('> vim\nneeds more emacs') - - elif 'chrome' in event.raw_text: - if can_react(event.chat_id): - await event.reply('> chrome\nneeds more firefox') - - # Reply always responds as a reply. We can respond without replying too - if 'shrug' in event.raw_text: - if can_react(event.chat_id): - await event.respond(r'¯\_(ツ)_/¯') - - # We can also use client methods from here - client = event.client - - # If we sent the message, we are replying to someone, - # and we said "save pic" in the message - if event.out and event.reply_to_msg_id and 'save pic' in event.raw_text: - reply_msg = await event.get_reply_message() - replied_to_user = await reply_msg.get_input_sender() - - message = await event.reply('Downloading your profile photo...') - file = await client.download_profile_photo(replied_to_user) - await message.edit('I saved your photo in {}'.format(file)) - - -client = TelegramClient( - os.environ.get('TG_SESSION', 'replier'), - get_env('TG_API_ID', 'Enter your API ID: ', int), - get_env('TG_API_HASH', 'Enter your API hash: '), - proxy=None -) - -with client: - # This remembers the events.NewMessage we registered before - client.add_event_handler(handler) - - print('(Press Ctrl+C to stop this)') - client.run_until_disconnected() diff --git a/telethon_examples/screenshot-gui.jpg b/telethon_examples/screenshot-gui.jpg index 4da34b0a..e69de29b 100644 Binary files a/telethon_examples/screenshot-gui.jpg and b/telethon_examples/screenshot-gui.jpg differ diff --git a/telethon_generator/__init__.py b/telethon_generator/__init__.py index 8b137891..e69de29b 100644 --- a/telethon_generator/__init__.py +++ b/telethon_generator/__init__.py @@ -1 +0,0 @@ - diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv index bdba3194..e69de29b 100644 --- a/telethon_generator/data/errors.csv +++ b/telethon_generator/data/errors.csv @@ -1,280 +0,0 @@ -name,codes,description -ABOUT_TOO_LONG,400,The provided bio is too long -ACCESS_TOKEN_EXPIRED,400,Bot token expired -ACCESS_TOKEN_INVALID,400,The provided token is not valid -ACTIVE_USER_REQUIRED,401,The method is only available to already activated users -ADMINS_TOO_MUCH,400,Too many admins -API_ID_INVALID,400,The api_id/api_hash combination is invalid -API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now" -ARTICLE_TITLE_EMPTY,400,The title of the article is empty -AUTH_BYTES_INVALID,400,The provided authorization is invalid -AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions" -AUTH_KEY_INVALID,401,The key is invalid -AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization key, not bound to permanent" -AUTH_KEY_UNREGISTERED,401,The key is not registered in the system -AUTH_RESTART,500,Restart the authorization process -BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default" -BOTS_TOO_MUCH,400,There are too many bots in this chat/channel -BOT_CHANNELS_NA,400,Bots can't edit admin privileges -BOT_GROUPS_BLOCKED,400,This bot can't be added to groups -BOT_INLINE_DISABLED,400,This bot can't be used in inline mode -BOT_INVALID,400,This is not a valid bot -BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot -BOT_MISSING,400,This method can only be run by a bot -BOT_POLLS_DISABLED,400,You cannot create polls under a bot account -BROADCAST_ID_INVALID,400,The channel is invalid -BUTTON_DATA_INVALID,400,The provided button data is invalid -BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid -BUTTON_URL_INVALID,400,Button URL invalid -CALL_ALREADY_ACCEPTED,400,The call was already accepted -CALL_ALREADY_DECLINED,400,The call was already declined -CALL_OCCUPY_FAILED,500,The call failed because the user is already making another call -CALL_PEER_INVALID,400,The provided call peer object is invalid -CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid -CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods -CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel" -CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups -CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited" -CHANNEL_PRIVATE,400,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it -CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available -CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed -CHAT_ABOUT_TOO_LONG,400,Chat about too long -CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this -CHAT_ADMIN_REQUIRED,400 403,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group" -CHAT_FORBIDDEN,,You cannot write in this chat -CHAT_ID_EMPTY,400,The provided chat ID is empty -CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead" -CHAT_INVALID,400,The chat is invalid for this request -CHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request -CHAT_NOT_MODIFIED,400,"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)" -CHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request -CHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat -CHAT_SEND_INLINE_FORBIDDEN,400,You cannot send inline results in this chat -CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat -CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat -CHAT_TITLE_EMPTY,400,No chat title provided -CHAT_WRITE_FORBIDDEN,403,You can't write in this chat -CODE_EMPTY,400,The provided code is empty -CODE_HASH_INVALID,400,Code hash invalid -CODE_INVALID,400,Code invalid (i.e. from email) -CONNECTION_API_ID_INVALID,400,The provided API id is invalid -CONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty -CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty" -CONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest -CONNECTION_NOT_INITED,400,Connection not initialized -CONNECTION_SYSTEM_EMPTY,400,Connection system empty -CONTACT_ID_INVALID,400,The provided contact ID is invalid -DATA_INVALID,400,Encrypted data invalid -DATA_JSON_INVALID,400,The provided JSON data is invalid -DATE_EMPTY,400,Date empty -DC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to -DH_G_A_INVALID,400,g_a invalid -EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it -EMAIL_INVALID,400,The given email is invalid -EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" -EMOTICON_EMPTY,400,The emoticon field cannot be empty -ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid -ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted -ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined -ENCRYPTION_DECLINED,400,The secret chat was declined -ENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid -ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420 -ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs) -ENTITY_MENTION_USER_INVALID,400,You can't use this entity -ERROR_TEXT_EMPTY,400,The provided error message is empty -EXPORT_CARD_INVALID,400,Provided card is invalid -EXTERNAL_URL_INVALID,400,External URL invalid -FIELD_NAME_EMPTY,,The field with the name FIELD_NAME is missing -FIELD_NAME_INVALID,,The field with the name FIELD_NAME is invalid -FILE_ID_INVALID,400,The provided file id is invalid -FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} -FILE_PARTS_INVALID,400,The number of file parts is invalid -FILE_PART_0_MISSING,,File part 0 missing -FILE_PART_EMPTY,400,The provided file part is empty -FILE_PART_INVALID,400,The file part number is invalid -FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid -FILE_PART_SIZE_INVALID,400,The provided file part size is invalid -FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage -FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again -FIRSTNAME_INVALID,400,The first name is invalid -FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers -FLOOD_WAIT_X,420,A wait of {seconds} seconds is required -FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty -FOLDER_ID_INVALID,400,The folder you tried to use was not valid -FRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet -GIF_ID_INVALID,400,The provided GIF ID is invalid -GROUPED_MEDIA_INVALID,400,Invalid grouped media -HASH_INVALID,400,The provided hash is invalid -HISTORY_GET_FAILED,500,Fetching of history failed -IMAGE_PROCESS_FAILED,400,Failure while processing image -INLINE_RESULT_EXPIRED,400,The inline query expired -INPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid -INPUT_FETCH_ERROR,,An error occurred while deserializing TL parameters -INPUT_FETCH_FAIL,400,Failed deserializing TL payload -INPUT_LAYER_INVALID,400,The provided layer is invalid -INPUT_METHOD_INVALID,,The invoked method does not exist anymore or has never existed -INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message) -INPUT_USER_DEACTIVATED,400,The specified user was deleted -INTERDC_X_CALL_ERROR,,An error occurred while communicating with DC {dc} -INTERDC_X_CALL_RICH_ERROR,,A rich error occurred while communicating with DC {dc} -INVITE_HASH_EMPTY,400,The invite hash is empty -INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore -INVITE_HASH_INVALID,400,The invite hash is invalid -LANG_PACK_INVALID,400,The provided language pack is invalid -LASTNAME_INVALID,,The last name is invalid -LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files -LINK_NOT_MODIFIED,400,The channel is already linked to this group -LOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files -MAX_ID_INVALID,400,The provided max ID is invalid -MAX_QTS_INVALID,400,The provided QTS were invalid -MD5_CHECKSUM_INVALID,,The MD5 check-sums do not match -MEDIA_CAPTION_TOO_LONG,400,The caption is too long -MEDIA_EMPTY,400,The provided media object is invalid -MEDIA_INVALID,400,Media invalid -MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes) -MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes) -MEGAGROUP_ID_INVALID,400,The group is invalid -MEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden -MEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location) -MEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed -MESSAGE_AUTHOR_REQUIRED,403,Message author required -MESSAGE_DELETE_FORBIDDEN,403,"You can't delete one of the messages you tried to delete, most likely because it is a service message." -MESSAGE_EDIT_TIME_EXPIRED,400,"You can't edit this message anymore, too much time has passed since its creation." -MESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent -MESSAGE_IDS_EMPTY,400,No message ids were provided -MESSAGE_ID_INVALID,400,The specified message ID is invalid -MESSAGE_NOT_MODIFIED,400,Content of the message was not modified -MESSAGE_TOO_LONG,400,Message was too long. Current maximum length is 4096 UTF-8 characters -MSG_WAIT_FAILED,400,A waiting call returned an error -MT_SEND_QUEUE_TOO_LONG,500, -NEED_CHAT_INVALID,500,The provided chat is invalid -NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size) -NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc} -NEW_SALT_INVALID,400,The new salt is invalid -NEW_SETTINGS_INVALID,400,The new settings are invalid -OFFSET_INVALID,400,"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files" -OFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid -OPTIONS_TOO_MUCH,400,You defined too many options for the poll -PACK_SHORT_NAME_INVALID,400,"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in ""_by_""." -PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists -PARTICIPANTS_TOO_FEW,400,Not enough participants -PARTICIPANT_CALL_FAILED,500,Failure while making call -PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls -PASSWORD_EMPTY,400,The provided password is empty -PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid -PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used -PEER_FLOOD,,Too many requests -PEER_ID_INVALID,400,An invalid Peer was used. Make sure to pass the right peer type -PEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported -PERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty -PERSISTENT_TIMESTAMP_INVALID,400,Persistent timestamp invalid -PERSISTENT_TIMESTAMP_OUTDATED,500,Persistent timestamp outdated -PHONE_CODE_EMPTY,400,The phone code is missing -PHONE_CODE_EXPIRED,400,The confirmation code has expired -PHONE_CODE_HASH_EMPTY,,The phone code hash is missing -PHONE_CODE_INVALID,400,The phone code entered was invalid -PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} -PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400, -PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam -PHONE_NUMBER_FLOOD,400,You asked for the code too many times. -PHONE_NUMBER_INVALID,400 406,The phone number is invalid -PHONE_NUMBER_OCCUPIED,400,The phone number is already in use -PHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used -PHONE_PASSWORD_FLOOD,406,You have tried logging in too many times -PHONE_PASSWORD_PROTECTED,400,This phone is password protected -PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error -PHOTO_CROP_SIZE_SMALL,400,Photo is too small -PHOTO_EXT_INVALID,400,The extension of the photo is invalid -PHOTO_INVALID,400,Photo invalid -PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images) -PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally -PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error -PIN_RESTRICTED,400,You can't pin messages in private chats with other people -POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll -POLL_UNSUPPORTED,400,This layer does not support polls in the issued method -PRIVACY_KEY_INVALID,400,The privacy key is invalid -PTS_CHANGE_EMPTY,500,No PTS change -QUERY_ID_EMPTY,400,The query ID is empty -QUERY_ID_INVALID,400,The query ID is invalid -QUERY_TOO_SHORT,400,The query string is too short -RANDOM_ID_DUPLICATE,500,You provided a random ID that was already used -RANDOM_ID_INVALID,400,A provided random ID is invalid -RANDOM_LENGTH_INVALID,400,Random length invalid -RANGES_INVALID,400,Invalid range provided -REG_ID_GENERATE_FAILED,500,Failure while generating registration ID -REPLY_MARKUP_INVALID,400,The provided reply markup is invalid -REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much -RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs. -RESULT_TYPE_INVALID,400,Result type invalid -RESULTS_TOO_MUCH,400,You sent too many results. See https://core.telegram.org/bots/api#answerinlinequery for the current limit. -RIGHT_FORBIDDEN,403,Your admin rights do not allow you to do this -RPC_CALL_FAIL,,"Telegram is having internal issues, please try again later." -RPC_MCGET_FAIL,,"Telegram is having internal issues, please try again later." -RSA_DECRYPT_FAILED,400,Internal RSA decryption failed -SEARCH_QUERY_EMPTY,400,The search query is empty -SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified -SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid -SESSION_EXPIRED,401,The authorization has expired -SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required -SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions" -SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid -SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name -START_PARAM_EMPTY,400,The start parameter is empty -START_PARAM_INVALID,400,Start parameter invalid -STICKERSET_INVALID,400,The provided sticker set is invalid -STICKERS_EMPTY,400,No sticker provided -STICKER_EMOJI_INVALID,400,Sticker emoji invalid -STICKER_FILE_INVALID,400,Sticker file invalid -STICKER_ID_INVALID,400,The provided sticker ID is invalid -STICKER_INVALID,400,The provided sticker is invalid -STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid -STORAGE_CHECK_FAILED,500,Server storage check failed -STORE_INVALID_SCALAR_TYPE,500, -TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout -TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session -TAKEOUT_REQUIRED,400,You must initialize a takeout request first -TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided -Timeout,-503,A timeout occurred while fetching data from the bot -TMP_PASSWORD_DISABLED,400,The temporary password is disabled -TOKEN_INVALID,400,The provided token is invalid -TTL_DAYS_INVALID,400,The provided TTL is invalid -TYPES_EMPTY,400,The types field is empty -TYPE_CONSTRUCTOR_INVALID,,The type constructor is invalid -UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs -UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) -URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with an URL that's not t.me/yourbot or your game's URL) -USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]""" -USERNAME_NOT_MODIFIED,400,The username is not different from the current username -USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet -USERNAME_OCCUPIED,400,The username is already taken -USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)" -USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)" -USER_ADMIN_INVALID,400,You're not an admin -USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat -USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels -USER_BLOCKED,400,User blocked -USER_BOT,400,Bots can only be admins in channels. -USER_BOT_INVALID,400 403,This method can only be called by a bot -USER_BOT_REQUIRED,400,This method can only be called by a bot -USER_CHANNELS_TOO_MUCH,403,One of the users you tried to add is already in too many channels/supergroups -USER_CREATOR,400,"You can't leave this channel, because you're its creator" -USER_DEACTIVATED,401,The user has been deleted/deactivated -USER_DEACTIVATED_BAN,401,The user has been deleted/deactivated -USER_ID_INVALID,400,"Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited" -USER_INVALID,400,The given user was invalid -USER_IS_BLOCKED,400 403,User is blocked -USER_IS_BOT,400,Bots can't send messages to other bots -USER_KICKED,400,This user was kicked from this supergroup/channel -USER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc} -USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact -USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel -USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this -USER_RESTRICTED,403,"You're spamreported, you can't create channels or chats." -VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming) -WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper -WALLPAPER_INVALID,400,The input wallpaper was not valid -WC_CONVERT_URL_INVALID,400,WC convert URL invalid -WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL -WEBPAGE_MEDIA_EMPTY,400,Webpage media empty -WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately -YOU_BLOCKED_USER,400,You blocked this user diff --git a/telethon_generator/data/friendly.csv b/telethon_generator/data/friendly.csv index eef85540..e69de29b 100644 --- a/telethon_generator/data/friendly.csv +++ b/telethon_generator/data/friendly.csv @@ -1,25 +0,0 @@ -ns,friendly,raw -account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession -auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization -auth.AuthMethods,sign_up,auth.signUp -auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode -auth.AuthMethods,log_out,auth.logOut -auth.AuthMethods,edit_2fa,account.updatePasswordSettings -bots.BotMethods,inline_query,messages.getInlineBotResults -chats.ChatMethods,action,messages.setTyping -chats.ChatMethods,iter_participants,channels.getParticipants -chats.ChatMethods,iter_admin_log,channels.getAdminLog -dialogs.DialogMethods,iter_dialogs,messages.getDialogs -dialogs.DialogMethods,iter_drafts,messages.getAllDrafts -dialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders -downloads.DownloadMethods,download_media,upload.getFile -messages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages -messages.MessageMethods,send_message,messages.sendMessage -messages.MessageMethods,forward_messages,messages.forwardMessages -messages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage -messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages -messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory -updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference -uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia -uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart -users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername diff --git a/telethon_generator/data/html/404.html b/telethon_generator/data/html/404.html index 8eb3d37d..e69de29b 100644 --- a/telethon_generator/data/html/404.html +++ b/telethon_generator/data/html/404.html @@ -1,44 +0,0 @@ - - - Oopsie! | Telethon - - - - - - - -
-

You seem a bit lost…

-

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

-
- - diff --git a/telethon_generator/data/html/core.html b/telethon_generator/data/html/core.html index 80c4c75d..e69de29b 100644 --- a/telethon_generator/data/html/core.html +++ b/telethon_generator/data/html/core.html @@ -1,179 +0,0 @@ - - - - - Telethon API - - - - - - -
- -

Telethon API

-

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

-

- light / - dark theme. -

-

Please note that when you see this:

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

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

- -

Index

- - -

Methods

-

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

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

- -

Types

-

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

- -

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

- -

Constructors

-

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

- -

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

- -

Core types

-

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

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

Full example

-

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

- -

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

-
- - - diff --git a/telethon_generator/data/html/css/docs.dark.css b/telethon_generator/data/html/css/docs.dark.css index b240a9e9..e69de29b 100644 --- a/telethon_generator/data/html/css/docs.dark.css +++ b/telethon_generator/data/html/css/docs.dark.css @@ -1,185 +0,0 @@ -body { - font-family: 'Nunito', sans-serif; - color: #bbb; - background-color:#000; - font-size: 16px; -} - -a { - color: #42aaed; - text-decoration: none; -} - -pre { - font-family: 'Source Code Pro', monospace; - padding: 8px; - color: #567; - background: #080a0c; - border-radius: 0; - overflow-x: auto; -} - -a:hover { - color: #64bbdd; - text-decoration: underline; -} - -table { - width: 100%; - max-width: 100%; -} - -table td { - border-top: 1px solid #111; - padding: 8px; -} - -.horizontal { - margin-bottom: 16px; - list-style: none; - background: #080a0c; - border-radius: 4px; - padding: 8px 16px; -} - -.horizontal li { - display: inline-block; - margin: 0 8px 0 0; -} - -.horizontal img { - display: inline-block; - margin: 0 8px -2px 0; -} - -h1, summary.title { - font-size: 24px; -} - -h3 { - font-size: 20px; -} - -#main_div { - padding: 20px 0; - max-width: 800px; - margin: 0 auto; -} - -pre::-webkit-scrollbar { - visibility: visible; - display: block; - height: 12px; -} - -pre::-webkit-scrollbar-track:horizontal { - background: #222; - border-radius: 0; - height: 12px; -} - -pre::-webkit-scrollbar-thumb:horizontal { - background: #444; - border-radius: 0; - height: 12px; -} - -:target { - border: 2px solid #149; - background: #246; - padding: 4px; -} - -/* 'sh' stands for Syntax Highlight */ -span.sh1 { - color: #f93; -} - -span.tooltip { - border-bottom: 1px dashed #ddd; -} - -#searchBox { - width: 100%; - border: none; - height: 20px; - padding: 8px; - font-size: 16px; - border-radius: 2px; - border: 2px solid #222; - background: #000; - color: #eee; -} - -#searchBox:placeholder-shown { - color: #bbb; - font-style: italic; -} - -button { - border-radius: 2px; - font-size: 16px; - padding: 8px; - color: #bbb; - background-color: #111; - border: 2px solid #146; - transition-duration: 300ms; -} - -button:hover { - background-color: #146; - color: #fff; -} - -/* https://www.w3schools.com/css/css_navbar.asp */ -ul.together { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; -} - -ul.together li { - float: left; -} - -ul.together li a { - display: block; - border-radius: 8px; - background: #111; - padding: 4px 8px; - margin: 8px; -} - -/* https://stackoverflow.com/a/30810322 */ -.invisible { - left: 0; - top: -99px; - padding: 0; - width: 2em; - height: 2em; - border: none; - outline: none; - position: fixed; - box-shadow: none; - color: transparent; - background: transparent; -} - -@media (max-width: 640px) { - h1, summary.title { - font-size: 18px; - } - h3 { - font-size: 16px; - } - - #dev_page_content_wrap { - padding-top: 12px; - } - - #dev_page_title { - margin-top: 10px; - margin-bottom: 20px; - } -} diff --git a/telethon_generator/data/html/css/docs.h4x0r.css b/telethon_generator/data/html/css/docs.h4x0r.css index af3cb210..e69de29b 100644 --- a/telethon_generator/data/html/css/docs.h4x0r.css +++ b/telethon_generator/data/html/css/docs.h4x0r.css @@ -1,229 +0,0 @@ -/* Begin of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css - * - * Hack typeface https://github.com/source-foundry/Hack - * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md - */ -@font-face { - font-family: 'Hack'; - src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff'); - font-weight: 400; - font-style: normal; -} - -@font-face { - font-family: 'Hack'; - src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff'); - font-weight: 700; - font-style: normal; -} - -@font-face { - font-family: 'Hack'; - src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff'); - font-weight: 400; - font-style: italic; -} - -@font-face { - font-family: 'Hack'; - src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff'); - font-weight: 700; - font-style: italic; -} - -/* End of https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css */ - -body { - font-family: 'Hack', monospace; - color: #0a0; - background-color: #000; - font-size: 16px; -} - -::-moz-selection { - color: #000; - background: #0a0; -} - -::selection { - color: #000; - background: #0a0; -} - -a { - color: #0a0; -} - -pre { - padding: 8px; - color: #0c0; - background: #010; - border-radius: 0; - overflow-x: auto; -} - -a:hover { - color: #0f0; - text-decoration: underline; -} - -table { - width: 100%; - max-width: 100%; -} - -table td { - border-top: 1px solid #111; - padding: 8px; -} - -.horizontal { - margin-bottom: 16px; - list-style: none; - background: #010; - border-radius: 4px; - padding: 8px 16px; -} - -.horizontal li { - display: inline-block; - margin: 0 8px 0 0; -} - -.horizontal img { - opacity: 0; - display: inline-block; - margin: 0 8px -2px 0; -} - -h1, summary.title { - font-size: 24px; -} - -h3 { - font-size: 20px; -} - -#main_div { - padding: 20px 0; - max-width: 800px; - margin: 0 auto; -} - -pre::-webkit-scrollbar { - visibility: visible; - display: block; - height: 12px; -} - -pre::-webkit-scrollbar-track:horizontal { - background: #222; - border-radius: 0; - height: 12px; -} - -pre::-webkit-scrollbar-thumb:horizontal { - background: #444; - border-radius: 0; - height: 12px; -} - -:target { - border: 2px solid #0f0; - background: #010; - padding: 4px; -} - -/* 'sh' stands for Syntax Highlight */ -span.sh1 { - color: #0f0; -} - -span.tooltip { - border-bottom: 1px dashed #ddd; -} - -#searchBox { - width: 100%; - border: none; - height: 20px; - padding: 8px; - font-size: 16px; - border-radius: 2px; - border: 2px solid #222; - background: #000; - color: #0e0; - font-family: 'Hack', monospace; -} - -#searchBox:placeholder-shown { - color: #0b0; - font-style: italic; -} - -button { - font-size: 16px; - padding: 8px; - color: #0f0; - background-color: #071007; - border: 2px solid #131; - transition-duration: 300ms; - font-family: 'Hack', monospace; -} - -button:hover { - background-color: #131; -} - -/* https://www.w3schools.com/css/css_navbar.asp */ -ul.together { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; -} - -ul.together li { - float: left; -} - -ul.together li a { - display: block; - border-radius: 8px; - background: #121; - padding: 4px 8px; - margin: 8px; -} - -/* https://stackoverflow.com/a/30810322 */ -.invisible { - left: 0; - top: -99px; - padding: 0; - width: 2em; - height: 2em; - border: none; - outline: none; - position: fixed; - box-shadow: none; - color: transparent; - background: transparent; -} - -@media (max-width: 640px) { - h1, summary.title { - font-size: 18px; - } - h3 { - font-size: 16px; - } - - #dev_page_content_wrap { - padding-top: 12px; - } - - #dev_page_title { - margin-top: 10px; - margin-bottom: 20px; - } -} diff --git a/telethon_generator/data/html/css/docs.light.css b/telethon_generator/data/html/css/docs.light.css index 2d0e95d7..e69de29b 100644 --- a/telethon_generator/data/html/css/docs.light.css +++ b/telethon_generator/data/html/css/docs.light.css @@ -1,182 +0,0 @@ -body { - font-family: 'Nunito', sans-serif; - color: #333; - background-color:#eee; - font-size: 16px; -} - -a { - color: #329add; - text-decoration: none; -} - -pre { - font-family: 'Source Code Pro', monospace; - padding: 8px; - color: #567; - background: #e0e4e8; - border-radius: 0; - overflow-x: auto; -} - -a:hover { - color: #64bbdd; - text-decoration: underline; -} - -table { - width: 100%; - max-width: 100%; -} - -table td { - border-top: 1px solid #ddd; - padding: 8px; -} - -.horizontal { - margin-bottom: 16px; - list-style: none; - background: #e0e4e8; - border-radius: 4px; - padding: 8px 16px; -} - -.horizontal li { - display: inline-block; - margin: 0 8px 0 0; -} - -.horizontal img { - display: inline-block; - margin: 0 8px -2px 0; -} - -h1, summary.title { - font-size: 24px; -} - -h3 { - font-size: 20px; -} - -#main_div { - padding: 20px 0; - max-width: 800px; - margin: 0 auto; -} - -pre::-webkit-scrollbar { - visibility: visible; - display: block; - height: 12px; -} - -pre::-webkit-scrollbar-track:horizontal { - background: #def; - border-radius: 0; - height: 12px; -} - -pre::-webkit-scrollbar-thumb:horizontal { - background: #bdd; - border-radius: 0; - height: 12px; -} - -:target { - border: 2px solid #f8f800; - background: #f8f8f8; - padding: 4px; -} - -/* 'sh' stands for Syntax Highlight */ -span.sh1 { - color: #f70; -} - -span.tooltip { - border-bottom: 1px dashed #444; -} - -#searchBox { - width: 100%; - border: none; - height: 20px; - padding: 8px; - font-size: 16px; - border-radius: 2px; - border: 2px solid #ddd; -} - -#searchBox:placeholder-shown { - font-style: italic; -} - -button { - border-radius: 2px; - font-size: 16px; - padding: 8px; - color: #000; - background-color: #f7f7f7; - border: 2px solid #329add; - transition-duration: 300ms; -} - -button:hover { - background-color: #329add; - color: #f7f7f7; -} - -/* https://www.w3schools.com/css/css_navbar.asp */ -ul.together { - list-style-type: none; - margin: 0; - padding: 0; - overflow: hidden; -} - -ul.together li { - float: left; -} - -ul.together li a { - display: block; - border-radius: 8px; - background: #e0e4e8; - padding: 4px 8px; - margin: 8px; -} - -/* https://stackoverflow.com/a/30810322 */ -.invisible { - left: 0; - top: -99px; - padding: 0; - width: 2em; - height: 2em; - border: none; - outline: none; - position: fixed; - box-shadow: none; - color: transparent; - background: transparent; -} - -@media (max-width: 640px) { - h1, summary.title { - font-size: 18px; - } - h3 { - font-size: 16px; - } - - #dev_page_content_wrap { - padding-top: 12px; - } - - #dev_page_title { - margin-top: 10px; - margin-bottom: 20px; - } -} diff --git a/telethon_generator/data/html/img/arrow.svg b/telethon_generator/data/html/img/arrow.svg index 1e131224..e69de29b 100644 --- a/telethon_generator/data/html/img/arrow.svg +++ b/telethon_generator/data/html/img/arrow.svg @@ -1,35 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - diff --git a/telethon_generator/data/html/js/search.js b/telethon_generator/data/html/js/search.js index a67ffabc..e69de29b 100644 --- a/telethon_generator/data/html/js/search.js +++ b/telethon_generator/data/html/js/search.js @@ -1,244 +0,0 @@ -root = document.getElementById("main_div"); -root.innerHTML = ` - - - -
- - -
Methods (0) -
    -
-
- -
Types (0) -
    -
-
- -
Constructors (0) -
    -
-
-
-
-` + root.innerHTML + "
"; - -// HTML modified, now load documents -contentDiv = document.getElementById("contentDiv"); -searchDiv = document.getElementById("searchDiv"); -searchBox = document.getElementById("searchBox"); - -// Search lists -methodsDetails = document.getElementById("methods"); -methodsList = document.getElementById("methodsList"); -methodsCount = document.getElementById("methodsCount"); - -typesDetails = document.getElementById("types"); -typesList = document.getElementById("typesList"); -typesCount = document.getElementById("typesCount"); - -constructorsDetails = document.getElementById("constructors"); -constructorsList = document.getElementById("constructorsList"); -constructorsCount = document.getElementById("constructorsCount"); - -// Exact match -exactMatch = document.getElementById("exactMatch"); -exactList = document.getElementById("exactList"); - -try { - requests = [{request_names}]; - types = [{type_names}]; - constructors = [{constructor_names}]; - - requestsu = [{request_urls}]; - typesu = [{type_urls}]; - constructorsu = [{constructor_urls}]; -} catch (e) { - requests = []; - types = []; - constructors = []; - requestsu = []; - typesu = []; - constructorsu = []; -} - -if (typeof prependPath !== 'undefined') { - for (var i = 0; i != requestsu.length; ++i) { - requestsu[i] = prependPath + requestsu[i]; - } - for (var i = 0; i != typesu.length; ++i) { - typesu[i] = prependPath + typesu[i]; - } - for (var i = 0; i != constructorsu.length; ++i) { - constructorsu[i] = prependPath + constructorsu[i]; - } -} - -// Assumes haystack has no whitespace and both are lowercase. -// -// Returns the penalty for finding the needle in the haystack -// or -1 if the needle wasn't found at all. -function find(haystack, needle) { - if (haystack.indexOf(needle) != -1) { - return 0; - } - var hi = 0; - var ni = 0; - var penalty = 0; - var started = false; - while (true) { - while (needle[ni] < 'a' || needle[ni] > 'z') { - ++ni; - if (ni == needle.length) { - return penalty; - } - } - while (haystack[hi] != needle[ni]) { - ++hi; - if (started) { - ++penalty; - } - if (hi == haystack.length) { - return -1; - } - } - ++hi; - ++ni; - started = true; - if (ni == needle.length) { - return penalty; - } - if (hi == haystack.length) { - return -1; - } - } -} - -// Given two input arrays "original" and "original urls" and a query, -// return a pair of arrays with matching "query" elements from "original". -// -// TODO Perhaps return an array of pairs instead a pair of arrays (for cache). -function getSearchArray(original, originalu, query) { - var destination = []; - var destinationu = []; - - for (var i = 0; i < original.length; ++i) { - var penalty = find(original[i].toLowerCase(), query); - if (penalty > -1 && penalty < original[i].length / 3) { - destination.push(original[i]); - destinationu.push(originalu[i]); - } - } - - return [destination, destinationu]; -} - -// Modify "countSpan" and "resultList" accordingly based on the elements -// given as [[elements], [element urls]] (both with the same length) -function buildList(countSpan, resultList, foundElements) { - var result = ""; - for (var i = 0; i < foundElements[0].length; ++i) { - result += '
  • '; - result += ''; - result += foundElements[0][i]; - result += '
  • '; - } - - if (countSpan) { - countSpan.innerHTML = "" + foundElements[0].length; - } - resultList.innerHTML = result; -} - -function updateSearch(event) { - var query = searchBox.value.toLowerCase(); - if (!query) { - contentDiv.style.display = ""; - searchDiv.style.display = "none"; - return; - } - - contentDiv.style.display = "none"; - searchDiv.style.display = ""; - - var foundRequests = getSearchArray(requests, requestsu, query); - var foundTypes = getSearchArray(types, typesu, query); - var foundConstructors = getSearchArray(constructors, constructorsu, query); - - var original = requests.concat(constructors); - var originalu = requestsu.concat(constructorsu); - var destination = []; - var destinationu = []; - - for (var i = 0; i < original.length; ++i) { - if (original[i].toLowerCase().replace("request", "") == query) { - destination.push(original[i]); - destinationu.push(originalu[i]); - } - } - - if (event && event.keyCode == 13) { - if (destination.length != 0) { - window.location = destinationu[0]; - } else if (methodsDetails.open && foundRequests[1].length) { - window.location = foundRequests[1][0]; - } else if (typesDetails.open && foundTypes[1].length) { - window.location = foundTypes[1][0]; - } else if (constructorsDetails.open && foundConstructors[1].length) { - window.location = foundConstructors[1][0]; - } - return; - } - - buildList(methodsCount, methodsList, foundRequests); - buildList(typesCount, typesList, foundTypes); - buildList(constructorsCount, constructorsList, foundConstructors); - - // Now look for exact matches - if (destination.length == 0) { - exactMatch.style.display = "none"; - } else { - exactMatch.style.display = ""; - buildList(null, exactList, [destination, destinationu]); - return destinationu[0]; - } -} - -function getQuery(name) { - var query = window.location.search.substring(1); - var vars = query.split("&"); - for (var i = 0; i != vars.length; ++i) { - var pair = vars[i].split("="); - if (pair[0] == name) - return decodeURI(pair[1]); - } -} - -document.onkeydown = function (e) { - if (e.key == '/' || e.key == 's' || e.key == 'S') { - if (document.activeElement != searchBox) { - searchBox.focus(); - return false; - } - } else if (e.key == '?') { - alert('Pressing any of: /sS\nWill focus the search bar\n\n' + - 'Pressing: enter\nWill navigate to the first match') - } -} - -var query = getQuery('q'); -if (query) { - searchBox.value = query; -} - -var exactUrl = updateSearch(); -var redirect = getQuery('redirect'); -if (exactUrl && redirect != 'no') { - window.location = exactUrl; -} diff --git a/telethon_generator/data/methods.csv b/telethon_generator/data/methods.csv index 08b65ed3..e69de29b 100644 --- a/telethon_generator/data/methods.csv +++ b/telethon_generator/data/methods.csv @@ -1,298 +0,0 @@ -method,usability,errors -account.acceptAuthorization,user, -account.cancelPasswordEmail,user, -account.changePhone,user,PHONE_NUMBER_INVALID -account.checkUsername,user,USERNAME_INVALID -account.confirmPasswordEmail,user, -account.confirmPhone,user,CODE_HASH_INVALID PHONE_CODE_EMPTY -account.deleteSecureValue,user, -account.finishTakeoutSession,user, -account.getAccountTTL,user, -account.getAllSecureValues,user, -account.getAuthorizationForm,user, -account.getAuthorizations,user, -account.getContactSignUpNotification,user, -account.getNotifyExceptions,user, -account.getNotifySettings,user,PEER_ID_INVALID -account.getPassword,user, -account.getPasswordSettings,user,PASSWORD_HASH_INVALID -account.getPrivacy,user,PRIVACY_KEY_INVALID -account.getSecureValue,user, -account.getTmpPassword,user,PASSWORD_HASH_INVALID TMP_PASSWORD_DISABLED -account.getWallPaper,user,WALLPAPER_INVALID -account.getWallPapers,user, -account.getWebAuthorizations,user, -account.initTakeoutSession,user, -account.installWallPaper,user,WALLPAPER_INVALID -account.registerDevice,user,TOKEN_INVALID -account.reportPeer,user,PEER_ID_INVALID -account.resendPasswordEmail,user, -account.resetAuthorization,user,HASH_INVALID -account.resetNotifySettings,user, -account.resetWallPapers,user, -account.resetWebAuthorization,user, -account.resetWebAuthorizations,user, -account.saveSecureValue,user,PASSWORD_REQUIRED -account.saveWallPaper,user,WALLPAPER_INVALID -account.sendChangePhoneCode,user,PHONE_NUMBER_INVALID -account.sendConfirmPhoneCode,user,HASH_INVALID -account.sendVerifyEmailCode,user,EMAIL_INVALID -account.sendVerifyPhoneCode,user, -account.setAccountTTL,user,TTL_DAYS_INVALID -account.setContactSignUpNotification,user, -account.setPrivacy,user,PRIVACY_KEY_INVALID -account.unregisterDevice,user,TOKEN_INVALID -account.updateDeviceLocked,user, -account.updateNotifySettings,user,PEER_ID_INVALID -account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID -account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID -account.updateStatus,user,SESSION_PASSWORD_NEEDED -account.updateUsername,user,USERNAME_INVALID USERNAME_NOT_MODIFIED USERNAME_OCCUPIED -account.uploadWallPaper,user,WALLPAPER_FILE_INVALID -account.verifyEmail,user,EMAIL_INVALID -account.verifyPhone,user, -auth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY Timeout -auth.cancelCode,user,PHONE_NUMBER_INVALID -auth.checkPassword,user,PASSWORD_HASH_INVALID -auth.dropTempAuthKeys,both, -auth.exportAuthorization,both,DC_ID_INVALID -auth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID -auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID -auth.logOut,both, -auth.recoverPassword,user,CODE_EMPTY -auth.requestPasswordRecovery,user,PASSWORD_EMPTY -auth.resendCode,user,PHONE_NUMBER_INVALID -auth.resetAuthorizations,user,Timeout -auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED -auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED -auth.signUp,user,FIRSTNAME_INVALID MEMBER_OCCUPY_PRIMARY_LOC_FAILED PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_NUMBER_OCCUPIED REG_ID_GENERATE_FAILED -bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID -bots.sendCustomRequest,bot,USER_BOT_INVALID -channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID -channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED -channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE -channels.deleteHistory,user, -channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN -channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED -channels.editAdmin,both,ADMINS_TOO_MUCH BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID -channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED PHOTO_INVALID -channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED -channels.exportMessageLink,user,CHANNEL_INVALID -channels.getAdminLog,user,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED -channels.getAdminedPublicChannels,user, -channels.getChannels,both,CHANNEL_INVALID CHANNEL_PRIVATE NEED_CHAT_INVALID -channels.getFullChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA Timeout -channels.getLeftChannels,user, -channels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY -channels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT -channels.getParticipants,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID Timeout -channels.inviteToChannel,user,BOTS_TOO_MUCH BOT_GROUPS_BLOCKED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_INVALID CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED USERS_TOO_MUCH USER_BANNED_IN_CHANNEL USER_BLOCKED USER_BOT USER_CHANNELS_TOO_MUCH USER_ID_INVALID USER_KICKED USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -channels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE -channels.leaveChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT -channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE -channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE -channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED -channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN -channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW -channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS -channels.toggleSignatures,user,CHANNEL_INVALID -channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED -contacts.block,user,CONTACT_ID_INVALID -contacts.deleteByPhones,user, -contacts.deleteContact,user,CONTACT_ID_INVALID -contacts.deleteContacts,user,NEED_MEMBER_INVALID Timeout -contacts.getBlocked,user, -contacts.getContactIDs,user, -contacts.getContacts,user, -contacts.getSaved,user,TAKEOUT_REQUIRED -contacts.getStatuses,user, -contacts.getTopPeers,user,TYPES_EMPTY -contacts.importContacts,user, -contacts.resetSaved,user, -contacts.resetTopPeerRating,user,PEER_ID_INVALID -contacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNAME_INVALID USERNAME_NOT_OCCUPIED -contacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY Timeout -contacts.toggleTopPeers,user, -contacts.unblock,user,CONTACT_ID_INVALID -contest.saveDeveloperInfo,both, -folders.deleteFolder,user,FOLDER_ID_EMPTY -folders.editPeerFolders,user,FOLDER_ID_INVALID -help.acceptTermsOfService,user, -help.editUserInfo,user,USER_INVALID -help.getAppChangelog,user, -help.getAppConfig,user, -help.getAppUpdate,user, -help.getCdnConfig,both,AUTH_KEY_PERM_EMPTY Timeout -help.getConfig,both,AUTH_KEY_DUPLICATED Timeout -help.getDeepLinkInfo,user, -help.getInviteText,user, -help.getNearestDc,user, -help.getPassportConfig,user, -help.getProxyData,user, -help.getRecentMeUrls,user, -help.getSupport,user, -help.getSupportName,user,USER_INVALID -help.getTermsOfServiceUpdate,user, -help.getUserInfo,user,USER_INVALID -help.saveAppLog,user, -help.setBotUpdatesStatus,both, -initConnection,both,CONNECTION_LAYER_INVALID INPUT_FETCH_FAIL -invokeAfterMsg,both, -invokeAfterMsgs,both, -invokeWithLayer,both,AUTH_BYTES_INVALID AUTH_KEY_DUPLICATED CDN_METHOD_INVALID CHAT_WRITE_FORBIDDEN CONNECTION_API_ID_INVALID CONNECTION_DEVICE_MODEL_EMPTY CONNECTION_LANG_PACK_INVALID CONNECTION_NOT_INITED CONNECTION_SYSTEM_EMPTY INPUT_LAYER_INVALID INVITE_HASH_EXPIRED NEED_MEMBER_INVALID Timeout -invokeWithMessagesRange,both, -invokeWithTakeout,both, -invokeWithoutUpdates,both, -langpack.getDifference,user,LANG_PACK_INVALID -langpack.getLangPack,user,LANG_PACK_INVALID -langpack.getLanguage,user, -langpack.getLanguages,user,LANG_PACK_INVALID -langpack.getStrings,user,LANG_PACK_INVALID -messages.acceptEncryption,user,CHAT_ID_INVALID ENCRYPTION_ALREADY_ACCEPTED ENCRYPTION_ALREADY_DECLINED ENCRYPTION_OCCUPY_FAILED -messages.addChatUser,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID USERS_TOO_MUCH USER_ALREADY_PARTICIPANT USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -messages.checkChatInvite,user,INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID -messages.clearAllDrafts,user, -messages.clearRecentStickers,user, -messages.createChat,user,USERS_TOO_FEW USER_RESTRICTED -messages.deleteChatUser,both,CHAT_ID_INVALID PEER_ID_INVALID USER_NOT_PARTICIPANT -messages.deleteHistory,user,PEER_ID_INVALID -messages.deleteMessages,both,MESSAGE_DELETE_FORBIDDEN -messages.discardEncryption,user,CHAT_ID_EMPTY ENCRYPTION_ALREADY_DECLINED ENCRYPTION_ID_INVALID -messages.editChatAbout,both, -messages.editChatAdmin,user,CHAT_ID_INVALID -messages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID -messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID -messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID -messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED -messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID -messages.exportChatInvite,user,CHAT_ID_INVALID -messages.faveSticker,user,STICKER_ID_INVALID -messages.forwardMessages,both,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 RANDOM_ID_DUPLICATE RANDOM_ID_INVALID Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER -messages.getAllChats,user, -messages.getAllDrafts,user, -messages.getAllStickers,user, -messages.getArchivedStickers,user, -messages.getAttachedStickers,user, -messages.getBotCallbackAnswer,user,CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID Timeout -messages.getChats,both,CHAT_ID_INVALID PEER_ID_INVALID -messages.getCommonChats,user,USER_ID_INVALID -messages.getDhConfig,user,RANDOM_LENGTH_INVALID -messages.getDialogUnreadMarks,user, -messages.getDialogs,user,INPUT_CONSTRUCTOR_INVALID OFFSET_PEER_ID_INVALID SESSION_PASSWORD_NEEDED Timeout -messages.getDocumentByHash,both,SHA256_HASH_INVALID -messages.getFavedStickers,user, -messages.getFeaturedStickers,user, -messages.getFullChat,both,CHAT_ID_INVALID PEER_ID_INVALID -messages.getGameHighScores,bot,PEER_ID_INVALID USER_BOT_REQUIRED -messages.getHistory,user,AUTH_KEY_DUPLICATED AUTH_KEY_PERM_EMPTY CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID Timeout -messages.getInlineBotResults,user,BOT_INLINE_DISABLED BOT_INVALID CHANNEL_PRIVATE Timeout -messages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED -messages.getMaskStickers,user, -messages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID -messages.getMessages,both, -messages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID -messages.getOnlines,user, -messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID -messages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID -messages.getPinnedDialogs,user, -messages.getPollResults,user, -messages.getRecentLocations,user, -messages.getRecentStickers,user, -messages.getSavedGifs,user, -messages.getSplitRanges,user, -messages.getStatsURL,user, -messages.getStickerSet,both,STICKERSET_INVALID -messages.getStickers,user,EMOTICON_EMPTY -messages.getUnreadMentions,user,PEER_ID_INVALID -messages.getWebPage,user,WC_CONVERT_URL_INVALID -messages.getWebPagePreview,user, -messages.hideReportSpam,user,PEER_ID_INVALID -messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT -messages.installStickerSet,user,STICKERSET_INVALID -messages.markDialogUnread,user, -messages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID -messages.readEncryptedHistory,user,MSG_WAIT_FAILED -messages.readFeaturedStickers,user, -messages.readHistory,user,PEER_ID_INVALID Timeout -messages.readMentions,user, -messages.readMessageContents,user, -messages.receivedMessages,user, -messages.receivedQueue,user,MSG_WAIT_FAILED MAX_QTS_INVALID -messages.reorderPinnedDialogs,user,PEER_ID_INVALID -messages.reorderStickerSets,user, -messages.report,user, -messages.reportEncryptedSpam,user,CHAT_ID_INVALID -messages.reportSpam,user,PEER_ID_INVALID -messages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID -messages.saveDraft,user,PEER_ID_INVALID -messages.saveGif,user,GIF_ID_INVALID -messages.saveRecentSticker,user,STICKER_ID_INVALID -messages.search,user,CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID -messages.searchGifs,user,SEARCH_QUERY_EMPTY -messages.searchGlobal,user,SEARCH_QUERY_EMPTY -messages.searchStickerSets,user, -messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED -messages.sendEncryptedFile,user,MSG_WAIT_FAILED -messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED -messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMedia,both,BOT_POLLS_DISABLED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_OPTION_DUPLICATE RANDOM_ID_DUPLICATE STORAGE_CHECK_FAILED Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMessage,both,AUTH_KEY_DUPLICATED 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_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG PEER_ID_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG Timeout USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER -messages.sendMultiMedia,both, -messages.sendVote,user, -messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID -messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY -messages.setBotShippingResults,both,QUERY_ID_INVALID -messages.setEncryptedTyping,user,CHAT_ID_INVALID -messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED -messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID MESSAGE_EMPTY 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 USER_BOT_INVALID -messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED -messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT -messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID -messages.toggleDialogPin,user,PEER_ID_INVALID -messages.uninstallStickerSet,user,STICKERSET_INVALID -messages.updatePinnedMessage,both, -messages.uploadEncryptedFile,user, -messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID -payments.clearSavedInfo,user, -payments.getPaymentForm,user,MESSAGE_ID_INVALID -payments.getPaymentReceipt,user,MESSAGE_ID_INVALID -payments.getSavedInfo,user, -payments.sendPaymentForm,user,MESSAGE_ID_INVALID -payments.validateRequestedInfo,user,MESSAGE_ID_INVALID -phone.acceptCall,user,CALL_ALREADY_ACCEPTED CALL_ALREADY_DECLINED CALL_OCCUPY_FAILED CALL_PEER_INVALID CALL_PROTOCOL_FLAGS_INVALID -phone.confirmCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID -phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID -phone.getCallConfig,user, -phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID -phone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED -phone.saveCallDebug,user,CALL_PEER_INVALID DATA_JSON_INVALID -phone.setCallRating,user,CALL_PEER_INVALID -photos.deletePhotos,user, -photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID -photos.updateProfilePhoto,user, -photos.uploadProfilePhoto,user,FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID -ping,both, -reqDHParams,both, -reqPq,both, -reqPqMulti,both, -rpcDropAnswer,both, -setClientDHParams,both, -stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID -stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID -stickers.createStickerSet,bot,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 USER_ID_INVALID -stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID -updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID Timeout -updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE Timeout -updates.getState,both,AUTH_KEY_DUPLICATED SESSION_PASSWORD_NEEDED Timeout -upload.getCdnFile,user,UNKNOWN_METHOD -upload.getCdnFileHashes,both,CDN_METHOD_INVALID RSA_DECRYPT_FAILED -upload.getFile,both,AUTH_KEY_PERM_EMPTY FILE_ID_INVALID INPUT_FETCH_FAIL LIMIT_INVALID LOCATION_INVALID OFFSET_INVALID Timeout -upload.getFileHashes,both, -upload.getWebFile,user,LOCATION_INVALID -upload.reuploadCdnFile,both,RSA_DECRYPT_FAILED -upload.saveBigFilePart,both,FILE_PARTS_INVALID FILE_PART_EMPTY FILE_PART_INVALID FILE_PART_SIZE_INVALID Timeout -upload.saveFilePart,both,FILE_PART_EMPTY FILE_PART_INVALID INPUT_FETCH_FAIL SESSION_PASSWORD_NEEDED -users.getFullUser,both,Timeout USER_ID_INVALID -users.getUsers,both,AUTH_KEY_PERM_EMPTY MEMBER_NO_LOCATION NEED_MEMBER_INVALID SESSION_PASSWORD_NEEDED Timeout -users.setSecureValueErrors,bot, diff --git a/telethon_generator/data/scheme.tl b/telethon_generator/data/scheme.tl index 357b7404..e69de29b 100644 --- a/telethon_generator/data/scheme.tl +++ b/telethon_generator/data/scheme.tl @@ -1,1498 +0,0 @@ -// Core types (no need to gen) - -//vector#1cb5c415 {t:Type} # [ t ] = Vector t; - -/////////////////////////////// -/////////////////// Layer cons -/////////////////////////////// - -//invokeAfterMsg#cb9f372d msg_id:long query:!X = X; -//invokeAfterMsgs#3dc4b4f0 msg_ids:Vector query:!X = X; -//invokeWithLayer1#53835315 query:!X = X; -//invokeWithLayer2#289dd1f6 query:!X = X; -//invokeWithLayer3#b7475268 query:!X = X; -//invokeWithLayer4#dea0d430 query:!X = X; -//invokeWithLayer5#417a57ae query:!X = X; -//invokeWithLayer6#3a64d54d query:!X = X; -//invokeWithLayer7#a5be56d3 query:!X = X; -//invokeWithLayer8#e9abd9fd query:!X = X; -//invokeWithLayer9#76715a63 query:!X = X; -//invokeWithLayer10#39620c41 query:!X = X; -//invokeWithLayer11#a6b88fdf query:!X = X; -//invokeWithLayer12#dda60d3c query:!X = X; -//invokeWithLayer13#427c8ea2 query:!X = X; -//invokeWithLayer14#2b9b08fa query:!X = X; -//invokeWithLayer15#b4418b64 query:!X = X; -//invokeWithLayer16#cf5f0987 query:!X = X; -//invokeWithLayer17#50858a19 query:!X = X; -//invokeWithLayer18#1c900537 query:!X = X; -//invokeWithLayer#da9b0d0d layer:int query:!X = X; // after 18 layer - -/////////////////////////////// -/// Authorization key creation -/////////////////////////////// - -resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector = ResPQ; - -p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; -p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; -p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; -p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; - -server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params; -server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params; - -server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data; - -client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data; - -dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer; -dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer; -dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer; - -destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes; -destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes; -destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; - ----functions--- - -req_pq#60469778 nonce:int128 = ResPQ; -req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; - -req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params; - -set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer; - -destroy_auth_key#d1435160 = DestroyAuthKeyRes; - -/////////////////////////////// -////////////// System messages -/////////////////////////////// - ----types--- - -msgs_ack#62d6b459 msg_ids:Vector = MsgsAck; - -bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; -bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; - -msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq; -msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo; -msgs_all_info#8cc0d131 msg_ids:Vector info:string = MsgsAllInfo; - -msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; -msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; - -msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq; - -//rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually - -rpc_error#2144ca19 error_code:int error_message:string = RpcError; - -rpc_answer_unknown#5e2ad36e = RpcDropAnswer; -rpc_answer_dropped_running#cd78e586 = RpcDropAnswer; -rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer; - -future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt; -future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; - -pong#347773c5 msg_id:long ping_id:long = Pong; - -destroy_session_ok#e22045fc session_id:long = DestroySessionRes; -destroy_session_none#62d350c9 session_id:long = DestroySessionRes; - -new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; - -//message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually -//msg_container#73f1f8dc messages:vector = MessageContainer; // parsed manually -//msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container -//gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually - -http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; - -//ipPort ipv4:int port:int = IpPort; -//help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; - -ipPort#d433ad73 ipv4:int port:int = IpPort; -ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; -accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; -help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; - ----functions--- - -rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; - -get_future_salts#b921bd04 num:int = FutureSalts; - -ping#7abe77ec ping_id:long = Pong; -ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; - -destroy_session#e7512126 session_id:long = DestroySessionRes; - -contest.saveDeveloperInfo#9a5f6e95 vk_id:int name:string phone_number:string age:int city:string = Bool; - -/////////////////////////////// -///////// Main application API -/////////////////////////////// - ----types--- - -boolFalse#bc799737 = Bool; -boolTrue#997275b5 = Bool; - -true#3fedd339 = True; - -vector#1cb5c415 {t:Type} # [ t ] = Vector t; - -error#c4b9f9bb code:int text:string = Error; - -null#56730bcc = Null; - -inputPeerEmpty#7f3b18ea = InputPeer; -inputPeerSelf#7da07ec9 = InputPeer; -inputPeerChat#179be863 chat_id:int = InputPeer; -inputPeerUser#7b8e7de6 user_id:int access_hash:long = InputPeer; -inputPeerChannel#20adaef8 channel_id:int access_hash:long = InputPeer; -inputPeerUserFromMessage#17bae2e6 peer:InputPeer msg_id:int user_id:int = InputPeer; -inputPeerChannelFromMessage#9c95f7bb peer:InputPeer msg_id:int channel_id:int = InputPeer; - -inputUserEmpty#b98886cf = InputUser; -inputUserSelf#f7c1b13f = InputUser; -inputUser#d8292816 user_id:int access_hash:long = InputUser; -inputUserFromMessage#2d117597 peer:InputPeer msg_id:int user_id:int = InputUser; - -inputPhoneContact#f392b7f4 client_id:long phone:string first_name:string last_name:string = InputContact; - -inputFile#f52ff27f id:long parts:int name:string md5_checksum:string = InputFile; -inputFileBig#fa4f0bb5 id:long parts:int name:string = InputFile; - -inputMediaEmpty#9664f57f = InputMedia; -inputMediaUploadedPhoto#1e287d04 flags:# file:InputFile stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaPhoto#b3ba0635 flags:# id:InputPhoto ttl_seconds:flags.0?int = InputMedia; -inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; -inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; -inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaDocument#23ab23d2 flags:# id:InputDocument ttl_seconds:flags.0?int = InputMedia; -inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; -inputMediaGifExternal#4843b0fd url:string q:string = InputMedia; -inputMediaPhotoExternal#e5bbfe1a flags:# url:string ttl_seconds:flags.0?int = InputMedia; -inputMediaDocumentExternal#fb52dc99 flags:# url:string ttl_seconds:flags.0?int = InputMedia; -inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#f4e096c3 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:string = InputMedia; -inputMediaGeoLive#ce4e82fd flags:# stopped:flags.0?true geo_point:InputGeoPoint period:flags.1?int = InputMedia; -inputMediaPoll#6b3765b poll:Poll = InputMedia; - -inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; -inputChatUploadedPhoto#927c55b4 file:InputFile = InputChatPhoto; -inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; - -inputGeoPointEmpty#e4c123d6 = InputGeoPoint; -inputGeoPoint#f3b7acc9 lat:double long:double = InputGeoPoint; - -inputPhotoEmpty#1cd7bf0d = InputPhoto; -inputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto; - -inputFileLocation#dfdaabe1 volume_id:long local_id:int secret:long file_reference:bytes = InputFileLocation; -inputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation; -inputDocumentFileLocation#bad07584 id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; -inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation; -inputTakeoutFileLocation#29be5899 = InputFileLocation; -inputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; -inputPeerPhotoFileLocation#27d69997 flags:# big:flags.0?true peer:InputPeer volume_id:long local_id:int = InputFileLocation; -inputStickerSetThumb#dbaeae9 stickerset:InputStickerSet volume_id:long local_id:int = InputFileLocation; - -peerUser#9db1bc6d user_id:int = Peer; -peerChat#bad0e5bb chat_id:int = Peer; -peerChannel#bddde532 channel_id:int = Peer; - -storage.fileUnknown#aa963b05 = storage.FileType; -storage.filePartial#40bc6f52 = storage.FileType; -storage.fileJpeg#7efe0e = storage.FileType; -storage.fileGif#cae1aadf = storage.FileType; -storage.filePng#a4f63c0 = storage.FileType; -storage.filePdf#ae1e508d = storage.FileType; -storage.fileMp3#528a0677 = storage.FileType; -storage.fileMov#4b09ebbc = storage.FileType; -storage.fileMp4#b3cea0e4 = storage.FileType; -storage.fileWebp#1081464c = storage.FileType; - -userEmpty#200250ba id:int = User; -user#2e13f4c3 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 id:int 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?string bot_inline_placeholder:flags.19?string lang_code:flags.22?string = User; - -userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; -userProfilePhoto#ecd75d8c photo_id:long photo_small:FileLocation photo_big:FileLocation dc_id:int = UserProfilePhoto; - -userStatusEmpty#9d05049 = UserStatus; -userStatusOnline#edb93949 expires:int = UserStatus; -userStatusOffline#8c703f was_online:int = UserStatus; -userStatusRecently#e26f42f1 = UserStatus; -userStatusLastWeek#7bf09fc = UserStatus; -userStatusLastMonth#77ebc742 = UserStatus; - -chatEmpty#9ba2d800 id:int = Chat; -chat#3bda1bde flags:# creator:flags.0?true kicked:flags.1?true left:flags.2?true deactivated:flags.5?true id:int title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; -chatForbidden#7328bdb id:int title:string = Chat; -channel#4df30834 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 id:int access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int version:int restriction_reason:flags.9?string admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int = Chat; -channelForbidden#289da732 flags:# broadcast:flags.5?true megagroup:flags.8?true id:int access_hash:long title:string until_date:flags.16?int = Chat; - -chatFull#1b7c9db3 flags:# can_set_username:flags.7?true id:int about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int = ChatFull; -channelFull#10916653 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_view_stats:flags.12?true can_set_location:flags.16?true id:int about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int 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:ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?int migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?int location:flags.15?ChannelLocation pts:int = ChatFull; - -chatParticipant#c8d7493e user_id:int inviter_id:int date:int = ChatParticipant; -chatParticipantCreator#da13538a user_id:int = ChatParticipant; -chatParticipantAdmin#e2d6e436 user_id:int inviter_id:int date:int = ChatParticipant; - -chatParticipantsForbidden#fc900c2b flags:# chat_id:int self_participant:flags.0?ChatParticipant = ChatParticipants; -chatParticipants#3f460fed chat_id:int participants:Vector version:int = ChatParticipants; - -chatPhotoEmpty#37c1011c = ChatPhoto; -chatPhoto#475cdbd5 photo_small:FileLocation photo_big:FileLocation dc_id:int = ChatPhoto; - -messageEmpty#83e5de54 id:int = Message; -message#44f9b43d flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true id:int from_id:flags.8?int to_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long = Message; -messageService#9e19a1f6 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true legacy:flags.19?true id:int from_id:flags.8?int to_id:Peer reply_to_msg_id:flags.3?int date:int action:MessageAction = Message; - -messageMediaEmpty#3ded6320 = MessageMedia; -messageMediaPhoto#695150d7 flags:# photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; -messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; -messageMediaContact#cbf24940 phone_number:string first_name:string last_name:string vcard:string user_id:int = MessageMedia; -messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#9cb070d7 flags:# document:flags.0?Document ttl_seconds:flags.2?int = MessageMedia; -messageMediaWebPage#a32dd600 webpage:WebPage = MessageMedia; -messageMediaVenue#2ec0533f geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string = MessageMedia; -messageMediaGame#fdb19008 game:Game = MessageMedia; -messageMediaInvoice#84551347 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string = MessageMedia; -messageMediaGeoLive#7c3c2609 geo:GeoPoint period:int = MessageMedia; -messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; - -messageActionEmpty#b6aef7b0 = MessageAction; -messageActionChatCreate#a6638b9a title:string users:Vector = MessageAction; -messageActionChatEditTitle#b5a1ce5a title:string = MessageAction; -messageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction; -messageActionChatDeletePhoto#95e3fbef = MessageAction; -messageActionChatAddUser#488a7337 users:Vector = MessageAction; -messageActionChatDeleteUser#b2ae9b0c user_id:int = MessageAction; -messageActionChatJoinedByLink#f89cf5e8 inviter_id:int = MessageAction; -messageActionChannelCreate#95d2ac92 title:string = MessageAction; -messageActionChatMigrateTo#51bdb021 channel_id:int = MessageAction; -messageActionChannelMigrateFrom#b055eaee title:string chat_id:int = MessageAction; -messageActionPinMessage#94bd38ed = MessageAction; -messageActionHistoryClear#9fbab604 = MessageAction; -messageActionGameScore#92a72876 game_id:long score:int = MessageAction; -messageActionPaymentSentMe#8f31b327 flags:# currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; -messageActionPaymentSent#40699cd0 currency:string total_amount:long = MessageAction; -messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; -messageActionScreenshotTaken#4792929b = MessageAction; -messageActionCustomAction#fae69f56 message:string = MessageAction; -messageActionBotAllowed#abe9affe domain:string = MessageAction; -messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; -messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; -messageActionContactSignUp#f3f25f76 = MessageAction; - -dialog#2c171f72 flags:# pinned:flags.2?true unread_mark:flags.3?true peer:Peer top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int = Dialog; -dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; - -photoEmpty#2331b22d id:long = Photo; -photo#d07504a5 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector dc_id:int = Photo; - -photoSizeEmpty#e17e23c type:string = PhotoSize; -photoSize#77bfb61b type:string location:FileLocation w:int h:int size:int = PhotoSize; -photoCachedSize#e9a734fa type:string location:FileLocation w:int h:int bytes:bytes = PhotoSize; -photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; - -geoPointEmpty#1117dd5f = GeoPoint; -geoPoint#296f104 long:double lat:double access_hash:long = GeoPoint; - -auth.checkedPhone#811ea28e phone_registered:Bool = auth.CheckedPhone; - -auth.sentCode#38faab5f flags:# phone_registered:flags.0?true type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int terms_of_service:flags.3?help.TermsOfService = auth.SentCode; - -auth.authorization#cd050916 flags:# tmp_sessions:flags.0?int user:User = auth.Authorization; - -auth.exportedAuthorization#df969c2d id:int bytes:bytes = auth.ExportedAuthorization; - -inputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer; -inputNotifyUsers#193b4417 = InputNotifyPeer; -inputNotifyChats#4a95e84e = InputNotifyPeer; -inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer; - -inputPeerNotifySettings#9c3d198e flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = InputPeerNotifySettings; - -peerNotifySettings#af509d20 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?string = PeerNotifySettings; - -peerSettings#818426cd 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 = PeerSettings; - -wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; - -inputReportReasonSpam#58dbcab8 = ReportReason; -inputReportReasonViolence#1e22c78d = ReportReason; -inputReportReasonPornography#2e59d922 = ReportReason; -inputReportReasonChildAbuse#adf44ee3 = ReportReason; -inputReportReasonOther#e1746d0a text:string = ReportReason; -inputReportReasonCopyright#9b89f93a = ReportReason; -inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; - -userFull#edf17c12 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true user:User about:flags.1?string settings:PeerSettings profile_photo:flags.2?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int = UserFull; - -contact#f911c994 user_id:int mutual:Bool = Contact; - -importedContact#d0028438 user_id:int client_id:long = ImportedContact; - -contactBlocked#561bc879 user_id:int date:int = ContactBlocked; - -contactStatus#d3680c61 user_id:int status:UserStatus = ContactStatus; - -contacts.contactsNotModified#b74ba9d2 = contacts.Contacts; -contacts.contacts#eae87e42 contacts:Vector saved_count:int users:Vector = contacts.Contacts; - -contacts.importedContacts#77d01c3b imported:Vector popular_invites:Vector retry_contacts:Vector users:Vector = contacts.ImportedContacts; - -contacts.blocked#1c138d15 blocked:Vector users:Vector = contacts.Blocked; -contacts.blockedSlice#900802a1 count:int blocked:Vector users:Vector = contacts.Blocked; - -messages.dialogs#15ba6c40 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; -messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; -messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; - -messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesSlice#c8edce1e flags:# inexact:flags.1?true count:int next_rate:flags.0?int messages:Vector chats:Vector users:Vector = messages.Messages; -messages.channelMessages#99262e37 flags:# inexact:flags.1?true pts:int count:int messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesNotModified#74535f21 count:int = messages.Messages; - -messages.chats#64ff9fd5 chats:Vector = messages.Chats; -messages.chatsSlice#9cd81144 count:int chats:Vector = messages.Chats; - -messages.chatFull#e5d7d19c full_chat:ChatFull chats:Vector users:Vector = messages.ChatFull; - -messages.affectedHistory#b45c69d1 pts:int pts_count:int offset:int = messages.AffectedHistory; - -inputMessagesFilterEmpty#57e2f66c = MessagesFilter; -inputMessagesFilterPhotos#9609a51c = MessagesFilter; -inputMessagesFilterVideo#9fc00e65 = MessagesFilter; -inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter; -inputMessagesFilterDocument#9eddf188 = MessagesFilter; -inputMessagesFilterUrl#7ef0dd87 = MessagesFilter; -inputMessagesFilterGif#ffc86587 = MessagesFilter; -inputMessagesFilterVoice#50f5c392 = MessagesFilter; -inputMessagesFilterMusic#3751b49e = MessagesFilter; -inputMessagesFilterChatPhotos#3a20ecb8 = MessagesFilter; -inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFilter; -inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; -inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; -inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; -inputMessagesFilterGeo#e7026d0d = MessagesFilter; -inputMessagesFilterContacts#e062db83 = MessagesFilter; - -updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; -updateMessageID#4e90bfd6 id:int random_id:long = Update; -updateDeleteMessages#a20db0e5 messages:Vector pts:int pts_count:int = Update; -updateUserTyping#5c486927 user_id:int action:SendMessageAction = Update; -updateChatUserTyping#9a65ea1f chat_id:int user_id:int action:SendMessageAction = Update; -updateChatParticipants#7761198 participants:ChatParticipants = Update; -updateUserStatus#1bfbd823 user_id:int status:UserStatus = Update; -updateUserName#a7332b73 user_id:int first_name:string last_name:string username:string = Update; -updateUserPhoto#95313b0c user_id:int date:int photo:UserProfilePhoto previous:Bool = Update; -updateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update; -updateEncryptedChatTyping#1710f156 chat_id:int = Update; -updateEncryption#b4a2e88d chat:EncryptedChat date:int = Update; -updateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update; -updateChatParticipantAdd#ea4b0e5c chat_id:int user_id:int inviter_id:int date:int version:int = Update; -updateChatParticipantDelete#6e5f8c22 chat_id:int user_id:int version:int = Update; -updateDcOptions#8e5e9873 dc_options:Vector = Update; -updateUserBlocked#80ece81a user_id:int blocked:Bool = Update; -updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update; -updateServiceNotification#ebe46819 flags:# popup:flags.0?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; -updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; -updateUserPhone#12b9417b user_id:int phone:string = Update; -updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; -updateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update; -updateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update; -updateReadMessagesContents#68c13933 messages:Vector pts:int pts_count:int = Update; -updateChannelTooLong#eb0467fb flags:# channel_id:int pts:flags.0?int = Update; -updateChannel#b6d45656 channel_id:int = Update; -updateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update; -updateReadChannelInbox#330b5424 flags:# folder_id:flags.0?int channel_id:int max_id:int still_unread_count:int pts:int = Update; -updateDeleteChannelMessages#c37521c9 channel_id:int messages:Vector pts:int pts_count:int = Update; -updateChannelMessageViews#98a12b4b channel_id:int id:int views:int = Update; -updateChatParticipantAdmin#b6901959 chat_id:int user_id:int is_admin:Bool version:int = Update; -updateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update; -updateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true order:Vector = Update; -updateStickerSets#43ae3dec = Update; -updateSavedGifs#9375341e = Update; -updateBotInlineQuery#54826690 flags:# query_id:long user_id:int query:string geo:flags.0?GeoPoint offset:string = Update; -updateBotInlineSend#e48f964 flags:# user_id:int query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; -updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; -updateChannelPinnedMessage#98592475 channel_id:int id:int = Update; -updateBotCallbackQuery#e73547e1 flags:# query_id:long user_id:int peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; -updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; -updateInlineBotCallbackQuery#f9d27a5a flags:# query_id:long user_id:int msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; -updateReadChannelOutbox#25d6c9c7 channel_id:int max_id:int = Update; -updateDraftMessage#ee2bb969 peer:Peer draft:DraftMessage = Update; -updateReadFeaturedStickers#571d2742 = Update; -updateRecentStickers#9a422c20 = Update; -updateConfig#a229dd06 = Update; -updatePtsChanged#3354678f = Update; -updateChannelWebPage#40771900 channel_id:int webpage:WebPage pts:int pts_count:int = Update; -updateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update; -updatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector = Update; -updateBotWebhookJSON#8317c0c3 data:DataJSON = Update; -updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update; -updateBotShippingQuery#e0cdc940 query_id:long user_id:int payload:bytes shipping_address:PostAddress = Update; -updateBotPrecheckoutQuery#5d2f3aa9 flags:# query_id:long user_id:int payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; -updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; -updateLangPackTooLong#46560264 lang_code:string = Update; -updateLangPack#56022f4d difference:LangPackDifference = Update; -updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#89893b45 channel_id:int messages:Vector = Update; -updateContactsReset#7084a7be = Update; -updateChannelAvailableMessages#70db6837 channel_id:int available_min_id:int = Update; -updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; -updateUserPinnedMessage#4c43da18 user_id:int id:int = Update; -updateChatPinnedMessage#e10db349 chat_id:int id:int version:int = Update; -updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; -updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; -updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; -updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; -updatePeerLocated#b4afcfb0 peers:Vector = Update; - -updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; - -updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; -updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; -updates.differenceSlice#a8fb1981 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector intermediate_state:updates.State = updates.Difference; -updates.differenceTooLong#4afe8f6d pts:int = updates.Difference; - -updatesTooLong#e317af7e = Updates; -updateShortMessage#914fbf11 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:int message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int entities:flags.7?Vector = Updates; -updateShortChatMessage#16812688 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:int chat_id:int message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?int reply_to_msg_id:flags.3?int entities:flags.7?Vector = Updates; -updateShort#78d4dec1 update:Update date:int = Updates; -updatesCombined#725b04c3 updates:Vector users:Vector chats:Vector date:int seq_start:int seq:int = Updates; -updates#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; -updateShortSentMessage#11f1331c flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector = Updates; - -photos.photos#8dca6aa5 photos:Vector users:Vector = photos.Photos; -photos.photosSlice#15051f54 count:int photos:Vector users:Vector = photos.Photos; - -photos.photo#20212ca8 photo:Photo users:Vector = photos.Photo; - -upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File; -upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector = upload.File; - -dcOption#18b7a10d flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true id:int ip_address:string port:int secret:flags.10?bytes = DcOption; - -config#330b4067 flags:# phonecalls_enabled:flags.1?true default_p2p_contacts:flags.3?true preload_featured_stickers:flags.4?true ignore_phone_entities:flags.5?true revoke_pm_inbox:flags.6?true blocked_mode:flags.8?true pfs_enabled:flags.13?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector dc_txt_domain_name:string chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int revoke_time_limit:int revoke_pm_time_limit:int rating_e_decay:int stickers_recent_limit:int stickers_faved_limit:int channels_read_media_period:int tmp_sessions:flags.0?int pinned_dialogs_count_max:int pinned_infolder_count_max:int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string 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 = Config; - -nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; - -help.appUpdate#1da7158f flags:# popup:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string = help.AppUpdate; -help.noAppUpdate#c45a6536 = help.AppUpdate; - -help.inviteText#18cb9f78 message:string = help.InviteText; - -encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat; -encryptedChatWaiting#3bf703dc id:int access_hash:long date:int admin_id:int participant_id:int = EncryptedChat; -encryptedChatRequested#c878527e id:int access_hash:long date:int admin_id:int participant_id:int g_a:bytes = EncryptedChat; -encryptedChat#fa56ce36 id:int access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long = EncryptedChat; -encryptedChatDiscarded#13d6dd27 id:int = EncryptedChat; - -inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat; - -encryptedFileEmpty#c21f497e = EncryptedFile; -encryptedFile#4a70994c id:long access_hash:long size:int dc_id:int key_fingerprint:int = EncryptedFile; - -inputEncryptedFileEmpty#1837c364 = InputEncryptedFile; -inputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile; -inputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile; -inputEncryptedFileBigUploaded#2dc173c8 id:long parts:int key_fingerprint:int = InputEncryptedFile; - -encryptedMessage#ed18c118 random_id:long chat_id:int date:int bytes:bytes file:EncryptedFile = EncryptedMessage; -encryptedMessageService#23734b06 random_id:long chat_id:int date:int bytes:bytes = EncryptedMessage; - -messages.dhConfigNotModified#c0e24635 random:bytes = messages.DhConfig; -messages.dhConfig#2c221edd g:int p:bytes version:int random:bytes = messages.DhConfig; - -messages.sentEncryptedMessage#560f8935 date:int = messages.SentEncryptedMessage; -messages.sentEncryptedFile#9493ff32 date:int file:EncryptedFile = messages.SentEncryptedMessage; - -inputDocumentEmpty#72f0eaae = InputDocument; -inputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument; - -documentEmpty#36f8c871 id:long = Document; -document#9ba29cc1 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:int thumbs:flags.0?Vector dc_id:int attributes:Vector = Document; - -help.support#17c6b5f6 phone_number:string user:User = help.Support; - -notifyPeer#9fd40bd8 peer:Peer = NotifyPeer; -notifyUsers#b4c83b4c = NotifyPeer; -notifyChats#c007cec3 = NotifyPeer; -notifyBroadcasts#d612e8ef = NotifyPeer; - -sendMessageTypingAction#16bf744e = SendMessageAction; -sendMessageCancelAction#fd5ec8f5 = SendMessageAction; -sendMessageRecordVideoAction#a187d66f = SendMessageAction; -sendMessageUploadVideoAction#e9763aec progress:int = SendMessageAction; -sendMessageRecordAudioAction#d52f73f7 = SendMessageAction; -sendMessageUploadAudioAction#f351d7ab progress:int = SendMessageAction; -sendMessageUploadPhotoAction#d1d34a26 progress:int = SendMessageAction; -sendMessageUploadDocumentAction#aa0cd9e4 progress:int = SendMessageAction; -sendMessageGeoLocationAction#176f8ba1 = SendMessageAction; -sendMessageChooseContactAction#628cbc6f = SendMessageAction; -sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; -sendMessageRecordRoundAction#88f27fbc = SendMessageAction; -sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; - -contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; - -inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey; -inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey; -inputPrivacyKeyPhoneCall#fabadc5f = InputPrivacyKey; -inputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey; -inputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey; -inputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey; -inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; - -privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; -privacyKeyChatInvite#500e6dfa = PrivacyKey; -privacyKeyPhoneCall#3d662b7b = PrivacyKey; -privacyKeyPhoneP2P#39491cc8 = PrivacyKey; -privacyKeyForwards#69ec56a3 = PrivacyKey; -privacyKeyProfilePhoto#96151fed = PrivacyKey; -privacyKeyPhoneNumber#d19ae46d = PrivacyKey; - -inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; -inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; -inputPrivacyValueAllowUsers#131cc67f users:Vector = InputPrivacyRule; -inputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule; -inputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule; -inputPrivacyValueDisallowUsers#90110467 users:Vector = InputPrivacyRule; -inputPrivacyValueAllowChatParticipants#4c81c1ba chats:Vector = InputPrivacyRule; -inputPrivacyValueDisallowChatParticipants#d82363af chats:Vector = InputPrivacyRule; - -privacyValueAllowContacts#fffe1bac = PrivacyRule; -privacyValueAllowAll#65427b82 = PrivacyRule; -privacyValueAllowUsers#4d5bbe0c users:Vector = PrivacyRule; -privacyValueDisallowContacts#f888fa1a = PrivacyRule; -privacyValueDisallowAll#8b73e763 = PrivacyRule; -privacyValueDisallowUsers#c7f49b7 users:Vector = PrivacyRule; -privacyValueAllowChatParticipants#18be796b chats:Vector = PrivacyRule; -privacyValueDisallowChatParticipants#acae0690 chats:Vector = PrivacyRule; - -account.privacyRules#50a04e45 rules:Vector chats:Vector users:Vector = account.PrivacyRules; - -accountDaysTTL#b8d0afdf days:int = AccountDaysTTL; - -documentAttributeImageSize#6c37c15c w:int h:int = DocumentAttribute; -documentAttributeAnimated#11b58939 = DocumentAttribute; -documentAttributeSticker#6319d612 flags:# mask:flags.1?true alt:string stickerset:InputStickerSet mask_coords:flags.0?MaskCoords = DocumentAttribute; -documentAttributeVideo#ef02ce6 flags:# round_message:flags.0?true supports_streaming:flags.1?true duration:int w:int h:int = DocumentAttribute; -documentAttributeAudio#9852f9c6 flags:# voice:flags.10?true duration:int title:flags.0?string performer:flags.1?string waveform:flags.2?bytes = DocumentAttribute; -documentAttributeFilename#15590068 file_name:string = DocumentAttribute; -documentAttributeHasStickers#9801d2f7 = DocumentAttribute; - -messages.stickersNotModified#f1749a22 = messages.Stickers; -messages.stickers#e4599bbd hash:int stickers:Vector = messages.Stickers; - -stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack; - -messages.allStickersNotModified#e86602c3 = messages.AllStickers; -messages.allStickers#edfd405f hash:int sets:Vector = messages.AllStickers; - -messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages; - -webPageEmpty#eb1477e8 id:long = WebPage; -webPagePending#c586da1c id:long date:int = WebPage; -webPage#5f07b4bc flags:# 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 = WebPage; -webPageNotModified#85849473 = WebPage; - -authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; - -account.authorizations#1250abde authorizations:Vector = account.Authorizations; - -account.password#ad2641f8 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 = account.Password; - -account.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings; - -account.passwordInputSettings#c23727c9 flags:# new_algo:flags.0?PasswordKdfAlgo new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string new_secure_settings:flags.2?SecureSecretSettings = account.PasswordInputSettings; - -auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; - -receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; - -chatInviteEmpty#69df3769 = ExportedChatInvite; -chatInviteExported#fc2e05bc link:string = ExportedChatInvite; - -chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#dfc2f58e flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true title:string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; - -inputStickerSetEmpty#ffb62b95 = InputStickerSet; -inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; -inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; - -stickerSet#eeb46f27 flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumb:flags.4?PhotoSize thumb_dc_id:flags.4?int count:int hash:int = StickerSet; - -messages.stickerSet#b60a24a6 set:StickerSet packs:Vector documents:Vector = messages.StickerSet; - -botCommand#c27ac8c7 command:string description:string = BotCommand; - -botInfo#98e81d3a user_id:int description:string commands:Vector = BotInfo; - -keyboardButton#a2fa4880 text:string = KeyboardButton; -keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; -keyboardButtonCallback#683a5e46 text:string data:bytes = KeyboardButton; -keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton; -keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton; -keyboardButtonSwitchInline#568a748 flags:# same_peer:flags.0?true text:string query:string = KeyboardButton; -keyboardButtonGame#50f41ccf text:string = KeyboardButton; -keyboardButtonBuy#afd93fbb text:string = KeyboardButton; -keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; -inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; - -keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; - -replyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup; -replyKeyboardForceReply#f4108aa0 flags:# single_use:flags.1?true selective:flags.2?true = ReplyMarkup; -replyKeyboardMarkup#3502758c flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true rows:Vector = ReplyMarkup; -replyInlineMarkup#48a30254 rows:Vector = ReplyMarkup; - -messageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity; -messageEntityMention#fa04579d offset:int length:int = MessageEntity; -messageEntityHashtag#6f635b0d offset:int length:int = MessageEntity; -messageEntityBotCommand#6cef8ac7 offset:int length:int = MessageEntity; -messageEntityUrl#6ed02538 offset:int length:int = MessageEntity; -messageEntityEmail#64e475c2 offset:int length:int = MessageEntity; -messageEntityBold#bd610bc9 offset:int length:int = MessageEntity; -messageEntityItalic#826f8b60 offset:int length:int = MessageEntity; -messageEntityCode#28a20571 offset:int length:int = MessageEntity; -messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity; -messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity; -messageEntityMentionName#352dca58 offset:int length:int user_id:int = MessageEntity; -inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity; -messageEntityPhone#9b69e34b offset:int length:int = MessageEntity; -messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity; -messageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity; -messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; - -inputChannelEmpty#ee8c1e86 = InputChannel; -inputChannel#afeb712e channel_id:int access_hash:long = InputChannel; -inputChannelFromMessage#2a286531 peer:InputPeer msg_id:int channel_id:int = InputChannel; - -contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer; - -messageRange#ae30253 min_id:int max_id:int = MessageRange; - -updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference; -updates.channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int dialog:Dialog messages:Vector chats:Vector users:Vector = updates.ChannelDifference; -updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int new_messages:Vector other_updates:Vector chats:Vector users:Vector = updates.ChannelDifference; - -channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; -channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; - -channelParticipant#15ebac1d user_id:int date:int = ChannelParticipant; -channelParticipantSelf#a3289a6d user_id:int inviter_id:int date:int = ChannelParticipant; -channelParticipantCreator#e3e2e1f9 user_id:int = ChannelParticipant; -channelParticipantAdmin#5daa6e23 flags:# can_edit:flags.0?true self:flags.1?true user_id:int inviter_id:flags.1?int promoted_by:int date:int admin_rights:ChatAdminRights = ChannelParticipant; -channelParticipantBanned#1c0facaf flags:# left:flags.0?true user_id:int kicked_by:int date:int banned_rights:ChatBannedRights = ChannelParticipant; - -channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; -channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter; -channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter; -channelParticipantsBots#b0d1865b = ChannelParticipantsFilter; -channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; -channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; -channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter; - -channels.channelParticipants#f56ee2a8 count:int participants:Vector users:Vector = channels.ChannelParticipants; -channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; - -channels.channelParticipant#d0d9b163 participant:ChannelParticipant users:Vector = channels.ChannelParticipant; - -help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector min_age_confirm:flags.1?int = help.TermsOfService; - -foundGif#162ecc1f url:string thumb_url:string content_url:string content_type:string w:int h:int = FoundGif; -foundGifCached#9c750409 url:string photo:Photo document:Document = FoundGif; - -messages.foundGifs#450a1c0a next_offset:int results:Vector = messages.FoundGifs; - -messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; -messages.savedGifs#2e0709a5 hash:int gifs:Vector = messages.SavedGifs; - -inputBotInlineMessageMediaAuto#3380c786 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageText#3dcd7a87 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaGeo#c1b15d65 flags:# geo_point:InputGeoPoint period:int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; - -inputBotInlineResult#88bf9319 flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?InputWebDocument content:flags.5?InputWebDocument send_message:InputBotInlineMessage = InputBotInlineResult; -inputBotInlineResultPhoto#a8d864a7 id:string type:string photo:InputPhoto send_message:InputBotInlineMessage = InputBotInlineResult; -inputBotInlineResultDocument#fff8fdc4 flags:# id:string type:string title:flags.1?string description:flags.2?string document:InputDocument send_message:InputBotInlineMessage = InputBotInlineResult; -inputBotInlineResultGame#4fa417f2 id:string short_name:string send_message:InputBotInlineMessage = InputBotInlineResult; - -botInlineMessageMediaAuto#764cf810 flags:# message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageText#8c7f65e2 flags:# no_webpage:flags.0?true message:string entities:flags.1?Vector reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaGeo#b722de65 flags:# geo:GeoPoint period:int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; - -botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult; -botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; - -messages.botResults#947ca848 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM results:Vector cache_time:int users:Vector = messages.BotResults; - -exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink; - -messageFwdHeader#ec338270 flags:# from_id:flags.0?int from_name:flags.5?string date:int channel_id:flags.1?int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int = MessageFwdHeader; - -auth.codeTypeSms#72a3158c = auth.CodeType; -auth.codeTypeCall#741cd3e3 = auth.CodeType; -auth.codeTypeFlashCall#226ccefb = auth.CodeType; - -auth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType; -auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; -auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; -auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; - -messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; - -messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; - -inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID; - -inlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM; - -messages.peerDialogs#3371c354 dialogs:Vector messages:Vector chats:Vector users:Vector state:updates.State = messages.PeerDialogs; - -topPeer#edcdc05b peer:Peer rating:double = TopPeer; - -topPeerCategoryBotsPM#ab661b5b = TopPeerCategory; -topPeerCategoryBotsInline#148677e2 = TopPeerCategory; -topPeerCategoryCorrespondents#637b7ed = TopPeerCategory; -topPeerCategoryGroups#bd17a14a = TopPeerCategory; -topPeerCategoryChannels#161d9628 = TopPeerCategory; -topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory; -topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory; -topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory; - -topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector = TopPeerCategoryPeers; - -contacts.topPeersNotModified#de266ef5 = contacts.TopPeers; -contacts.topPeers#70b772a8 categories:Vector chats:Vector users:Vector = contacts.TopPeers; -contacts.topPeersDisabled#b52c939d = contacts.TopPeers; - -draftMessageEmpty#1b0c841a flags:# date:flags.0?int = DraftMessage; -draftMessage#fd8e711f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int message:string entities:flags.3?Vector date:int = DraftMessage; - -messages.featuredStickersNotModified#4ede3cf = messages.FeaturedStickers; -messages.featuredStickers#f89d88e5 hash:int sets:Vector unread:Vector = messages.FeaturedStickers; - -messages.recentStickersNotModified#b17f890 = messages.RecentStickers; -messages.recentStickers#22f3afb3 hash:int packs:Vector stickers:Vector dates:Vector = messages.RecentStickers; - -messages.archivedStickers#4fcba9c8 count:int sets:Vector = messages.ArchivedStickers; - -messages.stickerSetInstallResultSuccess#38641628 = messages.StickerSetInstallResult; -messages.stickerSetInstallResultArchive#35e410a8 sets:Vector = messages.StickerSetInstallResult; - -stickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered; -stickerSetMultiCovered#3407e51b set:StickerSet covers:Vector = StickerSetCovered; - -maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords; - -inputStickeredMediaPhoto#4a992157 id:InputPhoto = InputStickeredMedia; -inputStickeredMediaDocument#438865b id:InputDocument = InputStickeredMedia; - -game#bdf9653b flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document = Game; - -inputGameID#32c3e77 id:long access_hash:long = InputGame; -inputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame; - -highScore#58fffcd0 pos:int user_id:int score:int = HighScore; - -messages.highScores#9a3bfd99 scores:Vector users:Vector = messages.HighScores; - -textEmpty#dc3d824f = RichText; -textPlain#744694e0 text:string = RichText; -textBold#6724abc4 text:RichText = RichText; -textItalic#d912a59c text:RichText = RichText; -textUnderline#c12622c4 text:RichText = RichText; -textStrike#9bf8bb95 text:RichText = RichText; -textFixed#6c3f19b9 text:RichText = RichText; -textUrl#3c2884c1 text:RichText url:string webpage_id:long = RichText; -textEmail#de5a0dd6 text:RichText email:string = RichText; -textConcat#7e6260d7 texts:Vector = RichText; -textSubscript#ed6a8504 text:RichText = RichText; -textSuperscript#c7fb5e01 text:RichText = RichText; -textMarked#34b8621 text:RichText = RichText; -textPhone#1ccb966a text:RichText phone:string = RichText; -textImage#81ccf4f document_id:long w:int h:int = RichText; -textAnchor#35553762 text:RichText name:string = RichText; - -pageBlockUnsupported#13567e8a = PageBlock; -pageBlockTitle#70abc3fd text:RichText = PageBlock; -pageBlockSubtitle#8ffa9a1f text:RichText = PageBlock; -pageBlockAuthorDate#baafe5e0 author:RichText published_date:int = PageBlock; -pageBlockHeader#bfd064ec text:RichText = PageBlock; -pageBlockSubheader#f12bb6e1 text:RichText = PageBlock; -pageBlockParagraph#467a0766 text:RichText = PageBlock; -pageBlockPreformatted#c070d93e text:RichText language:string = PageBlock; -pageBlockFooter#48870999 text:RichText = PageBlock; -pageBlockDivider#db20b188 = PageBlock; -pageBlockAnchor#ce0d37b0 name:string = PageBlock; -pageBlockList#e4e88011 items:Vector = PageBlock; -pageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock; -pageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock; -pageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock; -pageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock; -pageBlockCover#39f23300 cover:PageBlock = PageBlock; -pageBlockEmbed#a8718dc5 flags:# full_width:flags.0?true allow_scrolling:flags.3?true url:flags.1?string html:flags.2?string poster_photo_id:flags.4?long w:flags.5?int h:flags.5?int caption:PageCaption = PageBlock; -pageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector caption:PageCaption = PageBlock; -pageBlockCollage#65a0fa4d items:Vector caption:PageCaption = PageBlock; -pageBlockSlideshow#31f9590 items:Vector caption:PageCaption = PageBlock; -pageBlockChannel#ef1751b5 channel:Chat = PageBlock; -pageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock; -pageBlockKicker#1e148390 text:RichText = PageBlock; -pageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector = PageBlock; -pageBlockOrderedList#9a8ae1e1 items:Vector = PageBlock; -pageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector title:RichText = PageBlock; -pageBlockRelatedArticles#16115a96 title:RichText articles:Vector = PageBlock; -pageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock; - -phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason; -phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason; -phoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason; -phoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason; - -dataJSON#7d748d04 data:string = DataJSON; - -labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; - -invoice#c30aa358 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 currency:string prices:Vector = Invoice; - -paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; - -postAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress; - -paymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo; - -paymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials; - -webDocument#1c570ed1 url:string access_hash:long size:int mime_type:string attributes:Vector = WebDocument; -webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector = WebDocument; - -inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector = InputWebDocument; - -inputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation; -inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w:int h:int zoom:int scale:int = InputWebFileLocation; - -upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; - -payments.paymentForm#3f56aea3 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true bot_id:int invoice:Invoice provider_id:int url:string native_provider:flags.4?string native_params:flags.4?DataJSON saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?PaymentSavedCredentials users:Vector = payments.PaymentForm; - -payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; - -payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; -payments.paymentVerficationNeeded#6b56b921 url:string = payments.PaymentResult; - -payments.paymentReceipt#500911e1 flags:# date:int bot_id:int invoice:Invoice provider_id:int info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; - -payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; - -inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; -inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; -inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials; -inputPaymentCredentialsAndroidPay#ca05d50e payment_token:DataJSON google_transaction_id:string = InputPaymentCredentials; - -account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword; - -shippingOption#b6213cdf id:string title:string prices:Vector = ShippingOption; - -inputStickerSetItem#ffa0a496 flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords = InputStickerSetItem; - -inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall; - -phoneCallEmpty#5366c915 id:long = PhoneCall; -phoneCallWaiting#1b8f4ad1 flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall; -phoneCallRequested#87eabb53 flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCallAccepted#997c454a flags:# video:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_b:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCall#8742ae7f flags:# p2p_allowed:flags.5?true id:long access_hash:long date:int admin_id:int participant_id:int g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int = PhoneCall; -phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.5?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall; - -phoneConnection#9d4c17c0 id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection; - -phoneCallProtocol#a2bb35cb flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int = PhoneCallProtocol; - -phone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector = phone.PhoneCall; - -upload.cdnFileReuploadNeeded#eea8e46e request_token:bytes = upload.CdnFile; -upload.cdnFile#a99fca4f bytes:bytes = upload.CdnFile; - -cdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey; - -cdnConfig#5725e40a public_keys:Vector = CdnConfig; - -langPackString#cad181f6 key:string value:string = LangPackString; -langPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString; -langPackStringDeleted#2979eeb2 key:string = LangPackString; - -langPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector = LangPackDifference; - -langPackLanguage#eeca5ce3 flags:# official:flags.0?true rtl:flags.2?true beta:flags.3?true name:string native_name:string lang_code:string base_lang_code:flags.1?string plural_code:string strings_count:int translated_count:int translations_url:string = LangPackLanguage; - -channelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangePhoto#434bd2af prev_photo:Photo new_photo:Photo = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; -channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction; -channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeLinkedChat#a26f881b prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; - -channelAdminLogEvent#3b5a3e40 id:long date:int user_id:int action:ChannelAdminLogEventAction = ChannelAdminLogEvent; - -channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; - -channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true = ChannelAdminLogEventsFilter; - -popularContact#5ce14175 client_id:long importers:int = PopularContact; - -messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; -messages.favedStickers#f37f2f16 hash:int packs:Vector stickers:Vector = messages.FavedStickers; - -recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; -recentMeUrlUser#8dbc3336 url:string user_id:int = RecentMeUrl; -recentMeUrlChat#a01b22f9 url:string chat_id:int = RecentMeUrl; -recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; -recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; - -help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; - -inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; - -webAuthorization#cac943f2 hash:long bot_id:int domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; - -account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; - -inputMessageID#a676a322 id:int = InputMessage; -inputMessageReplyTo#bad88395 id:int = InputMessage; -inputMessagePinned#86872538 = InputMessage; - -inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer; -inputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer; - -dialogPeer#e56dbf05 peer:Peer = DialogPeer; -dialogPeerFolder#514519e2 folder_id:int = DialogPeer; - -messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; -messages.foundStickerSets#5108d648 hash:int sets:Vector = messages.FoundStickerSets; - -fileHash#6242c773 offset:int limit:int hash:bytes = FileHash; - -inputClientProxy#75588b3f address:string port:int = InputClientProxy; - -help.proxyDataEmpty#e09e1fb8 expires:int = help.ProxyData; -help.proxyDataPromo#2bf7ee23 expires:int peer:Peer chats:Vector users:Vector = help.ProxyData; - -help.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate; -help.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate; - -inputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile; -inputSecureFile#5367e5be id:long access_hash:long = InputSecureFile; - -secureFileEmpty#64199744 = SecureFile; -secureFile#e0277a62 id:long access_hash:long size:int dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; - -secureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData; - -securePlainPhone#7d6099dd phone:string = SecurePlainData; -securePlainEmail#21ec5a5f email:string = SecurePlainData; - -secureValueTypePersonalDetails#9d2a81e3 = SecureValueType; -secureValueTypePassport#3dac6a00 = SecureValueType; -secureValueTypeDriverLicense#6e425c4 = SecureValueType; -secureValueTypeIdentityCard#a0d0744b = SecureValueType; -secureValueTypeInternalPassport#99a48f23 = SecureValueType; -secureValueTypeAddress#cbe31e26 = SecureValueType; -secureValueTypeUtilityBill#fc36954e = SecureValueType; -secureValueTypeBankStatement#89137c0d = SecureValueType; -secureValueTypeRentalAgreement#8b883488 = SecureValueType; -secureValueTypePassportRegistration#99e3806a = SecureValueType; -secureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType; -secureValueTypePhone#b320aadb = SecureValueType; -secureValueTypeEmail#8e3ca7ee = SecureValueType; - -secureValue#187fa0ca flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData hash:bytes = SecureValue; - -inputSecureValue#db21d0a7 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData = InputSecureValue; - -secureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash; - -secureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError; -secureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector text:string = SecureValueError; -secureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError; -secureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector text:string = SecureValueError; - -secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted; - -account.authorizationForm#ad2e1cd8 flags:# required_types:Vector values:Vector errors:Vector users:Vector privacy_policy_url:flags.0?string = account.AuthorizationForm; - -account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode; - -help.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo; -help.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector = help.DeepLinkInfo; - -savedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact; - -account.takeout#4dba4501 id:long = account.Takeout; - -passwordKdfAlgoUnknown#d45ab096 = PasswordKdfAlgo; -passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow#3a912d4a salt1:bytes salt2:bytes g:int p:bytes = PasswordKdfAlgo; - -securePasswordKdfAlgoUnknown#4a8537 = SecurePasswordKdfAlgo; -securePasswordKdfAlgoPBKDF2HMACSHA512iter100000#bbf2dda0 salt:bytes = SecurePasswordKdfAlgo; -securePasswordKdfAlgoSHA512#86471d92 salt:bytes = SecurePasswordKdfAlgo; - -secureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:bytes secure_secret_id:long = SecureSecretSettings; - -inputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP; -inputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP; - -secureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType; -secureRequiredTypeOneOf#27477b4 types:Vector = SecureRequiredType; - -help.passportConfigNotModified#bfb9f457 = help.PassportConfig; -help.passportConfig#a098d6af hash:int countries_langs:DataJSON = help.PassportConfig; - -inputAppEvent#1d1b1245 time:double type:string peer:long data:JSONValue = InputAppEvent; - -jsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue; - -jsonNull#3f6d7b68 = JSONValue; -jsonBool#c7345e6a value:Bool = JSONValue; -jsonNumber#2be0dfa4 value:double = JSONValue; -jsonString#b71e767a value:string = JSONValue; -jsonArray#f7444763 value:Vector = JSONValue; -jsonObject#99c1d49d value:Vector = JSONValue; - -pageTableCell#34566b6a flags:# header:flags.0?true align_center:flags.3?true align_right:flags.4?true valign_middle:flags.5?true valign_bottom:flags.6?true text:flags.7?RichText colspan:flags.1?int rowspan:flags.2?int = PageTableCell; - -pageTableRow#e0c0c5e5 cells:Vector = PageTableRow; - -pageCaption#6f747657 text:RichText credit:RichText = PageCaption; - -pageListItemText#b92fb6cd text:RichText = PageListItem; -pageListItemBlocks#25e073fc blocks:Vector = PageListItem; - -pageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem; -pageListOrderedItemBlocks#98dd8936 num:string blocks:Vector = PageListOrderedItem; - -pageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle; - -page#ae891bec flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector photos:Vector documents:Vector = Page; - -help.supportName#8c05f1c9 name:string = help.SupportName; - -help.userInfoEmpty#f3ae2eed = help.UserInfo; -help.userInfo#1eb3758 message:string entities:Vector author:string date:int = help.UserInfo; - -pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer; - -poll#d5529d06 id:long flags:# closed:flags.0?true question:string answers:Vector = Poll; - -pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true option:bytes voters:int = PollAnswerVoters; - -pollResults#5755785a flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int = PollResults; - -chatOnlines#f041e250 onlines:int = ChatOnlines; - -statsURL#47a971e0 url:string = StatsURL; - -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true = ChatAdminRights; - -chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true until_date:int = ChatBannedRights; - -inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; -inputWallPaperSlug#72091c80 slug:string = InputWallPaper; - -account.wallPapersNotModified#1c199183 = account.WallPapers; -account.wallPapers#702b65a9 hash:int wallpapers:Vector = account.WallPapers; - -codeSettings#302f59f3 flags:# allow_flashcall:flags.0?true current_number:flags.1?true app_hash_persistent:flags.2?true app_hash:flags.3?string = CodeSettings; - -wallPaperSettings#a12f40b8 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int intensity:flags.3?int = WallPaperSettings; - -autoDownloadSettings#d246fd47 flags:# disabled:flags.0?true video_preload_large:flags.1?true audio_preload_next:flags.2?true phonecalls_less_data:flags.3?true photo_size_max:int video_size_max:int file_size_max:int = AutoDownloadSettings; - -account.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings; - -emojiKeyword#d5b3b9f9 keyword:string emoticons:Vector = EmojiKeyword; -emojiKeywordDeleted#236df622 keyword:string emoticons:Vector = EmojiKeyword; - -emojiKeywordsDifference#5cc761bd lang_code:string from_version:int version:int keywords:Vector = EmojiKeywordsDifference; - -emojiURL#a575739d url:string = EmojiURL; - -emojiLanguage#b3fb5361 lang_code:string = EmojiLanguage; - -fileLocationToBeDeprecated#bc7fc6cd volume_id:long local_id:int = FileLocation; - -folder#ff544e65 flags:# autofill_new_broadcasts:flags.0?true autofill_public_groups:flags.1?true autofill_new_correspondents:flags.2?true id:int title:string photo:flags.3?ChatPhoto = Folder; - -inputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer; - -folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; - -messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; - -urlAuthResultRequest#92d33a0e flags:# request_write_access:flags.0?true bot:User domain:string = UrlAuthResult; -urlAuthResultAccepted#8f8c0e4e url:string = UrlAuthResult; -urlAuthResultDefault#a9d6db1f = UrlAuthResult; - -channelLocationEmpty#bfb5ad8b = ChannelLocation; -channelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation; - -peerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated; - ----functions--- - -invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; -invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector query:!X = X; -initConnection#785188b8 {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 query:!X = X; -invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; -invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; -invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; -invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; - -auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; -auth.signUp#1b067634 phone_number:string phone_code_hash:string phone_code:string first_name:string last_name:string = auth.Authorization; -auth.signIn#bcd51581 phone_number:string phone_code_hash:string phone_code:string = auth.Authorization; -auth.logOut#5717da40 = Bool; -auth.resetAuthorizations#9fab0d1a = Bool; -auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization; -auth.importAuthorization#e3ef9613 id:int bytes:bytes = auth.Authorization; -auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool; -auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization; -auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; -auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; -auth.recoverPassword#4ea56e92 code:string = auth.Authorization; -auth.resendCode#3ef1a9bf phone_number:string phone_code_hash:string = auth.SentCode; -auth.cancelCode#1f040578 phone_number:string phone_code_hash:string = Bool; -auth.dropTempAuthKeys#8e48a188 except_auth_keys:Vector = Bool; - -account.registerDevice#5cbea590 token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; -account.unregisterDevice#3076c4bf token_type:int token:string other_uids:Vector = Bool; -account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; -account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; -account.resetNotifySettings#db7e1747 = Bool; -account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User; -account.updateStatus#6628562c offline:Bool = Bool; -account.getWallPapers#aabb1763 hash:int = account.WallPapers; -account.reportPeer#ae189d5f peer:InputPeer reason:ReportReason = Bool; -account.checkUsername#2714d86c username:string = Bool; -account.updateUsername#3e0bdd7c username:string = User; -account.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules; -account.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector = account.PrivacyRules; -account.deleteAccount#418d4e0b reason:string = Bool; -account.getAccountTTL#8fc711d = AccountDaysTTL; -account.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool; -account.sendChangePhoneCode#82574ae5 phone_number:string settings:CodeSettings = auth.SentCode; -account.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User; -account.updateDeviceLocked#38df3532 period:int = Bool; -account.getAuthorizations#e320c158 = account.Authorizations; -account.resetAuthorization#df77f3bc hash:long = Bool; -account.getPassword#548a30f5 = account.Password; -account.getPasswordSettings#9cd4eaf9 password:InputCheckPasswordSRP = account.PasswordSettings; -account.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_settings:account.PasswordInputSettings = Bool; -account.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode; -account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool; -account.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword; -account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; -account.resetWebAuthorization#2d01b9ef hash:long = Bool; -account.resetWebAuthorizations#682d2594 = Bool; -account.getAllSecureValues#b288bc7d = Vector; -account.getSecureValue#73665bc2 types:Vector = Vector; -account.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue; -account.deleteSecureValue#b880bc4b types:Vector = Bool; -account.getAuthorizationForm#b86ba8e1 bot_id:int scope:string public_key:string = account.AuthorizationForm; -account.acceptAuthorization#e7027c94 bot_id:int scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; -account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode; -account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; -account.sendVerifyEmailCode#7011509f email:string = account.SentEmailCode; -account.verifyEmail#ecba39db email:string code:string = Bool; -account.initTakeoutSession#f05b4804 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?int = account.Takeout; -account.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool; -account.confirmPasswordEmail#8fdf1920 code:string = Bool; -account.resendPasswordEmail#7a7f2a15 = Bool; -account.cancelPasswordEmail#c1cbd5b6 = Bool; -account.getContactSignUpNotification#9f07c728 = Bool; -account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; -account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true peer:flags.0?InputNotifyPeer = Updates; -account.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper; -account.uploadWallPaper#dd853661 file:InputFile mime_type:string settings:WallPaperSettings = WallPaper; -account.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool; -account.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSettings = Bool; -account.resetWallPapers#bb3b9804 = Bool; -account.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings; -account.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool; - -users.getUsers#d91a548 id:Vector = Vector; -users.getFullUser#ca30a5b1 id:InputUser = UserFull; -users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; - -contacts.getContactIDs#2caa4a42 hash:int = Vector; -contacts.getStatuses#c4a353ee = Vector; -contacts.getContacts#c023849f hash:int = contacts.Contacts; -contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; -contacts.deleteContacts#96a0e00 id:Vector = Updates; -contacts.deleteByPhones#1013fd9e phones:Vector = Bool; -contacts.block#332b49fc id:InputUser = Bool; -contacts.unblock#e54100bd id:InputUser = Bool; -contacts.getBlocked#f57c350f offset:int limit:int = contacts.Blocked; -contacts.search#11f812d8 q:string limit:int = contacts.Found; -contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; -contacts.getTopPeers#d4982db5 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 offset:int limit:int hash:int = contacts.TopPeers; -contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; -contacts.resetSaved#879537f1 = Bool; -contacts.getSaved#82f1e39f = Vector; -contacts.toggleTopPeers#8514bdda enabled:Bool = Bool; -contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates; -contacts.acceptContact#f831a20f id:InputUser = Updates; -contacts.getLocated#a356056 geo_point:InputGeoPoint = Updates; - -messages.getMessages#63c66506 id:Vector = messages.Messages; -messages.getDialogs#a0ee3b73 flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:int = messages.Dialogs; -messages.getHistory#dcbb8260 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; -messages.search#8614ef68 flags:# peer:InputPeer q:string from_id:flags.0?InputUser filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:int = messages.Messages; -messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; -messages.deleteHistory#1c015b09 flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int = messages.AffectedHistory; -messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; -messages.receivedMessages#5a954c0 max_id:int = Vector; -messages.setTyping#a3825e50 peer:InputPeer action:SendMessageAction = Bool; -messages.sendMessage#fa88427a flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.sendMedia#b8d1262b flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.forwardMessages#708e0195 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true grouped:flags.9?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer = Updates; -messages.reportSpam#cf1592db peer:InputPeer = Bool; -messages.getPeerSettings#3672e09c peer:InputPeer = PeerSettings; -messages.report#bd82b658 peer:InputPeer id:Vector reason:ReportReason = Bool; -messages.getChats#3c6aa187 id:Vector = messages.Chats; -messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull; -messages.editChatTitle#dc452855 chat_id:int title:string = Updates; -messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates; -messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates; -messages.deleteChatUser#e0611f16 chat_id:int user_id:InputUser = Updates; -messages.createChat#9cb126e users:Vector title:string = Updates; -messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig; -messages.requestEncryption#f64daf43 user_id:InputUser random_id:int g_a:bytes = EncryptedChat; -messages.acceptEncryption#3dbc0415 peer:InputEncryptedChat g_b:bytes key_fingerprint:long = EncryptedChat; -messages.discardEncryption#edd923c5 chat_id:int = Bool; -messages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool; -messages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool; -messages.sendEncrypted#a9776773 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; -messages.sendEncryptedFile#9a901b66 peer:InputEncryptedChat random_id:long data:bytes file:InputEncryptedFile = messages.SentEncryptedMessage; -messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; -messages.receivedQueue#55a5bb66 max_qts:int = Vector; -messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; -messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; -messages.getStickers#43d4f2c emoticon:string hash:int = messages.Stickers; -messages.getAllStickers#1c9618b1 hash:int = messages.AllStickers; -messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; -messages.exportChatInvite#df7534c peer:InputPeer = ExportedChatInvite; -messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; -messages.importChatInvite#6c50051c hash:string = Updates; -messages.getStickerSet#2619a90e stickerset:InputStickerSet = messages.StickerSet; -messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; -messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; -messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; -messages.getMessagesViews#c4c8a55d peer:InputPeer id:Vector increment:Bool = Vector; -messages.editChatAdmin#a9e69f2e chat_id:int user_id:InputUser is_admin:Bool = Bool; -messages.migrateChat#15a3b8e3 chat_id:int = Updates; -messages.searchGlobal#bf7225a4 flags:# folder_id:flags.0?int q:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; -messages.reorderStickerSets#78337739 flags:# masks:flags.0?true order:Vector = Bool; -messages.getDocumentByHash#338e2464 sha256:bytes size:int mime_type:string = Document; -messages.searchGifs#bf9a776b q:string offset:int = messages.FoundGifs; -messages.getSavedGifs#83bf3d52 hash:int = messages.SavedGifs; -messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; -messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; -messages.setInlineBotResults#eb5ea206 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM = Bool; -messages.sendInlineBotResult#b16e06fe flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true hide_via:flags.11?true peer:InputPeer reply_to_msg_id:flags.0?int random_id:long query_id:long id:string = Updates; -messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#d116f31e flags:# no_webpage:flags.1?true peer:InputPeer id:int message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Updates; -messages.editInlineBotMessage#83557dba flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string media:flags.14?InputMedia reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; -messages.getBotCallbackAnswer#810a9fec flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes = messages.BotCallbackAnswer; -messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; -messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; -messages.saveDraft#bc39e14b flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int peer:InputPeer message:string entities:flags.3?Vector = Bool; -messages.getAllDrafts#6a3f8d65 = Updates; -messages.getFeaturedStickers#2dacca4f hash:int = messages.FeaturedStickers; -messages.readFeaturedStickers#5b118126 id:Vector = Bool; -messages.getRecentStickers#5ea192c9 flags:# attached:flags.0?true hash:int = messages.RecentStickers; -messages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool; -messages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool; -messages.getArchivedStickers#57f17692 flags:# masks:flags.0?true offset_id:long limit:int = messages.ArchivedStickers; -messages.getMaskStickers#65b8c79f hash:int = messages.AllStickers; -messages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector; -messages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates; -messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool; -messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; -messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; -messages.getCommonChats#d0a48c4 user_id:InputUser max_id:int limit:int = messages.Chats; -messages.getAllChats#eba80ff0 except_ids:Vector = messages.Chats; -messages.getWebPage#32ca8f91 url:string hash:int = WebPage; -messages.toggleDialogPin#a731e257 flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; -messages.reorderPinnedDialogs#3b1adf37 flags:# force:flags.0?true folder_id:int order:Vector = Bool; -messages.getPinnedDialogs#d6b94df2 folder_id:int = messages.PeerDialogs; -messages.setBotShippingResults#e5f672fa flags:# query_id:long error:flags.0?string shipping_options:flags.1?Vector = Bool; -messages.setBotPrecheckoutResults#9c2dd95 flags:# success:flags.1?true query_id:long error:flags.0?string = Bool; -messages.uploadMedia#519bc2b1 peer:InputPeer media:InputMedia = MessageMedia; -messages.sendScreenshotNotification#c97df020 peer:InputPeer reply_to_msg_id:int random_id:long = Updates; -messages.getFavedStickers#21ce0b0e hash:int = messages.FavedStickers; -messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; -messages.getUnreadMentions#46578472 peer:InputPeer offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readMentions#f0189d3 peer:InputPeer = messages.AffectedHistory; -messages.getRecentLocations#bbc45b09 peer:InputPeer limit:int hash:int = messages.Messages; -messages.sendMultiMedia#2095512f flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true peer:InputPeer reply_to_msg_id:flags.0?int multi_media:Vector = Updates; -messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; -messages.searchStickerSets#c2b7d08b flags:# exclude_featured:flags.0?true q:string hash:int = messages.FoundStickerSets; -messages.getSplitRanges#1cff7e08 = Vector; -messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; -messages.getDialogUnreadMarks#22e24e22 = Vector; -messages.clearAllDrafts#7e58ee9c = Bool; -messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true peer:InputPeer id:int = Updates; -messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector = Updates; -messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates; -messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines; -messages.getStatsURL#812c2ae6 flags:# dark:flags.0?true peer:InputPeer params:string = StatsURL; -messages.editChatAbout#def60797 peer:InputPeer about:string = Bool; -messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates; -messages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference; -messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference; -messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector; -messages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL; -messages.getSearchCounters#732eef00 peer:InputPeer filters:Vector = Vector; -messages.requestUrlAuth#e33f5613 peer:InputPeer msg_id:int button_id:int = UrlAuthResult; -messages.acceptUrlAuth#f729ea98 flags:# write_allowed:flags.0?true peer:InputPeer msg_id:int button_id:int = UrlAuthResult; -messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; - -updates.getState#edd4882a = updates.State; -updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; -updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; - -photos.updateProfilePhoto#f0bb5152 id:InputPhoto = UserProfilePhoto; -photos.uploadProfilePhoto#4f32c098 file:InputFile = photos.Photo; -photos.deletePhotos#87cf7f2f id:Vector = Vector; -photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; - -upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; -upload.getFile#e3a6cfb5 location:InputFileLocation offset:int limit:int = upload.File; -upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; -upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; -upload.getCdnFile#2000bcc3 file_token:bytes offset:int limit:int = upload.CdnFile; -upload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector; -upload.getCdnFileHashes#4da54231 file_token:bytes offset:int = Vector; -upload.getFileHashes#c7025931 location:InputFileLocation offset:int = Vector; - -help.getConfig#c4f9186b = Config; -help.getNearestDc#1fb33026 = NearestDc; -help.getAppUpdate#522d5a7d source:string = help.AppUpdate; -help.getInviteText#4d392343 = help.InviteText; -help.getSupport#9cdf08cd = help.Support; -help.getAppChangelog#9010ef6f prev_app_version:string = Updates; -help.setBotUpdatesStatus#ec22cfcd pending_updates_count:int message:string = Bool; -help.getCdnConfig#52029342 = CdnConfig; -help.getRecentMeUrls#3dc0f114 referer:string = help.RecentMeUrls; -help.getProxyData#3d7758e1 = help.ProxyData; -help.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate; -help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; -help.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo; -help.getAppConfig#98914110 = JSONValue; -help.saveAppLog#6f02f748 events:Vector = Bool; -help.getPassportConfig#c661ad08 hash:int = help.PassportConfig; -help.getSupportName#d360e72c = help.SupportName; -help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo; -help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector = help.UserInfo; - -channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; -channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; -channels.deleteUserHistory#d10dd71b channel:InputChannel user_id:InputUser = messages.AffectedHistory; -channels.reportSpam#fe087810 channel:InputChannel user_id:InputUser id:Vector = Bool; -channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; -channels.getParticipants#123e05e9 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:int = channels.ChannelParticipants; -channels.getParticipant#546dd7a6 channel:InputChannel user_id:InputUser = channels.ChannelParticipant; -channels.getChannels#a7f6bbb id:Vector = messages.Chats; -channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; -channels.createChannel#3d5fb10f flags:# broadcast:flags.0?true megagroup:flags.1?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string = Updates; -channels.editAdmin#70f893ba channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights = Updates; -channels.editTitle#566decd0 channel:InputChannel title:string = Updates; -channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; -channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; -channels.updateUsername#3514b3de channel:InputChannel username:string = Bool; -channels.joinChannel#24b524c5 channel:InputChannel = Updates; -channels.leaveChannel#f836aa95 channel:InputChannel = Updates; -channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector = Updates; -channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; -channels.exportMessageLink#ceb77163 channel:InputChannel id:int grouped:Bool = ExportedMessageLink; -channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; -channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true = messages.Chats; -channels.editBanned#72796912 channel:InputChannel user_id:InputUser banned_rights:ChatBannedRights = Updates; -channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector max_id:long min_id:long limit:int = channels.AdminLogResults; -channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool; -channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = Bool; -channels.deleteHistory#af369d42 channel:InputChannel max_id:int = Bool; -channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates; -channels.getLeftChannels#8341ecc0 offset:int = messages.Chats; -channels.getGroupsForDiscussion#f5dad378 = messages.Chats; -channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; -channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; -channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; - -bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; -bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; - -payments.getPaymentForm#99f09745 msg_id:int = payments.PaymentForm; -payments.getPaymentReceipt#a092a980 msg_id:int = payments.PaymentReceipt; -payments.validateRequestedInfo#770a8e74 flags:# save:flags.0?true msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; -payments.sendPaymentForm#2b8879b3 flags:# msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials = payments.PaymentResult; -payments.getSavedInfo#227d824b = payments.SavedInfo; -payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; - -stickers.createStickerSet#9bd86e6a flags:# masks:flags.0?true user_id:InputUser title:string short_name:string stickers:Vector = messages.StickerSet; -stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; -stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; -stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; - -phone.getCallConfig#55451fa9 = DataJSON; -phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; -phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; -phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall; -phone.receivedCall#17d54f61 peer:InputPhoneCall = Bool; -phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates; -phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates; -phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool; - -langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; -langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; -langpack.getDifference#cd984aa5 lang_pack:string lang_code:string from_version:int = LangPackDifference; -langpack.getLanguages#42c6978f lang_pack:string = Vector; -langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage; - -folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; -folders.deleteFolder#1c295881 folder_id:int = Updates; - -// LAYER 102 diff --git a/telethon_generator/docswriter.py b/telethon_generator/docswriter.py index 16aff292..e69de29b 100644 --- a/telethon_generator/docswriter.py +++ b/telethon_generator/docswriter.py @@ -1,296 +0,0 @@ -import os -import re - - -class DocsWriter: - """ - Utility class used to write the HTML files used on the documentation. - """ - def __init__(self, root, filename, type_to_path): - """ - Initializes the writer to the specified output file, - creating the parent directories when used if required. - """ - self.root = root - self.filename = filename - self._parent = str(self.filename.parent) - self.handle = None - self.title = '' - - # Should be set before calling adding items to the menu - self.menu_separator_tag = None - - # Utility functions - self.type_to_path = lambda t: self._rel(type_to_path(t)) - - # Control signals - self.menu_began = False - self.table_columns = 0 - self.table_columns_left = None - self.write_copy_script = False - self._script = '' - - def _rel(self, path): - """ - Get the relative path for the given path from the current - file by working around https://bugs.python.org/issue20012. - """ - return os.path.relpath( - str(path), self._parent).replace(os.path.sep, '/') - - # High level writing - def write_head(self, title, css_path, default_css): - """Writes the head part for the generated document, - with the given title and CSS - """ - self.title = title - self.write( - ''' - - - - {title} - - - - - - -
    ''', - title=title, - rel_css=self._rel(css_path), - def_css=default_css - ) - - def set_menu_separator(self, img): - """Sets the menu separator. - Must be called before adding entries to the menu - """ - if img: - self.menu_separator_tag = '/'.format( - self._rel(img)) - else: - self.menu_separator_tag = None - - def add_menu(self, name, link=None): - """Adds a menu entry, will create it if it doesn't exist yet""" - if self.menu_began: - if self.menu_separator_tag: - self.write(self.menu_separator_tag) - else: - # First time, create the menu tag - self.write('') - - def write_title(self, title, level=1, id=None): - """Writes a title header in the document body, - with an optional depth level - """ - if id: - self.write('{title}', - title=title, lv=level, id=id) - else: - self.write('{title}', - title=title, lv=level) - - def write_code(self, tlobject): - """Writes the code for the given 'tlobject' properly - formatted with hyperlinks - """ - self.write('
    ---{}---\n',
    -                   'functions' if tlobject.is_function else 'types')
    -
    -        # Write the function or type and its ID
    -        if tlobject.namespace:
    -            self.write(tlobject.namespace)
    -            self.write('.')
    -
    -        self.write('{}#{:08x}', tlobject.name, tlobject.id)
    -
    -        # Write all the arguments (or do nothing if there's none)
    -        for arg in tlobject.args:
    -            self.write(' ')
    -            add_link = not arg.generic_definition and not arg.is_generic
    -
    -            # "Opening" modifiers
    -            if arg.generic_definition:
    -                self.write('{')
    -
    -            # Argument name
    -            self.write(arg.name)
    -            self.write(':')
    -
    -            # "Opening" modifiers
    -            if arg.is_flag:
    -                self.write('flags.{}?', arg.flag_index)
    -
    -            if arg.is_generic:
    -                self.write('!')
    -
    -            if arg.is_vector:
    -                self.write('Vector<',
    -                           self.type_to_path('vector'))
    -
    -            # Argument type
    -            if arg.type:
    -                if add_link:
    -                    self.write('', self.type_to_path(arg.type))
    -                self.write(arg.type)
    -                if add_link:
    -                    self.write('')
    -            else:
    -                self.write('#')
    -
    -            # "Closing" modifiers
    -            if arg.is_vector:
    -                self.write('>')
    -
    -            if arg.generic_definition:
    -                self.write('}')
    -
    -        # Now write the resulting type (result from a function/type)
    -        self.write(' = ')
    -        generic_name = next((arg.name for arg in tlobject.args
    -                             if arg.generic_definition), None)
    -
    -        if tlobject.result == generic_name:
    -            # Generic results cannot have any link
    -            self.write(tlobject.result)
    -        else:
    -            if re.search('^vector<', tlobject.result, re.IGNORECASE):
    -                # Notice that we don't simply make up the "Vector" part,
    -                # because some requests (as of now, only FutureSalts),
    -                # use a lower type name for it (see #81)
    -                vector, inner = tlobject.result.split('<')
    -                inner = inner.strip('>')
    -                self.write('{}<',
    -                           self.type_to_path(vector), vector)
    -
    -                self.write('{}>',
    -                           self.type_to_path(inner), inner)
    -            else:
    -                self.write('{}',
    -                           self.type_to_path(tlobject.result), tlobject.result)
    -
    -        self.write('
    ') - - def begin_table(self, column_count): - """Begins a table with the given 'column_count', required to automatically - create the right amount of columns when adding items to the rows""" - self.table_columns = column_count - self.table_columns_left = 0 - self.write('') - - def add_row(self, text, link=None, bold=False, align=None): - """This will create a new row, or add text to the next column - of the previously created, incomplete row, closing it if complete""" - if not self.table_columns_left: - # Starting a new row - self.write('') - self.table_columns_left = self.table_columns - - self.write('') - - if bold: - self.write('') - if link: - self.write('', self._rel(link)) - - # Finally write the real table data, the given text - self.write(text) - - if link: - self.write('') - if bold: - self.write('') - - self.write('') - - self.table_columns_left -= 1 - if not self.table_columns_left: - self.write('') - - def end_table(self): - # If there was any column left, finish it before closing the table - if self.table_columns_left: - self.write('') - - self.write('
    ') - - def write_text(self, text): - """Writes a paragraph of text""" - self.write('

    {}

    ', text) - - def write_copy_button(self, text, text_to_copy): - """Writes a button with 'text' which can be used - to copy 'text_to_copy' to clipboard when it's clicked.""" - self.write_copy_script = True - self.write('' - .format(text_to_copy, text)) - - def add_script(self, src='', path=None): - if path: - self._script += ''.format( - self._rel(path)) - elif src: - self._script += ''.format(src) - - def end_body(self): - """Ends the whole document. This should be called the last""" - if self.write_copy_script: - self.write( - '' - '' - ) - - self.write('
    {}', self._script) - - # "Low" level writing - def write(self, s, *args, **kwargs): - """Wrapper around handle.write""" - if args or kwargs: - self.handle.write(s.format(*args, **kwargs)) - else: - self.handle.write(s) - - # With block - def __enter__(self): - # Sanity check - self.filename.parent.mkdir(parents=True, exist_ok=True) - self.handle = self.filename.open('w', encoding='utf-8') - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.handle.close() diff --git a/telethon_generator/generators/__init__.py b/telethon_generator/generators/__init__.py index 156606e0..e69de29b 100644 --- a/telethon_generator/generators/__init__.py +++ b/telethon_generator/generators/__init__.py @@ -1,3 +0,0 @@ -from .errors import generate_errors -from .tlobject import generate_tlobjects, clean_tlobjects -from .docs import generate_docs diff --git a/telethon_generator/generators/docs.py b/telethon_generator/generators/docs.py index 89554f4a..e69de29b 100755 --- a/telethon_generator/generators/docs.py +++ b/telethon_generator/generators/docs.py @@ -1,657 +0,0 @@ -#!/usr/bin/env python3 -import functools -import os -import re -import shutil -from collections import defaultdict -from pathlib import Path - -from ..docswriter import DocsWriter -from ..parsers import TLObject, Usability -from ..utils import snake_to_camel_case - -CORE_TYPES = { - 'int', 'long', 'int128', 'int256', 'double', - 'vector', 'string', 'bool', 'true', 'bytes', 'date' -} - - -def _get_file_name(tlobject): - """``ClassName -> class_name.html``.""" - name = tlobject.name if isinstance(tlobject, TLObject) else tlobject - # Courtesy of http://stackoverflow.com/a/1176023/4759433 - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - return '{}.html'.format(result) - - -def get_import_code(tlobject): - """``TLObject -> from ... import ...``.""" - kind = 'functions' if tlobject.is_function else 'types' - ns = '.' + tlobject.namespace if tlobject.namespace else '' - return 'from telethon.tl.{}{} import {}'\ - .format(kind, ns, tlobject.class_name) - - -def _get_path_for(root, tlobject): - """Creates and returns the path for the given TLObject at root.""" - out_dir = root / ('methods' if tlobject.is_function else 'constructors') - if tlobject.namespace: - out_dir /= tlobject.namespace - - return out_dir / _get_file_name(tlobject) - - -def _get_path_for_type(type_): - """Similar to `_get_path_for` but for only type names.""" - if type_.lower() in CORE_TYPES: - return Path('index.html#%s' % type_.lower()) - elif '.' in type_: - namespace, name = type_.split('.') - return Path('types', namespace, _get_file_name(name)) - else: - return Path('types', _get_file_name(type_)) - - -def _find_title(html_file): - """Finds the for the given HTML file, or (Unknown).""" - # TODO Is it necessary to read files like this? - with html_file.open() as f: - for line in f: - if '<title>' in line: - # + 7 to skip len('<title>') - return line[line.index('<title>') + 7:line.index('')] - - return '(Unknown)' - - -def _build_menu(docs): - """ - Builds the menu used for the current ``DocumentWriter``. - """ - - paths = [] - current = docs.filename - while current != docs.root: - current = current.parent - paths.append(current) - - for path in reversed(paths): - docs.add_menu(path.stem.title(), link=path / 'index.html') - - if docs.filename.stem != 'index': - docs.add_menu(docs.title, link=docs.filename) - - docs.end_menu() - - -def _generate_index(root, folder, paths, - bots_index=False, bots_index_paths=()): - """Generates the index file for the specified folder""" - # Determine the namespaces listed here (as sub folders) - # and the files (.html files) that we should link to - namespaces = [] - files = [] - INDEX = 'index.html' - BOT_INDEX = 'botindex.html' - - for item in (bots_index_paths or folder.iterdir()): - if item.is_dir(): - namespaces.append(item) - elif item.name not in (INDEX, BOT_INDEX): - files.append(item) - - # Now that everything is setup, write the index.html file - filename = folder / (BOT_INDEX if bots_index else INDEX) - with DocsWriter(root, filename, _get_path_for_type) as docs: - # Title should be the current folder name - docs.write_head(str(folder).replace(os.path.sep, '/').title(), - css_path=paths['css'], - default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - docs.write_title(str(filename.parent.relative_to(root)) - .replace(os.path.sep, '/').title()) - - if bots_index: - docs.write_text('These are the methods that you may be able to ' - 'use as a bot. Click here to ' - 'view them all.'.format(INDEX)) - else: - docs.write_text('Click here to view the methods ' - 'that you can use as a bot.'.format(BOT_INDEX)) - if namespaces: - docs.write_title('Namespaces', level=3) - docs.begin_table(4) - namespaces.sort() - for namespace in namespaces: - # For every namespace, also write the index of it - namespace_paths = [] - if bots_index: - for item in bots_index_paths: - if item.parent == namespace: - namespace_paths.append(item) - - _generate_index(root, namespace, paths, - bots_index, namespace_paths) - - docs.add_row( - namespace.stem.title(), - link=namespace / (BOT_INDEX if bots_index else INDEX)) - - docs.end_table() - - docs.write_title('Available items') - docs.begin_table(2) - - files = [(f, _find_title(f)) for f in files] - files.sort(key=lambda t: t[1]) - - for file, title in files: - docs.add_row(title, link=file) - - docs.end_table() - docs.end_body() - - -def _get_description(arg): - """Generates a proper description for the given argument.""" - desc = [] - otherwise = False - if arg.can_be_inferred: - desc.append('If left unspecified, it will be inferred automatically.') - otherwise = True - elif arg.is_flag: - desc.append('This argument defaults to ' - 'None and can be omitted.') - otherwise = True - - if arg.type in {'InputPeer', 'InputUser', 'InputChannel', - 'InputNotifyPeer', 'InputDialogPeer'}: - desc.append( - 'Anything entity-like will work if the library can find its ' - 'Input version (e.g., usernames, Peer, ' - 'User or Channel objects, etc.).' - ) - - if arg.is_vector: - if arg.is_generic: - desc.append('A list of other Requests must be supplied.') - else: - desc.append('A list must be supplied.') - elif arg.is_generic: - desc.append('A different Request must be supplied for this argument.') - else: - otherwise = False # Always reset to false if no other text is added - - if otherwise: - desc.insert(1, 'Otherwise,') - desc[-1] = desc[-1][:1].lower() + desc[-1][1:] - - return ' '.join(desc).replace( - 'list', - 'list' - ) - - -def _copy_replace(src, dst, replacements): - """Copies the src file into dst applying the replacements dict""" - with src.open() as infile, dst.open('w') as outfile: - outfile.write(re.sub( - '|'.join(re.escape(k) for k in replacements), - lambda m: str(replacements[m.group(0)]), - infile.read() - )) - - -def _write_html_pages(root, tlobjects, methods, layer, input_res): - """ - Generates the documentation HTML files from from ``scheme.tl`` - to ``/methods`` and ``/constructors``, etc. - """ - # Save 'Type: [Constructors]' for use in both: - # * Seeing the return type or constructors belonging to the same type. - # * Generating the types documentation, showing available constructors. - paths = {k: root / v for k, v in ( - ('css', 'css'), - ('arrow', 'img/arrow.svg'), - ('search.js', 'js/search.js'), - ('404', '404.html'), - ('index_all', 'index.html'), - ('bot_index', 'botindex.html'), - ('index_types', 'types/index.html'), - ('index_methods', 'methods/index.html'), - ('index_constructors', 'constructors/index.html') - )} - paths['default_css'] = 'light' # docs..css, local path - type_to_constructors = defaultdict(list) - type_to_functions = defaultdict(list) - for tlobject in tlobjects: - d = type_to_functions if tlobject.is_function else type_to_constructors - d[tlobject.result].append(tlobject) - - for t, cs in type_to_constructors.items(): - type_to_constructors[t] = list(sorted(cs, key=lambda c: c.name)) - - methods = {m.name: m for m in methods} - - # Since the output directory is needed everywhere partially apply it now - create_path_for = functools.partial(_get_path_for, root) - path_for_type = lambda t: root / _get_path_for_type(t) - bot_docs_paths = [] - - for tlobject in tlobjects: - filename = create_path_for(tlobject) - with DocsWriter(root, filename, path_for_type) as docs: - docs.write_head(title=tlobject.class_name, - css_path=paths['css'], - default_css=paths['default_css']) - - # Create the menu (path to the current TLObject) - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - - # Create the page title - docs.write_title(tlobject.class_name) - - if tlobject.is_function: - if tlobject.usability == Usability.USER: - start = 'Only users can' - elif tlobject.usability == Usability.BOT: - bot_docs_paths.append(filename) - start = 'Only bots can' - elif tlobject.usability == Usability.BOTH: - bot_docs_paths.append(filename) - start = 'Both users and bots can' - else: - bot_docs_paths.append(filename) - start = \ - 'Both users and bots may be able to' - - docs.write_text('{} use this method. ' - 'See code examples.'.format(start)) - - # Write the code definition for this TLObject - docs.write_code(tlobject) - docs.write_copy_button('Copy import to the clipboard', - get_import_code(tlobject)) - - # Write the return type (or constructors belonging to the same type) - docs.write_title('Returns' if tlobject.is_function - else 'Belongs to', level=3) - - generic_arg = next((arg.name for arg in tlobject.args - if arg.generic_definition), None) - - if tlobject.result == generic_arg: - # We assume it's a function returning a generic type - generic_arg = next((arg.name for arg in tlobject.args - if arg.is_generic)) - docs.write_text('This function returns the result of whatever ' - 'the result from invoking the request passed ' - 'through {} is.'.format(generic_arg)) - else: - if re.search('^vector<', tlobject.result, re.IGNORECASE): - docs.write_text('A list of the following type is returned.') - _, inner = tlobject.result.split('<') - inner = inner.strip('>') - else: - inner = tlobject.result - - docs.begin_table(column_count=1) - docs.add_row(inner, link=path_for_type(inner)) - docs.end_table() - - cs = type_to_constructors.get(inner, []) - if not cs: - docs.write_text('This type has no instances available.') - elif len(cs) == 1: - docs.write_text('This type can only be an instance of:') - else: - docs.write_text('This type can be an instance of either:') - - docs.begin_table(column_count=2) - for constructor in cs: - link = create_path_for(constructor) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - - # Return (or similar types) written. Now parameters/members - docs.write_title( - 'Parameters' if tlobject.is_function else 'Members', level=3 - ) - - # Sort the arguments in the same way they're sorted - # on the generated code (flags go last) - args = [ - a for a in tlobject.sorted_args() - if not a.flag_indicator and not a.generic_definition - ] - - if args: - # Writing parameters - docs.begin_table(column_count=3) - - for arg in args: - # Name row - docs.add_row(arg.name, - bold=True) - - # Type row - friendly_type = 'flag' if arg.type == 'true' else arg.type - if arg.is_generic: - docs.add_row('!' + friendly_type, align='center') - else: - docs.add_row( - friendly_type, align='center', - link=path_for_type(arg.type) - ) - - # Add a description for this argument - docs.add_row(_get_description(arg)) - - docs.end_table() - else: - if tlobject.is_function: - docs.write_text('This request takes no input parameters.') - else: - docs.write_text('This type has no members.') - - if tlobject.is_function: - docs.write_title('Known RPC errors') - method_info = methods.get(tlobject.fullname) - errors = method_info and method_info.errors - if not errors: - docs.write_text("This request can't cause any RPC error " - "as far as we know.") - else: - docs.write_text( - 'This request can cause {} known error{}:'.format( - len(errors), '' if len(errors) == 1 else 's' - )) - docs.begin_table(column_count=2) - for error in errors: - docs.add_row('{}'.format(error.name)) - docs.add_row('{}.'.format(error.description)) - docs.end_table() - docs.write_text('You can import these from ' - 'telethon.errors.') - - docs.write_title('Example', id='examples') - if tlobject.friendly: - ns, friendly = tlobject.friendly - docs.write_text( - 'Please refer to the documentation of client.{1}() ' - 'to learn about the parameters and see several code ' - 'examples on how to use it.' - .format(ns, friendly) - ) - docs.write_text( - 'The method above is the recommended way to do it. ' - 'If you need more control over the parameters or want ' - 'to learn how it is implemented, open the details by ' - 'clicking on the "Details" text.' - ) - docs.write('
    ') - - docs.write('''
    \
    -from telethon.sync import TelegramClient
    -from telethon import functions, types
    -
    -with TelegramClient(name, api_id, api_hash) as client:
    -    result = client(''')
    -                tlobject.as_example(docs, indent=1)
    -                docs.write(')\n')
    -                if tlobject.result.startswith('Vector'):
    -                    docs.write('''\
    -    for x in result:
    -        print(x''')
    -                else:
    -                    docs.write('    print(result')
    -                    if tlobject.result != 'Bool' \
    -                            and not tlobject.result.startswith('Vector'):
    -                        docs.write('.stringify()')
    -
    -                docs.write(')
    ') - if tlobject.friendly: - docs.write('
    ') - - depth = '../' * (2 if tlobject.namespace else 1) - docs.add_script(src='prependPath = "{}";'.format(depth)) - docs.add_script(path=paths['search.js']) - docs.end_body() - - # Find all the available types (which are not the same as the constructors) - # Each type has a list of constructors associated to it, hence is a map - for t, cs in type_to_constructors.items(): - filename = path_for_type(t) - out_dir = filename.parent - if out_dir: - out_dir.mkdir(parents=True, exist_ok=True) - - # Since we don't have access to the full TLObject, split the type - if '.' in t: - namespace, name = t.split('.') - else: - namespace, name = None, t - - with DocsWriter(root, filename, path_for_type) as docs: - docs.write_head(title=snake_to_camel_case(name), - css_path=paths['css'], - default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - - # Main file title - docs.write_title(snake_to_camel_case(name)) - - # List available constructors for this type - docs.write_title('Available constructors', level=3) - if not cs: - docs.write_text('This type has no constructors available.') - elif len(cs) == 1: - docs.write_text('This type has one constructor available.') - else: - docs.write_text('This type has %d constructors available.' % - len(cs)) - - docs.begin_table(2) - for constructor in cs: - # Constructor full name - link = create_path_for(constructor) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - - # List all the methods which return this type - docs.write_title('Methods returning this type', level=3) - functions = type_to_functions.get(t, []) - if not functions: - docs.write_text('No method returns this type.') - elif len(functions) == 1: - docs.write_text('Only the following method returns this type.') - else: - docs.write_text( - 'The following %d methods return this type as a result.' % - len(functions) - ) - - docs.begin_table(2) - for func in functions: - link = create_path_for(func) - docs.add_row(func.class_name, link=link) - docs.end_table() - - # List all the methods which take this type as input - docs.write_title('Methods accepting this type as input', level=3) - other_methods = sorted( - (u for u in tlobjects - if any(a.type == t for a in u.args) and u.is_function), - key=lambda u: u.name - ) - if not other_methods: - docs.write_text( - 'No methods accept this type as an input parameter.') - elif len(other_methods) == 1: - docs.write_text( - 'Only this method has a parameter with this type.') - else: - docs.write_text( - 'The following %d methods accept this type as an input ' - 'parameter.' % len(other_methods)) - - docs.begin_table(2) - for ot in other_methods: - link = create_path_for(ot) - docs.add_row(ot.class_name, link=link) - docs.end_table() - - # List every other type which has this type as a member - docs.write_title('Other types containing this type', level=3) - other_types = sorted( - (u for u in tlobjects - if any(a.type == t for a in u.args) and not u.is_function), - key=lambda u: u.name - ) - - if not other_types: - docs.write_text( - 'No other types have a member of this type.') - elif len(other_types) == 1: - docs.write_text( - 'You can find this type as a member of this other type.') - else: - docs.write_text( - 'You can find this type as a member of any of ' - 'the following %d types.' % len(other_types)) - - docs.begin_table(2) - for ot in other_types: - link = create_path_for(ot) - docs.add_row(ot.class_name, link=link) - docs.end_table() - docs.end_body() - - # After everything's been written, generate an index.html per folder. - # This will be done automatically and not taking into account any extra - # information that we have available, simply a file listing all the others - # accessible by clicking on their title - for folder in ['types', 'methods', 'constructors']: - _generate_index(root, root / folder, paths) - - _generate_index(root, root / 'methods', paths, True, - bot_docs_paths) - - # Write the final core index, the main index for the rest of files - types = set() - methods = [] - cs = [] - for tlobject in tlobjects: - if tlobject.is_function: - methods.append(tlobject) - else: - cs.append(tlobject) - - if not tlobject.result.lower() in CORE_TYPES: - if re.search('^vector<', tlobject.result, re.IGNORECASE): - types.add(tlobject.result.split('<')[1].strip('>')) - else: - types.add(tlobject.result) - - types = sorted(types) - methods = sorted(methods, key=lambda m: m.name) - cs = sorted(cs, key=lambda c: c.name) - - shutil.copy(str(input_res / '404.html'), str(paths['404'])) - _copy_replace(input_res / 'core.html', paths['index_all'], { - '{type_count}': len(types), - '{method_count}': len(methods), - '{constructor_count}': len(tlobjects) - len(methods), - '{layer}': layer, - }) - - def fmt(xs): - zs = {} # create a dict to hold those which have duplicated keys - for x in xs: - zs[x.class_name] = x.class_name in zs - return ', '.join( - '"{}.{}"'.format(x.namespace, x.class_name) - if zs[x.class_name] and x.namespace - else '"{}"'.format(x.class_name) for x in xs - ) - - request_names = fmt(methods) - constructor_names = fmt(cs) - - def fmt(xs, formatter): - return ', '.join('"{}"'.format( - formatter(x)).replace(os.path.sep, '/') for x in xs) - - type_names = fmt(types, formatter=lambda x: x) - - # Local URLs shouldn't rely on the output's root, so set empty root - get_path_for = functools.partial(_get_path_for, Path()) - - request_urls = fmt(methods, get_path_for) - type_urls = fmt(types, _get_path_for_type) - constructor_urls = fmt(cs, get_path_for) - - paths['search.js'].parent.mkdir(parents=True, exist_ok=True) - _copy_replace(input_res / 'js/search.js', paths['search.js'], { - '{request_names}': request_names, - '{type_names}': type_names, - '{constructor_names}': constructor_names, - '{request_urls}': request_urls, - '{type_urls}': type_urls, - '{constructor_urls}': constructor_urls - }) - - -def _copy_resources(res_dir, out_dir): - for dirname, files in [('css', ['docs.light.css', 'docs.dark.css']), - ('img', ['arrow.svg'])]: - dirpath = out_dir / dirname - dirpath.mkdir(parents=True, exist_ok=True) - for file in files: - shutil.copy(str(res_dir / dirname / file), str(dirpath)) - - -def _create_structure(tlobjects, output_dir): - """ - Pre-create the required directory structure - in `output_dir` for the input objects. - """ - types_ns = set() - method_ns = set() - for obj in tlobjects: - if obj.namespace: - if obj.is_function: - method_ns.add(obj.namespace) - else: - types_ns.add(obj.namespace) - - output_dir.mkdir(exist_ok=True) - - type_dir = output_dir / 'types' - type_dir.mkdir(exist_ok=True) - - cons_dir = output_dir / 'constructors' - cons_dir.mkdir(exist_ok=True) - for ns in types_ns: - (type_dir / ns).mkdir(exist_ok=True) - (cons_dir / ns).mkdir(exist_ok=True) - - meth_dir = output_dir / 'methods' - meth_dir.mkdir(exist_ok=True) - for ns in types_ns: - (meth_dir / ns).mkdir(exist_ok=True) - - -def generate_docs(tlobjects, methods_info, layer, input_res, output_dir): - _create_structure(tlobjects, output_dir) - _write_html_pages(output_dir, tlobjects, methods_info, layer, input_res) - _copy_resources(input_res, output_dir) diff --git a/telethon_generator/generators/errors.py b/telethon_generator/generators/errors.py index 6f5a6021..e69de29b 100644 --- a/telethon_generator/generators/errors.py +++ b/telethon_generator/generators/errors.py @@ -1,52 +0,0 @@ -def generate_errors(errors, f): - # Exact/regex match to create {CODE: ErrorClassName} - exact_match = [] - regex_match = [] - - # Find out what subclasses to import and which to create - import_base, create_base = set(), {} - for error in errors: - if error.subclass_exists: - import_base.add(error.subclass) - else: - create_base[error.subclass] = error.int_code - - if error.has_captures: - regex_match.append(error) - else: - exact_match.append(error) - - # Imports and new subclass creation - f.write('from .rpcbaseerrors import RPCError, {}\n' - .format(", ".join(sorted(import_base)))) - - for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]): - f.write('\n\nclass {}(RPCError):\n code = {}\n' - .format(cls, int_code)) - - # Error classes generation - for error in errors: - f.write('\n\nclass {}({}):\n' - ' def __init__(self, **kwargs):\n' - ' '.format(error.name, error.subclass)) - - if error.has_captures: - f.write("self.{} = int(kwargs.get('capture', 0))\n " - .format(error.capture_name)) - - f.write('super(Exception, self).__init__(' - '{}'.format(repr(error.description))) - - if error.has_captures: - f.write('.format({0}=self.{0})'.format(error.capture_name)) - - f.write(" + self._fmt_request(kwargs['request']))\n") - - # Create the actual {CODE: ErrorClassName} dict once classes are defined - f.write('\n\nrpc_errors_dict = {\n') - for error in exact_match: - f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) - f.write('}\n\nrpc_errors_re = (\n') - for error in regex_match: - f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) - f.write(')\n') diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py index cd2b99d7..e69de29b 100644 --- a/telethon_generator/generators/tlobject.py +++ b/telethon_generator/generators/tlobject.py @@ -1,747 +0,0 @@ -import functools -import os -import re -import shutil -import struct -from collections import defaultdict -from zlib import crc32 - -from ..sourcebuilder import SourceBuilder -from ..utils import snake_to_camel_case - -AUTO_GEN_NOTICE = \ - '"""File generated by TLObjects\' generator. All changes will be ERASED"""' - - -AUTO_CASTS = { - 'InputPeer': - 'utils.get_input_peer(await client.get_input_entity({}))', - 'InputChannel': - 'utils.get_input_channel(await client.get_input_entity({}))', - 'InputUser': - 'utils.get_input_user(await client.get_input_entity({}))', - - 'InputDialogPeer': 'await client._get_input_dialog({})', - 'InputNotifyPeer': 'await client._get_input_notify({})', - 'InputMedia': 'utils.get_input_media({})', - 'InputPhoto': 'utils.get_input_photo({})', - 'InputMessage': 'utils.get_input_message({})', - 'InputDocument': 'utils.get_input_document({})', - 'InputChatPhoto': 'utils.get_input_chat_photo({})', -} - -NAMED_AUTO_CASTS = { - ('chat_id', 'int'): 'await client.get_peer_id({}, add_mark=False)' -} - -# Secret chats have a chat_id which may be negative. -# With the named auto-cast above, we would break it. -# However there are plenty of other legit requests -# with `chat_id:int` where it is useful. -# -# NOTE: This works because the auto-cast is not recursive. -# There are plenty of types that would break if we -# did recurse into them to resolve them. -NAMED_BLACKLIST = { - 'messages.discardEncryption' -} - -BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', - 'int256', 'double', 'Bool', 'true', 'date') - -# Patched types {fullname: custom.ns.Name} -PATCHED_TYPES = { - 'messageEmpty': 'message.Message', - 'message': 'message.Message', - 'messageService': 'message.Message' -} - - -def _write_modules( - out_dir, depth, kind, namespace_tlobjects, type_constructors): - # namespace_tlobjects: {'namespace', [TLObject]} - out_dir.mkdir(parents=True, exist_ok=True) - for ns, tlobjects in namespace_tlobjects.items(): - file = out_dir / '{}.py'.format(ns or '__init__') - with file.open('w') as f, SourceBuilder(f) as builder: - builder.writeln(AUTO_GEN_NOTICE) - - builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth) - if kind != 'TLObject': - builder.writeln( - 'from {}.tl.tlobject import {}', '.' * depth, kind) - - builder.writeln('from typing import Optional, List, ' - 'Union, TYPE_CHECKING') - - # Add the relative imports to the namespaces, - # unless we already are in a namespace. - if not ns: - builder.writeln('from . import {}', ', '.join(sorted( - x for x in namespace_tlobjects.keys() if x - ))) - - # Import 'os' for those needing access to 'os.urandom()' - # Currently only 'random_id' needs 'os' to be imported, - # for all those TLObjects with arg.can_be_inferred. - builder.writeln('import os') - - # Import struct for the .__bytes__(self) serialization - builder.writeln('import struct') - - # Import datetime for type hinting - builder.writeln('from datetime import datetime') - - tlobjects.sort(key=lambda x: x.name) - - type_names = set() - type_defs = [] - - # Find all the types in this file and generate type definitions - # based on the types. The type definitions are written to the - # file at the end. - for t in tlobjects: - if not t.is_function: - type_name = t.result - if '.' in type_name: - type_name = type_name[type_name.rindex('.'):] - if type_name in type_names: - continue - type_names.add(type_name) - constructors = type_constructors[type_name] - if not constructors: - pass - elif len(constructors) == 1: - type_defs.append('Type{} = {}'.format( - type_name, constructors[0].class_name)) - else: - type_defs.append('Type{} = Union[{}]'.format( - type_name, ','.join(c.class_name - for c in constructors))) - - imports = {} - primitives = {'int', 'long', 'int128', 'int256', 'double', - 'string', 'date', 'bytes', 'Bool', 'true'} - # Find all the types in other files that are used in this file - # and generate the information required to import those types. - for t in tlobjects: - for arg in t.args: - name = arg.type - if not name or name in primitives: - continue - - import_space = '{}.tl.types'.format('.' * depth) - if '.' in name: - namespace = name.split('.')[0] - name = name.split('.')[1] - import_space += '.{}'.format(namespace) - - if name not in type_names: - type_names.add(name) - if name == 'date': - imports['datetime'] = ['datetime'] - continue - elif import_space not in imports: - imports[import_space] = set() - imports[import_space].add('Type{}'.format(name)) - - # Add imports required for type checking - if imports: - builder.writeln('if TYPE_CHECKING:') - for namespace, names in imports.items(): - builder.writeln('from {} import {}', - namespace, ', '.join(sorted(names))) - - builder.end_block() - - # Generate the class for every TLObject - for t in tlobjects: - if t.fullname in PATCHED_TYPES: - builder.writeln('{} = None # Patched', t.class_name) - else: - _write_source_code(t, kind, builder, type_constructors) - builder.current_indent = 0 - - # Write the type definitions generated earlier. - builder.writeln() - for line in type_defs: - builder.writeln(line) - - -def _write_source_code(tlobject, kind, builder, type_constructors): - """ - Writes the source code corresponding to the given TLObject - by making use of the ``builder`` `SourceBuilder`. - - Additional information such as file path depth and - the ``Type: [Constructors]`` must be given for proper - importing and documentation strings. - """ - _write_class_init(tlobject, kind, type_constructors, builder) - _write_resolve(tlobject, builder) - _write_to_dict(tlobject, builder) - _write_to_bytes(tlobject, builder) - _write_from_reader(tlobject, builder) - _write_read_result(tlobject, builder) - - -def _write_class_init(tlobject, kind, type_constructors, builder): - builder.writeln() - builder.writeln() - builder.writeln('class {}({}):', tlobject.class_name, kind) - - # Class-level variable to store its Telegram's constructor ID - builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) - builder.writeln('SUBCLASS_OF_ID = {:#x}', - crc32(tlobject.result.encode('ascii'))) - builder.writeln() - - # Convert the args to string parameters, flags having =None - args = ['{}: {}{}'.format( - a.name, a.type_hint(), '=None' if a.is_flag or a.can_be_inferred else '') - for a in tlobject.real_args - ] - - # Write the __init__ function if it has any argument - if not tlobject.real_args: - return - - if any(a.name in __builtins__ for a in tlobject.real_args): - builder.writeln('# noinspection PyShadowingBuiltins') - - builder.writeln("def __init__({}):", ', '.join(['self'] + args)) - builder.writeln('"""') - if tlobject.is_function: - builder.write(':returns {}: ', tlobject.result) - else: - builder.write('Constructor for {}: ', tlobject.result) - - constructors = type_constructors[tlobject.result] - if not constructors: - builder.writeln('This type has no constructors.') - elif len(constructors) == 1: - builder.writeln('Instance of {}.', - constructors[0].class_name) - else: - builder.writeln('Instance of either {}.', ', '.join( - c.class_name for c in constructors)) - - builder.writeln('"""') - - # Set the arguments - for arg in tlobject.real_args: - if not arg.can_be_inferred: - builder.writeln('self.{0} = {0}', arg.name) - - # Currently the only argument that can be - # inferred are those called 'random_id' - elif arg.name == 'random_id': - # Endianness doesn't really matter, and 'big' is shorter - code = "int.from_bytes(os.urandom({}), 'big', signed=True)" \ - .format(8 if arg.type == 'long' else 4) - - if arg.is_vector: - # Currently for the case of "messages.forwardMessages" - # Ensure we can infer the length from id:Vector<> - if not next(a for a in tlobject.real_args - if a.name == 'id').is_vector: - raise ValueError( - 'Cannot infer list of random ids for ', tlobject - ) - code = '[{} for _ in range(len(id))]'.format(code) - - builder.writeln( - "self.random_id = random_id if random_id " - "is not None else {}", code - ) - else: - raise ValueError('Cannot infer a value for ', arg) - - builder.end_block() - - -def _write_resolve(tlobject, builder): - if tlobject.is_function and any( - (arg.type in AUTO_CASTS - or ((arg.name, arg.type) in NAMED_AUTO_CASTS - and tlobject.fullname not in NAMED_BLACKLIST)) - for arg in tlobject.real_args - ): - builder.writeln('async def resolve(self, client, utils):') - for arg in tlobject.real_args: - ac = AUTO_CASTS.get(arg.type) - if not ac: - ac = NAMED_AUTO_CASTS.get((arg.name, arg.type)) - if not ac: - continue - - if arg.is_flag: - builder.writeln('if self.{}:', arg.name) - - if arg.is_vector: - builder.writeln('_tmp = []') - builder.writeln('for _x in self.{0}:', arg.name) - builder.writeln('_tmp.append({})', ac.format('_x')) - builder.end_block() - builder.writeln('self.{} = _tmp', arg.name) - else: - builder.writeln('self.{} = {}', arg.name, - ac.format('self.' + arg.name)) - - if arg.is_flag: - builder.end_block() - builder.end_block() - - -def _write_to_dict(tlobject, builder): - builder.writeln('def to_dict(self):') - builder.writeln('return {') - builder.current_indent += 1 - - builder.write("'_': '{}'", tlobject.class_name) - for arg in tlobject.real_args: - builder.writeln(',') - builder.write("'{}': ", arg.name) - if arg.type in BASE_TYPES: - if arg.is_vector: - builder.write('[] if self.{0} is None else self.{0}[:]', - arg.name) - else: - builder.write('self.{}', arg.name) - else: - if arg.is_vector: - builder.write( - '[] if self.{0} is None else [x.to_dict() ' - 'if isinstance(x, TLObject) else x for x in self.{0}]', - arg.name - ) - else: - builder.write( - 'self.{0}.to_dict() ' - 'if isinstance(self.{0}, TLObject) else self.{0}', - arg.name - ) - - builder.writeln() - builder.current_indent -= 1 - builder.writeln("}") - - builder.end_block() - - -def _write_to_bytes(tlobject, builder): - builder.writeln('def __bytes__(self):') - - # Some objects require more than one flag parameter to be set - # at the same time. In this case, add an assertion. - repeated_args = defaultdict(list) - for arg in tlobject.args: - if arg.is_flag: - repeated_args[arg.flag_index].append(arg) - - for ra in repeated_args.values(): - if len(ra) > 1: - cnd1 = ('(self.{0} or self.{0} is not None)' - .format(a.name) for a in ra) - cnd2 = ('(self.{0} is None or self.{0} is False)' - .format(a.name) for a in ra) - builder.writeln( - "assert ({}) or ({}), '{} parameters must all " - "be False-y (like None) or all me True-y'", - ' and '.join(cnd1), ' and '.join(cnd2), - ', '.join(a.name for a in ra) - ) - - builder.writeln("return b''.join((") - builder.current_indent += 1 - - # First constructor code, we already know its bytes - builder.writeln('{},', repr(struct.pack('/Vector. - # If this weren't the case, we should check upper case after - # max(index('<'), index('.')) (and if it is, it's boxed, so return). - m = re.match(r'Vector<(int|long)>', tlobject.result) - if not m: - return - - builder.end_block() - builder.writeln('@staticmethod') - builder.writeln('def read_result(reader):') - builder.writeln('reader.read_int() # Vector ID') - builder.writeln('return [reader.read_{}() ' - 'for _ in range(reader.read_int())]', m.group(1)) - - -def _write_arg_to_bytes(builder, arg, args, name=None): - """ - Writes the .__bytes__() code for the given argument - :param builder: The source code builder - :param arg: The argument to write - :param args: All the other arguments in TLObject same __bytes__. - This is required to determine the flags value - :param name: The name of the argument. Defaults to "self.argname" - This argument is an option because it's required when - writing Vectors<> - """ - if arg.generic_definition: - return # Do nothing, this only specifies a later type - - if name is None: - name = 'self.{}'.format(arg.name) - - # The argument may be a flag, only write if it's not None AND - # if it's not a True type. - # True types are not actually sent, but instead only used to - # determine the flags. - if arg.is_flag: - if arg.type == 'true': - return # Exit, since True type is never written - elif arg.is_vector: - # Vector flags are special since they consist of 3 values, - # so we need an extra join here. Note that empty vector flags - # should NOT be sent either! - builder.write("b'' if {0} is None or {0} is False " - "else b''.join((", name) - else: - builder.write("b'' if {0} is None or {0} is False " - "else (", name) - - if arg.is_vector: - if arg.use_vector_id: - # vector code, unsigned 0x1cb5c415 as little endian - builder.write(r"b'\x15\xc4\xb5\x1c',") - - builder.write("struct.pack('3.5 feature, so add another join. - builder.write("b''.join(") - - # Temporary disable .is_vector, not to enter this if again - # Also disable .is_flag since it's not needed per element - old_flag = arg.is_flag - arg.is_vector = arg.is_flag = False - _write_arg_to_bytes(builder, arg, args, name='x') - arg.is_vector = True - arg.is_flag = old_flag - - builder.write(' for x in {})', name) - - elif arg.flag_indicator: - # Calculate the flags with those items which are not None - if not any(f.is_flag for f in args): - # There's a flag indicator, but no flag arguments so it's 0 - builder.write(r"b'\0\0\0\0'") - else: - builder.write("struct.pack(' - """ - - if arg.generic_definition: - return # Do nothing, this only specifies a later type - - # The argument may be a flag, only write that flag was given! - was_flag = False - if arg.is_flag: - # Treat 'true' flags as a special case, since they're true if - # they're set, and nothing else needs to actually be read. - if 'true' == arg.type: - builder.writeln('{} = bool(flags & {})', - name, 1 << arg.flag_index) - return - - was_flag = True - builder.writeln('if flags & {}:', 1 << arg.flag_index) - # Temporary disable .is_flag not to enter this if - # again when calling the method recursively - arg.is_flag = False - - if arg.is_vector: - if arg.use_vector_id: - # We have to read the vector's constructor ID - builder.writeln("reader.read_int()") - - builder.writeln('{} = []', name) - builder.writeln('for _ in range(reader.read_int()):') - # Temporary disable .is_vector, not to enter this if again - arg.is_vector = False - _write_arg_read_code(builder, arg, args, name='_x') - builder.writeln('{}.append(_x)', name) - arg.is_vector = True - - elif arg.flag_indicator: - # Read the flags, which will indicate what items we should read next - builder.writeln('flags = reader.read_int()') - builder.writeln() - - elif 'int' == arg.type: - builder.writeln('{} = reader.read_int()', name) - - elif 'long' == arg.type: - builder.writeln('{} = reader.read_long()', name) - - elif 'int128' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=128)', name) - - elif 'int256' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=256)', name) - - elif 'double' == arg.type: - builder.writeln('{} = reader.read_double()', name) - - elif 'string' == arg.type: - builder.writeln('{} = reader.tgread_string()', name) - - elif 'Bool' == arg.type: - builder.writeln('{} = reader.tgread_bool()', name) - - elif 'true' == arg.type: - # Arbitrary not-None value, don't actually read "true" flags - builder.writeln('{} = True', name) - - elif 'bytes' == arg.type: - builder.writeln('{} = reader.tgread_bytes()', name) - - elif 'date' == arg.type: # Custom format - builder.writeln('{} = reader.tgread_date()', name) - - else: - # Else it may be a custom type - if not arg.skip_constructor_id: - builder.writeln('{} = reader.tgread_object()', name) - else: - # Import the correct type inline to avoid cyclic imports. - # There may be better solutions so that we can just access - # all the types before the files have been parsed, but I - # don't know of any. - sep_index = arg.type.find('.') - if sep_index == -1: - ns, t = '.', arg.type - else: - ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:] - class_name = snake_to_camel_case(t) - - # There would be no need to import the type if we're in the - # file with the same namespace, but since it does no harm - # and we don't have information about such thing in the - # method we just ignore that case. - builder.writeln('from {} import {}', ns, class_name) - builder.writeln('{} = {}.from_reader(reader)', - name, class_name) - - # End vector and flag blocks if required (if we opened them before) - if arg.is_vector: - builder.end_block() - - if was_flag: - builder.current_indent -= 1 - builder.writeln('else:') - builder.writeln('{} = None', name) - builder.current_indent -= 1 - # Restore .is_flag - arg.is_flag = True - - -def _write_patched(out_dir, namespace_tlobjects): - out_dir.mkdir(parents=True, exist_ok=True) - for ns, tlobjects in namespace_tlobjects.items(): - file = out_dir / '{}.py'.format(ns or '__init__') - with file.open('w') as f, SourceBuilder(f) as builder: - builder.writeln(AUTO_GEN_NOTICE) - - builder.writeln('import struct') - builder.writeln('from .. import TLObject, types, custom') - builder.writeln() - for t in tlobjects: - builder.writeln('class {}(custom.{}):', t.class_name, - PATCHED_TYPES[t.fullname]) - - builder.writeln('CONSTRUCTOR_ID = {:#x}', t.id) - builder.writeln('SUBCLASS_OF_ID = {:#x}', - crc32(t.result.encode('ascii'))) - - _write_to_dict(t, builder) - _write_to_bytes(t, builder) - _write_from_reader(t, builder) - builder.current_indent = 0 - builder.writeln() - builder.writeln( - 'types.{1}{0} = {0}', t.class_name, - '{}.'.format(t.namespace) if t.namespace else '' - ) - builder.writeln() - - -def _write_all_tlobjects(tlobjects, layer, builder): - builder.writeln(AUTO_GEN_NOTICE) - builder.writeln() - - builder.writeln('from . import types, functions, patched') - builder.writeln() - - # Create a constant variable to indicate which layer this is - builder.writeln('LAYER = {}', layer) - builder.writeln() - - # Then create the dictionary containing constructor_id: class - builder.writeln('tlobjects = {') - builder.current_indent += 1 - - # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) - for tlobject in tlobjects: - builder.write('{:#010x}: ', tlobject.id) - # TODO Probably circular dependency - if tlobject.fullname in PATCHED_TYPES: - builder.write('patched') - else: - builder.write('functions' if tlobject.is_function else 'types') - - if tlobject.namespace: - builder.write('.{}', tlobject.namespace) - - builder.writeln('.{},', tlobject.class_name) - - builder.current_indent -= 1 - builder.writeln('}') - - -def generate_tlobjects(tlobjects, layer, import_depth, output_dir): - # Group everything by {namespace: [tlobjects]} to generate __init__.py - namespace_functions = defaultdict(list) - namespace_types = defaultdict(list) - namespace_patched = defaultdict(list) - - # Group {type: [constructors]} to generate the documentation - type_constructors = defaultdict(list) - for tlobject in tlobjects: - if tlobject.is_function: - namespace_functions[tlobject.namespace].append(tlobject) - else: - namespace_types[tlobject.namespace].append(tlobject) - type_constructors[tlobject.result].append(tlobject) - if tlobject.fullname in PATCHED_TYPES: - namespace_patched[tlobject.namespace].append(tlobject) - - _write_modules(output_dir / 'functions', import_depth, 'TLRequest', - namespace_functions, type_constructors) - _write_modules(output_dir / 'types', import_depth, 'TLObject', - namespace_types, type_constructors) - _write_patched(output_dir / 'patched', namespace_patched) - - filename = output_dir / 'alltlobjects.py' - with filename.open('w') as file: - with SourceBuilder(file) as builder: - _write_all_tlobjects(tlobjects, layer, builder) - - -def clean_tlobjects(output_dir): - for d in ('functions', 'types', 'patched'): - d = output_dir / d - if d.is_dir(): - shutil.rmtree(str(d)) - - tl = output_dir / 'alltlobjects.py' - if tl.is_file(): - tl.unlink() diff --git a/telethon_generator/parsers/__init__.py b/telethon_generator/parsers/__init__.py index a8c9a7b7..e69de29b 100644 --- a/telethon_generator/parsers/__init__.py +++ b/telethon_generator/parsers/__init__.py @@ -1,3 +0,0 @@ -from .errors import Error, parse_errors -from .methods import MethodInfo, Usability, parse_methods -from .tlobject import TLObject, parse_tl, find_layer diff --git a/telethon_generator/parsers/errors.py b/telethon_generator/parsers/errors.py index 4fb9d43d..e69de29b 100644 --- a/telethon_generator/parsers/errors.py +++ b/telethon_generator/parsers/errors.py @@ -1,77 +0,0 @@ -import csv -import re - -from ..utils import snake_to_camel_case - -# Core base classes depending on the integer error code -KNOWN_BASE_CLASSES = { - 303: 'InvalidDCError', - 400: 'BadRequestError', - 401: 'UnauthorizedError', - 403: 'ForbiddenError', - 404: 'NotFoundError', - 406: 'AuthKeyError', - 420: 'FloodError', - 500: 'ServerError', - 503: 'TimedOutError' -} - - -def _get_class_name(error_code): - """ - Gets the corresponding class name for the given error code, - this either being an integer (thus base error name) or str. - """ - if isinstance(error_code, int): - return KNOWN_BASE_CLASSES.get( - abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg') - ) - - return snake_to_camel_case( - error_code.replace('FIRSTNAME', 'FIRST_NAME').lower(), suffix='Error') - - -class Error: - def __init__(self, codes, name, description): - # TODO Some errors have the same name but different integer codes - # Should these be split into different files or doesn't really matter? - # Telegram isn't exactly consistent with returned errors anyway. - self.int_code = codes[0] - self.str_code = name - self.subclass = _get_class_name(codes[0]) - self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES - self.description = description - - self.has_captures = '_X' in name - if self.has_captures: - self.name = _get_class_name(name.replace('_X', '')) - self.pattern = name.replace('_X', r'_(\d+)') - self.capture_name = re.search(r'{(\w+)}', description).group(1) - else: - self.name = _get_class_name(name) - self.pattern = name - self.capture_name = None - - -def parse_errors(csv_file): - """ - Parses the input CSV file with columns (name, error codes, description) - and yields `Error` instances as a result. - """ - with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, tup in enumerate(f, start=2): - try: - name, codes, description = tup - except ValueError: - raise ValueError('Columns count mismatch, unquoted comma in ' - 'desc? (line {})'.format(line)) from None - - try: - codes = [int(x) for x in codes.split()] or [400] - except ValueError: - raise ValueError('Not all codes are integers ' - '(line {})'.format(line)) from None - - yield Error([int(x) for x in codes], name, description) diff --git a/telethon_generator/parsers/methods.py b/telethon_generator/parsers/methods.py index ebc7e22f..e69de29b 100644 --- a/telethon_generator/parsers/methods.py +++ b/telethon_generator/parsers/methods.py @@ -1,58 +0,0 @@ -import csv -import enum -import warnings - - -class Usability(enum.Enum): - UNKNOWN = 0 - USER = 1 - BOT = 2 - BOTH = 4 - - -class MethodInfo: - def __init__(self, name, usability, errors, friendly): - self.name = name - self.errors = errors - self.friendly = friendly - try: - self.usability = { - 'unknown': Usability.UNKNOWN, - 'user': Usability.USER, - 'bot': Usability.BOT, - 'both': Usability.BOTH, - }[usability.lower()] - except KeyError: - raise ValueError('Usability must be either user, bot, both or ' - 'unknown, not {}'.format(usability)) from None - - -def parse_methods(csv_file, friendly_csv_file, errors_dict): - """ - Parses the input CSV file with columns (method, usability, errors) - and yields `MethodInfo` instances as a result. - """ - raw_to_friendly = {} - with friendly_csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for ns, friendly, raw_list in f: - for raw in raw_list.split(): - raw_to_friendly[raw] = (ns, friendly) - - with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, (method, usability, errors) in enumerate(f, start=2): - try: - errors = [errors_dict[x] for x in errors.split()] - except KeyError: - raise ValueError('Method {} references unknown errors {}' - .format(method, errors)) from None - - friendly = raw_to_friendly.pop(method, None) - yield MethodInfo(method, usability, errors, friendly) - - if raw_to_friendly: - warnings.warn('note: unknown raw methods in friendly mapping: {}' - .format(', '.join(raw_to_friendly))) diff --git a/telethon_generator/parsers/tlobject/__init__.py b/telethon_generator/parsers/tlobject/__init__.py index e1f432b7..e69de29b 100644 --- a/telethon_generator/parsers/tlobject/__init__.py +++ b/telethon_generator/parsers/tlobject/__init__.py @@ -1,3 +0,0 @@ -from .tlarg import TLArg -from .tlobject import TLObject -from .parser import parse_tl, find_layer diff --git a/telethon_generator/parsers/tlobject/parser.py b/telethon_generator/parsers/tlobject/parser.py index 5aa0fc9a..e69de29b 100644 --- a/telethon_generator/parsers/tlobject/parser.py +++ b/telethon_generator/parsers/tlobject/parser.py @@ -1,148 +0,0 @@ -import collections -import re - -from .tlarg import TLArg -from .tlobject import TLObject -from ..methods import Usability - - -CORE_TYPES = { - 0xbc799737, # boolFalse#bc799737 = Bool; - 0x997275b5, # boolTrue#997275b5 = Bool; - 0x3fedd339, # true#3fedd339 = True; - 0xc4b9f9bb, # error#c4b9f9bb code:int text:string = Error; - 0x56730bcc # null#56730bcc = Null; -} - -# Telegram Desktop (C++) doesn't care about string/bytes, and the .tl files -# don't either. However in Python we *do*, and we want to deal with bytes -# for the authorization key process, not UTF-8 strings (they won't be). -# -# Every type with an ID that's in here should get their attribute types -# with string being replaced with bytes. -AUTH_KEY_TYPES = { - 0x05162463, # resPQ, - 0x83c95aec, # p_q_inner_data - 0xa9f55f95, # p_q_inner_data_dc - 0x3c6a84d4, # p_q_inner_data_temp - 0x56fddf88, # p_q_inner_data_temp_dc - 0xd0e8075c, # server_DH_params_ok - 0xb5890dba, # server_DH_inner_data - 0x6643b654, # client_DH_inner_data - 0xd712e4be, # req_DH_params - 0xf5045f1f, # set_client_DH_params - 0x3072cfa1 # gzip_packed -} - - -def _from_line(line, is_function, method_info, layer): - match = re.match( - r'^([\w.]+)' # 'name' - r'(?:#([0-9a-fA-F]+))?' # '#optionalcode' - r'(?:\s{?\w+:[\w\d<>#.?!]+}?)*' # '{args:.0?type}' - r'\s=\s' # ' = ' - r'([\w\d<>#.?]+);$', # ';' - line - ) - if match is None: - # Probably "vector#1cb5c415 {t:Type} # [ t ] = Vector t;" - raise ValueError('Cannot parse TLObject {}'.format(line)) - - args_match = re.findall( - r'({)?' - r'(\w+)' - r':' - r'([\w\d<>#.?!]+)' - r'}?', - line - ) - - name = match.group(1) - method_info = method_info.get(name) - if method_info: - usability = method_info.usability - friendly = method_info.friendly - else: - usability = Usability.UNKNOWN - friendly = None - - return TLObject( - fullname=name, - object_id=match.group(2), - result=match.group(3), - is_function=is_function, - layer=layer, - usability=usability, - friendly=friendly, - args=[TLArg(name, arg_type, brace != '') - for brace, name, arg_type in args_match] - ) - - -def parse_tl(file_path, layer, methods=None, ignored_ids=CORE_TYPES): - """ - This method yields TLObjects from a given .tl file. - - Note that the file is parsed completely before the function yields - because references to other objects may appear later in the file. - """ - method_info = {m.name: m for m in (methods or [])} - obj_all = [] - obj_by_name = {} - obj_by_type = collections.defaultdict(list) - with file_path.open() as file: - is_function = False - for line in file: - comment_index = line.find('//') - if comment_index != -1: - line = line[:comment_index] - - line = line.strip() - if not line: - continue - - match = re.match(r'---(\w+)---', line) - if match: - following_types = match.group(1) - is_function = following_types == 'functions' - continue - - try: - result = _from_line( - line, is_function, method_info, layer=layer) - - if result.id in ignored_ids: - continue - - obj_all.append(result) - if not result.is_function: - obj_by_name[result.fullname] = result - obj_by_type[result.result].append(result) - except ValueError as e: - if 'vector#1cb5c415' not in str(e): - raise - - # Once all objects have been parsed, replace the - # string type from the arguments with references - for obj in obj_all: - if obj.id in AUTH_KEY_TYPES: - for arg in obj.args: - if arg.type == 'string': - arg.type = 'bytes' - - for arg in obj.args: - arg.cls = obj_by_type.get(arg.type) or ( - [obj_by_name[arg.type]] if arg.type in obj_by_name else [] - ) - - yield from obj_all - - -def find_layer(file_path): - """Finds the layer used on the specified scheme.tl file.""" - layer_regex = re.compile(r'^//\s*LAYER\s*(\d+)$') - with file_path.open('r') as file: - for line in file: - match = layer_regex.match(line) - if match: - return int(match.group(1)) diff --git a/telethon_generator/parsers/tlobject/tlarg.py b/telethon_generator/parsers/tlobject/tlarg.py index bf33a3f6..e69de29b 100644 --- a/telethon_generator/parsers/tlobject/tlarg.py +++ b/telethon_generator/parsers/tlobject/tlarg.py @@ -1,247 +0,0 @@ -import re - - -def _fmt_strings(*dicts): - for d in dicts: - for k, v in d.items(): - if v in ('None', 'True', 'False'): - d[k] = '{}'.format(v) - else: - d[k] = re.sub( - r'([brf]?([\'"]).*\2)', - lambda m: '{}'.format(m.group(1)), - v - ) - - -KNOWN_NAMED_EXAMPLES = { - ('message', 'string'): "'Hello there!'", - ('expires_at', 'date'): 'datetime.timedelta(minutes=5)', - ('until_date', 'date'): 'datetime.timedelta(days=14)', - ('view_messages', 'true'): 'None', - ('send_messages', 'true'): 'None', - ('limit', 'int'): '100', - ('hash', 'int'): '0', - ('hash', 'string'): "'A4LmkR23G0IGxBE71zZfo1'", - ('min_id', 'int'): '0', - ('max_id', 'int'): '0', - ('min_id', 'long'): '0', - ('max_id', 'long'): '0', - ('add_offset', 'int'): '0', - ('title', 'string'): "'My awesome title'", - ('device_model', 'string'): "'ASUS Laptop'", - ('system_version', 'string'): "'Arch Linux'", - ('app_version', 'string'): "'1.0'", - ('system_lang_code', 'string'): "'en'", - ('lang_pack', 'string'): "''", - ('lang_code', 'string'): "'en'", - ('chat_id', 'int'): '478614198', - ('client_id', 'long'): 'random.randrange(-2**63, 2**63)' -} - -KNOWN_TYPED_EXAMPLES = { - 'int128': "int.from_bytes(os.urandom(16), 'big')", - 'bytes': "b'arbitrary\\x7f data \\xfa here'", - 'long': "-12398745604826", - 'string': "'some string here'", - 'int': '42', - 'date': 'datetime.datetime(2018, 6, 25)', - 'double': '7.13', - 'Bool': 'False', - 'true': 'True', - 'InputChatPhoto': "client.upload_file('/path/to/photo.jpg')", - 'InputFile': "client.upload_file('/path/to/file.jpg')", - 'InputPeer': "'username'" -} - -_fmt_strings(KNOWN_NAMED_EXAMPLES, KNOWN_TYPED_EXAMPLES) - -SYNONYMS = { - 'InputUser': 'InputPeer', - 'InputChannel': 'InputPeer', - 'InputDialogPeer': 'InputPeer', - 'InputNotifyPeer': 'InputPeer', - 'InputMessage': 'int' -} - -# These are flags that are cleaner to leave off -OMITTED_EXAMPLES = { - 'silent', - 'background', - 'clear_draft', - 'reply_to_msg_id', - 'random_id', - 'reply_markup', - 'entities', - 'embed_links', - 'hash', - 'min_id', - 'max_id', - 'add_offset', - 'grouped', - 'broadcast', - 'admins', - 'edit', - 'delete' -} - - -class TLArg: - def __init__(self, name, arg_type, generic_definition): - """ - Initializes a new .tl argument - :param name: The name of the .tl argument - :param arg_type: The type of the .tl argument - :param generic_definition: Is the argument a generic definition? - (i.e. {X:Type}) - """ - self.name = 'is_self' if name == 'self' else name - - # Default values - self.is_vector = False - self.is_flag = False - self.skip_constructor_id = False - self.flag_index = -1 - self.cls = None - - # Special case: some types can be inferred, which makes it - # less annoying to type. Currently the only type that can - # be inferred is if the name is 'random_id', to which a - # random ID will be assigned if left as None (the default) - self.can_be_inferred = name == 'random_id' - - # The type can be an indicator that other arguments will be flags - if arg_type == '#': - self.flag_indicator = True - self.type = None - self.is_generic = False - else: - self.flag_indicator = False - self.is_generic = arg_type.startswith('!') - # Strip the exclamation mark always to have only the name - self.type = arg_type.lstrip('!') - - # The type may be a flag (flags.IDX?REAL_TYPE) - # Note that 'flags' is NOT the flags name; this - # is determined by a previous argument - # However, we assume that the argument will always be called 'flags' - flag_match = re.match(r'flags.(\d+)\?([\w<>.]+)', self.type) - if flag_match: - self.is_flag = True - self.flag_index = int(flag_match.group(1)) - # Update the type to match the exact type, not the "flagged" one - self.type = flag_match.group(2) - - # Then check if the type is a Vector - vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type) - if vector_match: - self.is_vector = True - - # If the type's first letter is not uppercase, then - # it is a constructor and we use (read/write) its ID - # as pinpointed on issue #81. - self.use_vector_id = self.type[0] == 'V' - - # Update the type to match the one inside the vector - self.type = vector_match.group(1) - - # See use_vector_id. An example of such case is ipPort in - # help.configSpecial - if self.type.split('.')[-1][0].islower(): - self.skip_constructor_id = True - - # The name may contain "date" in it, if this is the case and - # the type is "int", we can safely assume that this should be - # treated as a "date" object. Note that this is not a valid - # Telegram object, but it's easier to work with - if self.type == 'int' and ( - re.search(r'(\b|_)([dr]ate|until|since)(\b|_)', name) or - name in ('expires', 'expires_at', 'was_online')): - self.type = 'date' - - self.generic_definition = generic_definition - - def type_hint(self): - cls = self.type - if '.' in cls: - cls = cls.split('.')[1] - result = { - 'int': 'int', - 'long': 'int', - 'int128': 'int', - 'int256': 'int', - 'double': 'float', - 'string': 'str', - 'date': 'Optional[datetime]', # None date = 0 timestamp - 'bytes': 'bytes', - 'Bool': 'bool', - 'true': 'bool', - }.get(cls, "'Type{}'".format(cls)) - if self.is_vector: - result = 'List[{}]'.format(result) - if self.is_flag and cls != 'date': - result = 'Optional[{}]'.format(result) - - return result - - def real_type(self): - # Find the real type representation by updating it as required - real_type = self.type - if self.flag_indicator: - real_type = '#' - - if self.is_vector: - if self.use_vector_id: - real_type = 'Vector<{}>'.format(real_type) - else: - real_type = 'vector<{}>'.format(real_type) - - if self.is_generic: - real_type = '!{}'.format(real_type) - - if self.is_flag: - real_type = 'flags.{}?{}'.format(self.flag_index, real_type) - - return real_type - - def __str__(self): - if self.generic_definition: - return '{{{}:{}}}'.format(self.name, self.real_type()) - else: - return '{}:{}'.format(self.name, self.real_type()) - - def __repr__(self): - return str(self).replace(':date', ':int').replace('?date', '?int') - - def to_dict(self): - return { - 'name': self.name.replace('is_self', 'self'), - 'type': re.sub(r'\bdate$', 'int', self.real_type()) - } - - def as_example(self, f, indent=0): - if self.is_generic: - f.write('other_request') - return - - known = (KNOWN_NAMED_EXAMPLES.get((self.name, self.type)) - or KNOWN_TYPED_EXAMPLES.get(self.type) - or KNOWN_TYPED_EXAMPLES.get(SYNONYMS.get(self.type))) - if known: - f.write(known) - return - - assert self.omit_example() or self.cls, 'TODO handle ' + str(self) - - # Pick an interesting example if any - for cls in self.cls: - if cls.is_good_example(): - cls.as_example(f, indent) - break - else: - # If no example is good, just pick the first - self.cls[0].as_example(f, indent) - - def omit_example(self): - return (self.is_flag or self.can_be_inferred) \ - and self.name in OMITTED_EXAMPLES diff --git a/telethon_generator/parsers/tlobject/tlobject.py b/telethon_generator/parsers/tlobject/tlobject.py index 95ce6e42..e69de29b 100644 --- a/telethon_generator/parsers/tlobject/tlobject.py +++ b/telethon_generator/parsers/tlobject/tlobject.py @@ -1,146 +0,0 @@ -import re -import struct -import zlib - -from ...utils import snake_to_camel_case - -# https://github.com/telegramdesktop/tdesktop/blob/4bf66cb6e93f3965b40084771b595e93d0b11bcd/Telegram/SourceFiles/codegen/scheme/codegen_scheme.py#L57-L62 -WHITELISTED_MISMATCHING_IDS = { - # 0 represents any layer - 0: {'channel', # Since layer 77, there seems to be no going back... - 'ipPortSecret', 'accessPointRule', 'help.configSimple'} -} - - -class TLObject: - def __init__(self, fullname, object_id, args, result, - is_function, usability, friendly, layer): - """ - Initializes a new TLObject, given its properties. - - :param fullname: The fullname of the TL object (namespace.name) - The namespace can be omitted. - :param object_id: The hexadecimal string representing the object ID - :param args: The arguments, if any, of the TL object - :param result: The result type of the TL object - :param is_function: Is the object a function or a type? - :param usability: The usability for this method. - :param friendly: A tuple (namespace, friendly method name) if known. - :param layer: The layer this TLObject belongs to. - """ - # The name can or not have a namespace - self.fullname = fullname - if '.' in fullname: - self.namespace, self.name = fullname.split('.', maxsplit=1) - else: - self.namespace, self.name = None, fullname - - self.args = args - self.result = result - self.is_function = is_function - self.usability = usability - self.friendly = friendly - self.id = None - if object_id is None: - self.id = self.infer_id() - else: - self.id = int(object_id, base=16) - whitelist = WHITELISTED_MISMATCHING_IDS[0] |\ - WHITELISTED_MISMATCHING_IDS.get(layer, set()) - - if self.fullname not in whitelist: - assert self.id == self.infer_id(),\ - 'Invalid inferred ID for ' + repr(self) - - self.class_name = snake_to_camel_case( - self.name, suffix='Request' if self.is_function else '') - - self.real_args = list(a for a in self.sorted_args() if not - (a.flag_indicator or a.generic_definition)) - - def sorted_args(self): - """Returns the arguments properly sorted and ready to plug-in - into a Python's method header (i.e., flags and those which - can be inferred will go last so they can default =None) - """ - return sorted(self.args, - key=lambda x: x.is_flag or x.can_be_inferred) - - def __repr__(self, ignore_id=False): - if self.id is None or ignore_id: - hex_id = '' - else: - hex_id = '#{:08x}'.format(self.id) - - if self.args: - args = ' ' + ' '.join([repr(arg) for arg in self.args]) - else: - args = '' - - return '{}{}{} = {}'.format(self.fullname, hex_id, args, self.result) - - def infer_id(self): - representation = self.__repr__(ignore_id=True) - representation = representation\ - .replace(':bytes ', ':string ')\ - .replace('?bytes ', '?string ')\ - .replace('<', ' ').replace('>', '')\ - .replace('{', '').replace('}', '') - - representation = re.sub( - r' \w+:flags\.\d+\?true', - r'', - representation - ) - return zlib.crc32(representation.encode('ascii')) - - def to_dict(self): - return { - 'id': - str(struct.unpack('i', struct.pack('I', self.id))[0]), - 'method' if self.is_function else 'predicate': - self.fullname, - 'params': - [x.to_dict() for x in self.args if not x.generic_definition], - 'type': - self.result - } - - def is_good_example(self): - return not self.class_name.endswith('Empty') - - def as_example(self, f, indent=0): - f.write('functions' if self.is_function else 'types') - if self.namespace: - f.write('.') - f.write(self.namespace) - - f.write('.') - f.write(self.class_name) - f.write('(') - - args = [arg for arg in self.real_args if not arg.omit_example()] - if not args: - f.write(')') - return - - f.write('\n') - indent += 1 - remaining = len(args) - for arg in args: - remaining -= 1 - f.write(' ' * indent) - f.write(arg.name) - f.write('=') - if arg.is_vector: - f.write('[') - arg.as_example(f, indent) - if arg.is_vector: - f.write(']') - if remaining: - f.write(',') - f.write('\n') - - indent -= 1 - f.write(' ' * indent) - f.write(')') diff --git a/telethon_generator/sourcebuilder.py b/telethon_generator/sourcebuilder.py index 9fb61593..e69de29b 100644 --- a/telethon_generator/sourcebuilder.py +++ b/telethon_generator/sourcebuilder.py @@ -1,65 +0,0 @@ -class SourceBuilder: - """This class should be used to build .py source files""" - - def __init__(self, out_stream, indent_size=4): - self.current_indent = 0 - self.on_new_line = False - self.indent_size = indent_size - self.out_stream = out_stream - - # Was a new line added automatically before? If so, avoid it - self.auto_added_line = False - - def indent(self): - """Indents the current source code line - by the current indentation level - """ - self.write(' ' * (self.current_indent * self.indent_size)) - - def write(self, string, *args, **kwargs): - """Writes a string into the source code, - applying indentation if required - """ - if self.on_new_line: - self.on_new_line = False # We're not on a new line anymore - # If the string was not empty, indent; Else probably a new line - if string.strip(): - self.indent() - - if args or kwargs: - self.out_stream.write(string.format(*args, **kwargs)) - else: - self.out_stream.write(string) - - def writeln(self, string='', *args, **kwargs): - """Writes a string into the source code _and_ appends a new line, - applying indentation if required - """ - self.write(string + '\n', *args, **kwargs) - self.on_new_line = True - - # If we're writing a block, increment indent for the next time - if string and string[-1] == ':': - self.current_indent += 1 - - # Clear state after the user adds a new line - self.auto_added_line = False - - def end_block(self): - """Ends an indentation block, leaving an empty line afterwards""" - self.current_indent -= 1 - - # If we did not add a new line automatically yet, now it's the time! - if not self.auto_added_line: - self.writeln() - self.auto_added_line = True - - def __str__(self): - self.out_stream.seek(0) - return self.out_stream.read() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.out_stream.close() diff --git a/telethon_generator/utils.py b/telethon_generator/utils.py index 9889803f..e69de29b 100644 --- a/telethon_generator/utils.py +++ b/telethon_generator/utils.py @@ -1,8 +0,0 @@ -import re - - -def snake_to_camel_case(name, suffix=None): - # Courtesy of http://stackoverflow.com/a/31531797/4759433 - result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name) - result = result[:1].upper() + result[1:].replace('_', '') - return result + suffix if suffix else result