From fb41cc054693e581708bd9bfe7b6c017606fe517 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sun, 4 Jun 2023 11:36:27 +0200 Subject: [PATCH] Reboot project --- .coveragerc | 8 - .gitignore | 25 +- .pre-commit-config.yaml | 22 - README.rst | 32 +- dev-requirements.txt | 3 - optional-requirements.txt | 5 - pyproject.toml | 36 - readthedocs/Makefile | 20 - readthedocs/basic/installation.rst | 96 - readthedocs/basic/next-steps.rst | 46 - readthedocs/basic/quick-start.rst | 111 - readthedocs/basic/signing-in.rst | 229 - readthedocs/basic/updates.rst | 159 - readthedocs/concepts/asyncio.rst | 368 -- readthedocs/concepts/botapi-vs-mtproto.rst | 336 -- readthedocs/concepts/chats-vs-channels.rst | 169 - readthedocs/concepts/entities.rst | 313 -- readthedocs/concepts/errors.rst | 155 - readthedocs/concepts/full-api.rst | 420 -- readthedocs/concepts/sessions.rst | 165 - readthedocs/concepts/strings.rst | 88 - readthedocs/concepts/updates.rst | 228 - readthedocs/conf.py | 211 - readthedocs/custom_roles.py | 67 - readthedocs/developing/coding-style.rst | 22 - readthedocs/developing/philosophy.rst | 25 - readthedocs/developing/project-structure.rst | 51 - .../telegram-api-in-other-languages.rst | 13 - readthedocs/developing/test-servers.rst | 41 - readthedocs/developing/testing.rst | 87 - .../tips-for-porting-the-project.rst | 17 - .../understanding-the-type-language.rst | 33 - readthedocs/examples/chats-and-channels.rst | 128 - readthedocs/examples/users.rst | 74 - readthedocs/examples/word-of-warning.rst | 17 - .../examples/working-with-messages.rst | 13 - readthedocs/index.rst | 120 - readthedocs/make.bat | 36 - readthedocs/misc/changelog.rst | 4329 ----------------- .../misc/compatibility-and-convenience.rst | 185 - readthedocs/misc/wall-of-shame.rst | 65 - readthedocs/modules/client.rst | 103 - readthedocs/modules/custom.rst | 163 - readthedocs/modules/errors.rst | 20 - readthedocs/modules/events.rst | 70 - readthedocs/modules/helpers.rst | 8 - readthedocs/modules/network.rst | 33 - readthedocs/modules/sessions.rst | 27 - readthedocs/modules/utils.rst | 12 - .../quick-references/client-reference.rst | 202 - .../quick-references/events-reference.rst | 247 - readthedocs/quick-references/faq.rst | 423 -- .../quick-references/objects-reference.rst | 353 -- readthedocs/requirements.txt | 1 - requirements.txt | 2 - setup.py | 263 - telethon/__init__.py | 13 - telethon/_updates/__init__.py | 3 - telethon/_updates/entitycache.py | 62 - telethon/_updates/messagebox.py | 810 --- telethon/_updates/session.py | 195 - telethon/client/__init__.py | 25 - telethon/client/account.py | 243 - telethon/client/auth.py | 665 --- telethon/client/bots.py | 72 - telethon/client/buttons.py | 96 - telethon/client/chats.py | 1347 ----- telethon/client/dialogs.py | 610 --- telethon/client/downloads.py | 1046 ---- telethon/client/messageparse.py | 233 - telethon/client/messages.py | 1490 ------ telethon/client/telegrambaseclient.py | 951 ---- telethon/client/telegramclient.py | 13 - telethon/client/updates.py | 691 --- telethon/client/uploads.py | 789 --- telethon/client/users.py | 612 --- telethon/crypto/__init__.py | 10 - telethon/crypto/aes.py | 111 - telethon/crypto/aesctr.py | 42 - telethon/crypto/authkey.py | 63 - telethon/crypto/cdndecrypter.py | 105 - telethon/crypto/factorization.py | 67 - telethon/crypto/libssl.py | 140 - telethon/crypto/rsa.py | 165 - telethon/custom.py | 1 - telethon/errors/__init__.py | 46 - telethon/errors/common.py | 180 - telethon/errors/rpcbaseerrors.py | 131 - telethon/events/__init__.py | 140 - telethon/events/album.py | 343 -- telethon/events/callbackquery.py | 344 -- telethon/events/chataction.py | 458 -- telethon/events/common.py | 186 - telethon/events/inlinequery.py | 247 - telethon/events/messagedeleted.py | 57 - telethon/events/messageedited.py | 52 - telethon/events/messageread.py | 143 - telethon/events/newmessage.py | 223 - telethon/events/raw.py | 53 - telethon/events/userupdate.py | 310 -- telethon/extensions/__init__.py | 6 - telethon/extensions/binaryreader.py | 185 - telethon/extensions/html.py | 229 - telethon/extensions/markdown.py | 197 - telethon/extensions/messagepacker.py | 111 - telethon/functions.py | 1 - telethon/helpers.py | 435 -- telethon/hints.py | 67 - telethon/network/__init__.py | 14 - telethon/network/authenticator.py | 212 - telethon/network/connection/__init__.py | 12 - telethon/network/connection/connection.py | 434 -- telethon/network/connection/http.py | 39 - telethon/network/connection/tcpabridged.py | 33 - telethon/network/connection/tcpfull.py | 55 - .../network/connection/tcpintermediate.py | 46 - telethon/network/connection/tcpmtproxy.py | 152 - telethon/network/connection/tcpobfuscated.py | 62 - telethon/network/mtprotoplainsender.py | 56 - telethon/network/mtprotosender.py | 909 ---- telethon/network/mtprotostate.py | 279 -- telethon/network/requeststate.py | 19 - telethon/password.py | 194 - telethon/requestiter.py | 134 - telethon/sessions/__init__.py | 4 - telethon/sessions/abstract.py | 172 - telethon/sessions/memory.py | 251 - telethon/sessions/sqlite.py | 368 -- telethon/sessions/string.py | 63 - telethon/sync.py | 74 - telethon/tl/__init__.py | 1 - telethon/tl/core/__init__.py | 26 - telethon/tl/core/gzippacked.py | 45 - telethon/tl/core/messagecontainer.py | 47 - telethon/tl/core/rpcresult.py | 35 - telethon/tl/core/tlmessage.py | 34 - telethon/tl/custom/__init__.py | 14 - telethon/tl/custom/adminlogevent.py | 475 -- telethon/tl/custom/button.py | 308 -- telethon/tl/custom/chatgetter.py | 150 - telethon/tl/custom/conversation.py | 529 -- telethon/tl/custom/dialog.py | 161 - telethon/tl/custom/draft.py | 191 - telethon/tl/custom/file.py | 146 - telethon/tl/custom/forward.py | 51 - telethon/tl/custom/inlinebuilder.py | 450 -- telethon/tl/custom/inlineresult.py | 176 - telethon/tl/custom/inlineresults.py | 83 - telethon/tl/custom/inputsizedfile.py | 9 - telethon/tl/custom/message.py | 1159 ----- telethon/tl/custom/messagebutton.py | 146 - telethon/tl/custom/participantpermissions.py | 138 - telethon/tl/custom/qrlogin.py | 119 - telethon/tl/custom/sendergetter.py | 102 - telethon/tl/patched/__init__.py | 20 - telethon/tl/tlobject.py | 222 - telethon/types.py | 1 - telethon/utils.py | 1559 ------ telethon/version.py | 3 - telethon_examples/LICENSE | 116 - telethon_examples/README.md | 167 - telethon_examples/assistant.py | 70 - telethon_examples/gui.py | 372 -- .../interactive_telegram_client.py | 406 -- telethon_examples/payment.py | 181 - telethon_examples/print_messages.py | 52 - telethon_examples/print_updates.py | 45 - telethon_examples/quart_login.py | 140 - telethon_examples/replier.py | 100 - telethon_examples/screenshot-gui.jpg | Bin 26010 -> 0 bytes telethon_generator/__init__.py | 1 - telethon_generator/data/api.tl | 2078 -------- telethon_generator/data/errors.csv | 523 -- telethon_generator/data/friendly.csv | 26 - telethon_generator/data/html/404.html | 44 - telethon_generator/data/html/core.html | 179 - .../data/html/css/docs.dark.css | 185 - .../data/html/css/docs.h4x0r.css | 229 - .../data/html/css/docs.light.css | 182 - telethon_generator/data/html/img/arrow.svg | 35 - telethon_generator/data/html/js/search.js | 244 - telethon_generator/data/methods.csv | 365 -- telethon_generator/data/mtproto.tl | 116 - telethon_generator/docswriter.py | 295 -- telethon_generator/generators/__init__.py | 3 - telethon_generator/generators/docs.py | 649 --- telethon_generator/generators/errors.py | 60 - telethon_generator/generators/tlobject.py | 716 --- telethon_generator/parsers/__init__.py | 3 - telethon_generator/parsers/errors.py | 85 - telethon_generator/parsers/methods.py | 67 - .../parsers/tlobject/__init__.py | 3 - telethon_generator/parsers/tlobject/parser.py | 148 - telethon_generator/parsers/tlobject/tlarg.py | 246 - .../parsers/tlobject/tlobject.py | 155 - telethon_generator/sourcebuilder.py | 65 - telethon_generator/syncerrors.py | 55 - telethon_generator/utils.py | 8 - tests/__init__.py | 0 tests/readthedocs/__init__.py | 0 tests/readthedocs/conftest.py | 8 - .../readthedocs/quick_references/__init__.py | 0 .../quick_references/test_client_reference.py | 14 - tests/telethon/__init__.py | 0 tests/telethon/client/__init__.py | 0 tests/telethon/client/test_messages.py | 40 - tests/telethon/crypto/__init__.py | 0 tests/telethon/crypto/test_rsa.py | 42 - tests/telethon/events/__init__.py | 0 tests/telethon/events/test_chataction.py | 67 - tests/telethon/extensions/__init__.py | 0 tests/telethon/extensions/test_html.py | 65 - tests/telethon/extensions/test_markdown.py | 65 - tests/telethon/test_helpers.py | 82 - tests/telethon/test_pickle.py | 35 - tests/telethon/test_utils.py | 57 - tests/telethon/tl/__init__.py | 0 tests/telethon/tl/test_serialization.py | 13 - update-docs.sh | 14 - 219 files changed, 17 insertions(+), 44190 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .pre-commit-config.yaml delete mode 100644 dev-requirements.txt delete mode 100644 optional-requirements.txt delete mode 100644 pyproject.toml delete mode 100644 readthedocs/Makefile delete mode 100644 readthedocs/basic/installation.rst delete mode 100644 readthedocs/basic/next-steps.rst delete mode 100644 readthedocs/basic/quick-start.rst delete mode 100644 readthedocs/basic/signing-in.rst delete mode 100644 readthedocs/basic/updates.rst delete mode 100644 readthedocs/concepts/asyncio.rst delete mode 100644 readthedocs/concepts/botapi-vs-mtproto.rst delete mode 100644 readthedocs/concepts/chats-vs-channels.rst delete mode 100644 readthedocs/concepts/entities.rst delete mode 100644 readthedocs/concepts/errors.rst delete mode 100644 readthedocs/concepts/full-api.rst delete mode 100644 readthedocs/concepts/sessions.rst delete mode 100644 readthedocs/concepts/strings.rst delete mode 100644 readthedocs/concepts/updates.rst delete mode 100644 readthedocs/conf.py delete mode 100644 readthedocs/custom_roles.py delete mode 100644 readthedocs/developing/coding-style.rst delete mode 100644 readthedocs/developing/philosophy.rst delete mode 100644 readthedocs/developing/project-structure.rst delete mode 100644 readthedocs/developing/telegram-api-in-other-languages.rst delete mode 100644 readthedocs/developing/test-servers.rst delete mode 100644 readthedocs/developing/testing.rst delete mode 100644 readthedocs/developing/tips-for-porting-the-project.rst delete mode 100644 readthedocs/developing/understanding-the-type-language.rst delete mode 100644 readthedocs/examples/chats-and-channels.rst delete mode 100644 readthedocs/examples/users.rst delete mode 100644 readthedocs/examples/word-of-warning.rst delete mode 100644 readthedocs/examples/working-with-messages.rst delete mode 100644 readthedocs/index.rst delete mode 100644 readthedocs/make.bat delete mode 100644 readthedocs/misc/changelog.rst delete mode 100644 readthedocs/misc/compatibility-and-convenience.rst delete mode 100644 readthedocs/misc/wall-of-shame.rst delete mode 100644 readthedocs/modules/client.rst delete mode 100644 readthedocs/modules/custom.rst delete mode 100644 readthedocs/modules/errors.rst delete mode 100644 readthedocs/modules/events.rst delete mode 100644 readthedocs/modules/helpers.rst delete mode 100644 readthedocs/modules/network.rst delete mode 100644 readthedocs/modules/sessions.rst delete mode 100644 readthedocs/modules/utils.rst delete mode 100644 readthedocs/quick-references/client-reference.rst delete mode 100644 readthedocs/quick-references/events-reference.rst delete mode 100644 readthedocs/quick-references/faq.rst delete mode 100644 readthedocs/quick-references/objects-reference.rst delete mode 100644 readthedocs/requirements.txt delete mode 100644 requirements.txt delete mode 100755 setup.py delete mode 100644 telethon/__init__.py delete mode 100644 telethon/_updates/__init__.py delete mode 100644 telethon/_updates/entitycache.py delete mode 100644 telethon/_updates/messagebox.py delete mode 100644 telethon/_updates/session.py delete mode 100644 telethon/client/__init__.py delete mode 100644 telethon/client/account.py delete mode 100644 telethon/client/auth.py delete mode 100644 telethon/client/bots.py delete mode 100644 telethon/client/buttons.py delete mode 100644 telethon/client/chats.py delete mode 100644 telethon/client/dialogs.py delete mode 100644 telethon/client/downloads.py delete mode 100644 telethon/client/messageparse.py delete mode 100644 telethon/client/messages.py delete mode 100644 telethon/client/telegrambaseclient.py delete mode 100644 telethon/client/telegramclient.py delete mode 100644 telethon/client/updates.py delete mode 100644 telethon/client/uploads.py delete mode 100644 telethon/client/users.py delete mode 100644 telethon/crypto/__init__.py delete mode 100644 telethon/crypto/aes.py delete mode 100644 telethon/crypto/aesctr.py delete mode 100644 telethon/crypto/authkey.py delete mode 100644 telethon/crypto/cdndecrypter.py delete mode 100644 telethon/crypto/factorization.py delete mode 100644 telethon/crypto/libssl.py delete mode 100644 telethon/crypto/rsa.py delete mode 100644 telethon/custom.py delete mode 100644 telethon/errors/__init__.py delete mode 100644 telethon/errors/common.py delete mode 100644 telethon/errors/rpcbaseerrors.py delete mode 100644 telethon/events/__init__.py delete mode 100644 telethon/events/album.py delete mode 100644 telethon/events/callbackquery.py delete mode 100644 telethon/events/chataction.py delete mode 100644 telethon/events/common.py delete mode 100644 telethon/events/inlinequery.py delete mode 100644 telethon/events/messagedeleted.py delete mode 100644 telethon/events/messageedited.py delete mode 100644 telethon/events/messageread.py delete mode 100644 telethon/events/newmessage.py delete mode 100644 telethon/events/raw.py delete mode 100644 telethon/events/userupdate.py delete mode 100644 telethon/extensions/__init__.py delete mode 100644 telethon/extensions/binaryreader.py delete mode 100644 telethon/extensions/html.py delete mode 100644 telethon/extensions/markdown.py delete mode 100644 telethon/extensions/messagepacker.py delete mode 100644 telethon/functions.py delete mode 100644 telethon/helpers.py delete mode 100644 telethon/hints.py delete mode 100644 telethon/network/__init__.py delete mode 100644 telethon/network/authenticator.py delete mode 100644 telethon/network/connection/__init__.py delete mode 100644 telethon/network/connection/connection.py delete mode 100644 telethon/network/connection/http.py delete mode 100644 telethon/network/connection/tcpabridged.py delete mode 100644 telethon/network/connection/tcpfull.py delete mode 100644 telethon/network/connection/tcpintermediate.py delete mode 100644 telethon/network/connection/tcpmtproxy.py delete mode 100644 telethon/network/connection/tcpobfuscated.py delete mode 100644 telethon/network/mtprotoplainsender.py delete mode 100644 telethon/network/mtprotosender.py delete mode 100644 telethon/network/mtprotostate.py delete mode 100644 telethon/network/requeststate.py delete mode 100644 telethon/password.py delete mode 100644 telethon/requestiter.py delete mode 100644 telethon/sessions/__init__.py delete mode 100644 telethon/sessions/abstract.py delete mode 100644 telethon/sessions/memory.py delete mode 100644 telethon/sessions/sqlite.py delete mode 100644 telethon/sessions/string.py delete mode 100644 telethon/sync.py delete mode 100644 telethon/tl/__init__.py delete mode 100644 telethon/tl/core/__init__.py delete mode 100644 telethon/tl/core/gzippacked.py delete mode 100644 telethon/tl/core/messagecontainer.py delete mode 100644 telethon/tl/core/rpcresult.py delete mode 100644 telethon/tl/core/tlmessage.py delete mode 100644 telethon/tl/custom/__init__.py delete mode 100644 telethon/tl/custom/adminlogevent.py delete mode 100644 telethon/tl/custom/button.py delete mode 100644 telethon/tl/custom/chatgetter.py delete mode 100644 telethon/tl/custom/conversation.py delete mode 100644 telethon/tl/custom/dialog.py delete mode 100644 telethon/tl/custom/draft.py delete mode 100644 telethon/tl/custom/file.py delete mode 100644 telethon/tl/custom/forward.py delete mode 100644 telethon/tl/custom/inlinebuilder.py delete mode 100644 telethon/tl/custom/inlineresult.py delete mode 100644 telethon/tl/custom/inlineresults.py delete mode 100644 telethon/tl/custom/inputsizedfile.py delete mode 100644 telethon/tl/custom/message.py delete mode 100644 telethon/tl/custom/messagebutton.py delete mode 100644 telethon/tl/custom/participantpermissions.py delete mode 100644 telethon/tl/custom/qrlogin.py delete mode 100644 telethon/tl/custom/sendergetter.py delete mode 100644 telethon/tl/patched/__init__.py delete mode 100644 telethon/tl/tlobject.py delete mode 100644 telethon/types.py delete mode 100644 telethon/utils.py delete mode 100644 telethon/version.py delete mode 100644 telethon_examples/LICENSE delete mode 100644 telethon_examples/README.md delete mode 100644 telethon_examples/assistant.py delete mode 100644 telethon_examples/gui.py delete mode 100644 telethon_examples/interactive_telegram_client.py delete mode 100644 telethon_examples/payment.py delete mode 100644 telethon_examples/print_messages.py delete mode 100755 telethon_examples/print_updates.py delete mode 100644 telethon_examples/quart_login.py delete mode 100755 telethon_examples/replier.py delete mode 100644 telethon_examples/screenshot-gui.jpg delete mode 100644 telethon_generator/__init__.py delete mode 100644 telethon_generator/data/api.tl delete mode 100644 telethon_generator/data/errors.csv delete mode 100644 telethon_generator/data/friendly.csv delete mode 100644 telethon_generator/data/html/404.html delete mode 100644 telethon_generator/data/html/core.html delete mode 100644 telethon_generator/data/html/css/docs.dark.css delete mode 100644 telethon_generator/data/html/css/docs.h4x0r.css delete mode 100644 telethon_generator/data/html/css/docs.light.css delete mode 100644 telethon_generator/data/html/img/arrow.svg delete mode 100644 telethon_generator/data/html/js/search.js delete mode 100644 telethon_generator/data/methods.csv delete mode 100644 telethon_generator/data/mtproto.tl delete mode 100644 telethon_generator/docswriter.py delete mode 100644 telethon_generator/generators/__init__.py delete mode 100755 telethon_generator/generators/docs.py delete mode 100644 telethon_generator/generators/errors.py delete mode 100644 telethon_generator/generators/tlobject.py delete mode 100644 telethon_generator/parsers/__init__.py delete mode 100644 telethon_generator/parsers/errors.py delete mode 100644 telethon_generator/parsers/methods.py delete mode 100644 telethon_generator/parsers/tlobject/__init__.py delete mode 100644 telethon_generator/parsers/tlobject/parser.py delete mode 100644 telethon_generator/parsers/tlobject/tlarg.py delete mode 100644 telethon_generator/parsers/tlobject/tlobject.py delete mode 100644 telethon_generator/sourcebuilder.py delete mode 100644 telethon_generator/syncerrors.py delete mode 100644 telethon_generator/utils.py delete mode 100644 tests/__init__.py delete mode 100644 tests/readthedocs/__init__.py delete mode 100644 tests/readthedocs/conftest.py delete mode 100644 tests/readthedocs/quick_references/__init__.py delete mode 100644 tests/readthedocs/quick_references/test_client_reference.py delete mode 100644 tests/telethon/__init__.py delete mode 100644 tests/telethon/client/__init__.py delete mode 100644 tests/telethon/client/test_messages.py delete mode 100644 tests/telethon/crypto/__init__.py delete mode 100644 tests/telethon/crypto/test_rsa.py delete mode 100644 tests/telethon/events/__init__.py delete mode 100644 tests/telethon/events/test_chataction.py delete mode 100644 tests/telethon/extensions/__init__.py delete mode 100644 tests/telethon/extensions/test_html.py delete mode 100644 tests/telethon/extensions/test_markdown.py delete mode 100644 tests/telethon/test_helpers.py delete mode 100644 tests/telethon/test_pickle.py delete mode 100644 tests/telethon/test_utils.py delete mode 100644 tests/telethon/tl/__init__.py delete mode 100644 tests/telethon/tl/test_serialization.py delete mode 100644 update-docs.sh diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 1b2271b9..00000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -branch = true -parallel = true -source = - telethon - -[report] -precision = 2 diff --git a/.gitignore b/.gitignore index 6f2cf6f7..ae90a5f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,4 @@ -# Generated code -/telethon/tl/functions/ -/telethon/tl/types/ -/telethon/tl/alltlobjects.py -/telethon/errors/rpcerrorlist.py - -# User session -*.session -/usermedia/ - -# Builds and testing __pycache__/ -/dist/ -/build/ -/*.egg-info/ -/readthedocs/_build/ -/.tox/ - -# API reference docs -/docs/ - -# File used to manually test new changes, contains sensitive data -/example.py +*.py[cod] +*$py.class +.pytest_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 49f7e027..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -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/README.rst b/README.rst index bef2c36e..f9ce068d 100755 --- a/README.rst +++ b/README.rst @@ -1,5 +1,6 @@ Telethon ======== + .. epigraph:: ⭐️ Thanks **everyone** who has starred the project, it means a lot! @@ -10,11 +11,12 @@ 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. + If you have code using Telethon before its 2.0 version, it is strongly + recommended to read the Migration Guide section in the documentation. As with any third-party library for Telegram, be careful not to break `Telegram's ToS`_ or `Telegram can ban the account`_. + What is this? ------------- @@ -29,7 +31,7 @@ Installing .. code-block:: sh - pip3 install telethon + pip install telethon Creating a client @@ -44,8 +46,8 @@ Creating a client api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('session_name', api_id, api_hash) - client.start() + async with TelegramClient('session_name', api_id, api_hash) as client: + ... Doing stuff @@ -53,34 +55,28 @@ Doing stuff .. code-block:: python - print(client.get_me().stringify()) + print(await client.get_me()) - client.send_message('username', 'Hello! Talking to you from Telethon') - client.send_file('username', '/home/myself/Pictures/holidays.jpg') + await client.send_message('username', 'Hello! Talking to you from Telethon') + await client.send_message('username', photo='/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!') + async for message in client.get_messages('username', 1): + path = await message.download_media() + print('Saved media to', path) Next steps ---------- -Do you like how Telethon looks? Check out `Read The Docs`_ for a more +Do you like how Telethon looks? Check out the documentation for a more in-depth explanation, with examples, troubleshooting issues, and more useful information. .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _MTProto: https://core.telegram.org/mtproto .. _Telegram: https://telegram.org -.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html .. _Telegram's ToS: https://core.telegram.org/api/terms .. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library -.. _Read The Docs: https://docs.telethon.dev .. |logo| image:: logo.svg :width: 24pt diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index e01a8206..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -pytest -pytest-cov -pytest-asyncio diff --git a/optional-requirements.txt b/optional-requirements.txt deleted file mode 100644 index 30326da9..00000000 --- a/optional-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -cryptg -pysocks -python-socks[asyncio] -hachoir -pillow diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index daae10fa..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,36 +0,0 @@ -# https://snarky.ca/what-the-heck-is-pyproject-toml/ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -# Need to use legacy format for the time being -# https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini -[tool.tox] -legacy_tox_ini = """ -[tox] -envlist = py35,py36,py37,py38 - -# run with tox -e py -[testenv] -deps = - -rrequirements.txt - -roptional-requirements.txt - -rdev-requirements.txt -commands = - # NOTE: you can run any command line tool here - not just tests - pytest {posargs} - -# run with tox -e flake -[testenv:flake] -deps = - -rrequirements.txt - -roptional-requirements.txt - -rdev-requirements.txt - flake8 -commands = - # stop the build if there are Python syntax errors or undefined names - flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics - -""" diff --git a/readthedocs/Makefile b/readthedocs/Makefile deleted file mode 100644 index fd6e0d0a..00000000 --- a/readthedocs/Makefile +++ /dev/null @@ -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 deleted file mode 100644 index 2c04d8c6..00000000 --- a/readthedocs/basic/installation.rst +++ /dev/null @@ -1,96 +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, `upgrade pip`__ and run: - -.. code-block:: sh - - python3 -m pip install --upgrade pip - python3 -m pip install --upgrade telethon - -…to install or upgrade the library to the latest version. - -.. __: https://pythonspeed.com/articles/upgrade-pip/ - -Installing Development Versions -=============================== - -If you want the *latest* unreleased changes, -you can run the following command instead: - -.. code-block:: sh - - python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip - -.. note:: - - The development version may have bugs and is not recommended for production - use. However, when you are `reporting a library bug`__, you should try if the - bug still occurs in this version. - -.. __: https://github.com/LonamiWebs/Telethon/issues/ - - -Verification -============ - -To verify that the library is installed correctly, run the following command: - -.. code-block:: sh - - python3 -c "import telethon; print(telethon.__version__)" - -The version number of the library should show in the output. - - -Optional Dependencies -===================== - -If cryptg_ is installed, **the library will work a lot faster**, since -encryption and decryption will be made in C instead of Python. If your -code deals with a lot of updates or you are downloading/uploading a lot -of files, you will notice a considerable speed-up (from a hundred kilobytes -per second to several megabytes per second, if your connection allows it). -If it's not installed, pyaes_ will be used (which is pure Python, so it's -much slower). - -If pillow_ is installed, large images will be automatically resized when -sending photos to prevent Telegram from failing with "invalid image". -Official clients also do this. - -If aiohttp_ is installed, the library will be able to download -:tl:`WebDocument` media files (otherwise you will get an error). - -If hachoir_ is installed, it will be used to extract metadata from files -when sending documents. Telegram uses this information to show the song's -performer, artist, title, duration, and for videos too (including size). -Otherwise, they will default to empty values, and you can set the attributes -manually. - -.. note:: - - Some of the modules may require additional dependencies before being - installed through ``pip``. If you have an ``apt``-based system, consider - installing the most commonly missing dependencies (with the right ``pip``): - - .. code-block:: sh - - apt update - apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev - pip install -U --user setuptools - pip install -U --user telethon cryptg pillow - - Thanks to `@bb010g`_ for writing down this nice list. - - -.. _cryptg: https://github.com/cher-nov/cryptg -.. _pyaes: https://github.com/ricmoo/pyaes -.. _pillow: https://python-pillow.org -.. _aiohttp: https://docs.aiohttp.org -.. _hachoir: https://hachoir.readthedocs.io -.. _@bb010g: https://static.bb010g.com diff --git a/readthedocs/basic/next-steps.rst b/readthedocs/basic/next-steps.rst deleted file mode 100644 index 688ce766..00000000 --- a/readthedocs/basic/next-steps.rst +++ /dev/null @@ -1,46 +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. - -A note on developing applications -================================= - -If you're using the library to make an actual application (and not just -automate things), you should make sure to `comply with the ToS`__: - - […] when logging in as an existing user, apps are supposed to call - [:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of - Service; this call should be repeated after ``expires`` seconds have - elapsed. If an update to the Terms Of Service is available, clients are - supposed to show a consent popup; if accepted, clients should call - [:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON - object; in case of denial, clients are to delete the account using - [:tl:`DeleteAccount`], providing Decline ToS update as deletion reason. - -.. __: https://core.telegram.org/api/config#terms-of-service - -However, if you use the library to automate or enhance your Telegram -experience, it's very likely that you are using other applications doing this -check for you (so you wouldn't run the risk of violating the ToS). - -The library itself will not automatically perform this check or accept the ToS -because it should require user action (the only exception is during sign-up). diff --git a/readthedocs/basic/quick-start.rst b/readthedocs/basic/quick-start.rst deleted file mode 100644 index cd187c81..00000000 --- a/readthedocs/basic/quick-start.rst +++ /dev/null @@ -1,111 +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 import TelegramClient - - # Remember to use your own values from my.telegram.org! - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - client = TelegramClient('anon', api_id, api_hash) - - async def main(): - # Getting information about yourself - me = await client.get_me() - - # "me" is a user object. You can pretty-print - # any Telegram object with the "stringify" method: - print(me.stringify()) - - # When you print something, you see a representation of it. - # You can access all attributes of Telegram objects with - # the dot operator. For example, to get the username: - username = me.username - print(username) - print(me.phone) - - # You can print all the dialogs/conversations that you are part of: - async for dialog in client.iter_dialogs(): - print(dialog.name, 'has ID', dialog.id) - - # You can send messages to yourself... - await client.send_message('me', 'Hello, myself!') - # ...to some chat ID - await client.send_message(-100123456, 'Hello, group!') - # ...to your contacts - await client.send_message('+34600123123', 'Hello, friend!') - # ...or even to any username - await client.send_message('username', 'Testing Telethon!') - - # You can, of course, use markdown in your messages: - message = await client.send_message( - 'me', - 'This message has **bold**, `code`, __italics__ and ' - 'a [nice website](https://example.com)!', - link_preview=False - ) - - # Sending a message returns the sent message object, which you can use - print(message.raw_text) - - # You can reply to messages directly if you have a message object - await message.reply('Cool!') - - # Or send files, songs, documents, albums... - await client.send_file('me', '/home/me/Pictures/holidays.jpg') - - # You can print the message history of any chat: - async for message in client.iter_messages('me'): - print(message.id, message.text) - - # You can download media from messages, too! - # The method will return the path where the file was saved. - if message.photo: - path = await message.download_media() - print('File saved to', path) # printed after download is done - - with client: - client.loop.run_until_complete(main()) - - -Here, we show how to sign in, get information about yourself, send -messages, files, getting chats, printing messages, and downloading -files. - -You should make sure that you understand what the code shown here -does, take note on how methods are called and used and so on before -proceeding. We will see all the available methods later on. - -.. important:: - - Note that Telethon is an asynchronous library, and as such, you should - get used to it and learn a bit of basic `asyncio`. This will help a lot. - As a quick start, this means you generally want to write all your code - inside some ``async def`` like so: - - .. code-block:: python - - client = ... - - async def do_something(me): - ... - - async def main(): - # Most of your code should go here. - # You can of course make and use your own async def (do_something). - # They only need to be async if they need to await things. - me = await client.get_me() - await do_something(me) - - with client: - client.loop.run_until_complete(main()) - - After you understand this, you may use the ``telethon.sync`` hack if you - want do so (see :ref:`compatibility-and-convenience`), but note you may - run into other issues (iPython, Anaconda, etc. have some issues with it). diff --git a/readthedocs/basic/signing-in.rst b/readthedocs/basic/signing-in.rst deleted file mode 100644 index 9fb14853..00000000 --- a/readthedocs/basic/signing-in.rst +++ /dev/null @@ -1,229 +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 import TelegramClient - - # Use your own values from my.telegram.org - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - - # The first parameter is the .session file name (absolute paths allowed) - with TelegramClient('anon', api_id, api_hash) as client: - client.loop.run_until_complete(client.send_message('me', 'Hello, myself!')) - - -In the first line, we import the class name so we can create an instance -of the client. Then, we define variables to store our API ID and hash -conveniently. - -At last, we create a new `TelegramClient ` -instance and call it ``client``. We can now use the client variable -for anything that we want, such as sending a message to ourselves. - -.. note:: - - Since Telethon is an asynchronous library, you need to ``await`` - coroutine functions to have them run (or otherwise, run the loop - until they are complete). In this tiny example, we don't bother - making an ``async def main()``. - - See :ref:`mastering-asyncio` to find out more. - - -Using a ``with`` block is the preferred way to use the library. It will -automatically `start() ` the client, -logging or signing up if necessary. - -If the ``.session`` file already existed, it will not login -again, so be aware of this if you move or rename the file! - - -Signing In as a Bot Account -=========================== - -You can also use Telethon for your bots (normal bot accounts, not users). -You will still need an API ID and hash, but the process is very similar: - - -.. code-block:: python - - from telethon.sync import TelegramClient - - api_id = 12345 - api_hash = '0123456789abcdef0123456789abcdef' - bot_token = '12345:0123456789abcdef0123456789abcdef' - - # We have to manually call "start" if we want an explicit bot token - bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token) - - # But then we can use the client instance as usual - with bot: - ... - - -To get a bot account, you need to talk -with `@BotFather `_. - - -Signing In behind a Proxy -========================= - -If you need to use a proxy to access Telegram, -you will need to either: - -* For Python >= 3.6 : `install python-socks[asyncio]`__ -* For Python <= 3.5 : `install PySocks`__ - -and then change - -.. code-block:: python - - TelegramClient('anon', api_id, api_hash) - -with - -.. code-block:: python - - TelegramClient('anon', api_id, api_hash, proxy=("socks5", '127.0.0.1', 4444)) - -(of course, replacing the protocol, IP and port with the protocol, IP and port of the proxy). - -The ``proxy=`` argument should be a dict (or tuple, for backwards compatibility), -consisting of parameters described `in PySocks usage`__. - -The allowed values for the argument ``proxy_type`` are: - -* For Python <= 3.5: - * ``socks.SOCKS5`` or ``'socks5'`` - * ``socks.SOCKS4`` or ``'socks4'`` - * ``socks.HTTP`` or ``'http'`` - -* For Python >= 3.6: - * All of the above - * ``python_socks.ProxyType.SOCKS5`` - * ``python_socks.ProxyType.SOCKS4`` - * ``python_socks.ProxyType.HTTP`` - - -Example: - -.. code-block:: python - - proxy = { - 'proxy_type': 'socks5', # (mandatory) protocol to use (see above) - 'addr': '1.1.1.1', # (mandatory) proxy IP address - 'port': 5555, # (mandatory) proxy port number - 'username': 'foo', # (optional) username if the proxy requires auth - 'password': 'bar', # (optional) password if the proxy requires auth - 'rdns': True # (optional) whether to use remote or local resolve, default remote - } - -For backwards compatibility with ``PySocks`` the following format -is possible (but discouraged): - -.. code-block:: python - - proxy = (socks.SOCKS5, '1.1.1.1', 5555, True, 'foo', 'bar') - -.. __: https://github.com/romis2012/python-socks#installation -.. __: https://github.com/Anorov/PySocks#installation -.. __: https://github.com/Anorov/PySocks#usage-1 - - -Using MTProto Proxies -===================== - -MTProto Proxies are Telegram's alternative to normal proxies, -and work a bit differently. The following protocols are available: - -* ``ConnectionTcpMTProxyAbridged`` -* ``ConnectionTcpMTProxyIntermediate`` -* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred) - -For now, you need to manually specify these special connection modes -if you want to use a MTProto Proxy. Your code would look like this: - -.. code-block:: python - - from telethon import TelegramClient, connection - # we need to change the connection ^^^^^^^^^^ - - client = TelegramClient( - 'anon', - api_id, - api_hash, - - # Use one of the available connection modes. - # Normally, this one works with most proxies. - connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, - - # Then, pass the proxy details as a tuple: - # (host name, port, proxy secret) - # - # If the proxy has no secret, the secret must be: - # '00000000000000000000000000000000' - proxy=('mtproxy.example.com', 2002, 'secret') - ) - -In future updates, we may make it easier to use MTProto Proxies -(such as avoiding the need to manually pass ``connection=``). - -In short, the same code above but without comments to make it clearer: - -.. code-block:: python - - from telethon import TelegramClient, connection - - client = TelegramClient( - 'anon', api_id, api_hash, - connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, - proxy=('mtproxy.example.com', 2002, 'secret') - ) diff --git a/readthedocs/basic/updates.rst b/readthedocs/basic/updates.rst deleted file mode 100644 index 6e22ae64..00000000 --- a/readthedocs/basic/updates.rst +++ /dev/null @@ -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 deleted file mode 100644 index cf775fcf..00000000 --- a/readthedocs/concepts/asyncio.rst +++ /dev/null @@ -1,368 +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? -======================== - -The code samples below assume that you have Python 3.7 or greater installed. - -.. code-block:: python - - # First we need the asyncio library - import asyncio - - # We also need something to run - async def main(): - for char in 'Hello, world!\n': - print(char, end='', flush=True) - await asyncio.sleep(0.2) - - # Then, we can create a new asyncio loop and use it to run our coroutine. - # The creation and tear-down of the loop is hidden away from us. - asyncio.run(main()) - - -What does telethon.sync do? -=========================== - -The moment you import any of these: - -.. code-block:: python - - from telethon import sync, ... - # or - from telethon.sync import ... - # or - import telethon.sync - -The ``sync`` module rewrites most ``async def`` -methods in Telethon to something similar to this: - -.. code-block:: python - - def new_method(): - result = original_method() - if loop.is_running(): - # the loop is already running, return the await-able to the user - return result - else: - # the loop is not running yet, so we can run it for the user - return loop.run_until_complete(result) - - -That means you can do this: - -.. code-block:: python - - print(client.get_me().username) - -Instead of this: - -.. code-block:: python - - me = client.loop.run_until_complete(client.get_me()) - print(me.username) - - # or, using asyncio's default loop (it's the same) - import asyncio - loop = asyncio.get_running_loop() # == client.loop - me = loop.run_until_complete(client.get_me()) - print(me.username) - - -As you can see, it's a lot of boilerplate and noise having to type -``run_until_complete`` all the time, so you can let the magic module -to rewrite it for you. But notice the comment above: it won't run -the loop if it's already running, because it can't. That means this: - -.. code-block:: python - - async def main(): - # 3. the loop is running here - print( - client.get_me() # 4. this will return a coroutine! - .username # 5. this fails, coroutines don't have usernames - ) - - loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine - main() # 1. calling ``async def`` "returns" a coroutine - ) - - -Will fail. So if you're inside an ``async def``, then the loop is -running, and if the loop is running, you must ``await`` things yourself: - -.. code-block:: python - - async def main(): - print((await client.get_me()).username) - - loop.run_until_complete(main()) - - -What are async, await and coroutines? -===================================== - -The ``async`` keyword lets you define asynchronous functions, -also known as coroutines, and also iterate over asynchronous -loops or use ``async with``: - -.. code-block:: python - - import asyncio - - async def main(): - # ^ this declares the main() coroutine function - - async with client: - # ^ this is an asynchronous with block - - async for message in client.iter_messages(chat): - # ^ this is a for loop over an asynchronous generator - - print(message.sender.username) - - asyncio.run(main()) - # ^ this will create a new asyncio loop behind the scenes and tear it down - # once the function returns. It will run the loop untiil main finishes. - # You should only use this function if there is no other loop running. - - -The ``await`` keyword blocks the *current* task, and the loop can run -other tasks. Tasks can be thought of as "threads", since many can run -concurrently: - -.. code-block:: python - - import asyncio - - async def hello(delay): - await asyncio.sleep(delay) # await tells the loop this task is "busy" - print('hello') # eventually the loop resumes the code here - - async def world(delay): - # the loop decides this method should run first - await asyncio.sleep(delay) # await tells the loop this task is "busy" - print('world') # eventually the loop finishes all tasks - - async def main(): - asyncio.create_task(world(2)) # create the world task, passing 2 as delay - asyncio.create_task(hello(delay=1)) # another task, but with delay 1 - await asyncio.sleep(3) # wait for three seconds before exiting - - try: - # create a new temporary asyncio loop and use it to run main - asyncio.run(main()) - except KeyboardInterrupt: - pass - -The same example, but without the comment noise: - -.. code-block:: python - - import asyncio - - async def hello(delay): - await asyncio.sleep(delay) - print('hello') - - async def world(delay): - await asyncio.sleep(delay) - print('world') - - async def main(): - asyncio.create_task(world(2)) - asyncio.create_task(hello(delay=1)) - await asyncio.sleep(3) - - try: - asyncio.run(main()) - except KeyboardInterrupt: - pass - - -Can I use threads? -================== - -Yes, you can, but you must understand that the loops themselves are -not thread safe. and you must be sure to know what is happening. The -easiest and cleanest option is to use `asyncio.run` to create and manage -the new event loop for you: - -.. code-block:: python - - import asyncio - import threading - - async def actual_work(): - client = TelegramClient(..., loop=loop) - ... # can use `await` here - - def go(): - asyncio.run(actual_work()) - - threading.Thread(target=go).start() - - -Generally, **you don't need threads** unless you know what you're doing. -Just create another task, as shown above. If you're using the Telethon -with a library that uses threads, you must be careful to use `threading.Lock` -whenever you use the client, or enable the compatible mode. For that, see -:ref:`compatibility-and-convenience`. - -You may have seen this error: - -.. code-block:: text - - RuntimeError: There is no current event loop in thread 'Thread-1'. - -It just means you didn't create a loop for that thread. Please refer to -the ``asyncio`` documentation to correctly learn how to set the event loop -for non-main threads. - - -client.run_until_disconnected() blocks! -======================================= - -All of what `client.run_until_disconnected() -` does is -run the `asyncio`'s event loop until the client is disconnected. That means -*the loop is running*. And if the loop is running, it will run all the tasks -in it. So if you want to run *other* code, create tasks for it: - -.. code-block:: python - - from datetime import datetime - - async def clock(): - while True: - print('The time:', datetime.now()) - await asyncio.sleep(1) - - loop.create_task(clock()) - ... - client.run_until_disconnected() - -This creates a task for a clock that prints the time every second. -You don't need to use `client.run_until_disconnected() -` either! -You just need to make the loop is running, somehow. `loop.run_forever() -` and `loop.run_until_complete() -` can also be used to run -the loop, and Telethon will be happy with any approach. - -Of course, there are better tools to run code hourly or daily, see below. - - -What else can asyncio do? -========================= - -Asynchronous IO is a really powerful tool, as we've seen. There are plenty -of other useful libraries that also use `asyncio` and that you can integrate -with Telethon. - -* `aiohttp `_ is like the infamous - `requests `_ but asynchronous. -* `quart `_ is an asynchronous alternative - to `Flask `_. -* `aiocron `_ lets you schedule things - to run things at a desired time, or run some tasks hourly, daily, etc. - -And of course, `asyncio `_ -itself! It has a lot of methods that let you do nice things. For example, -you can run requests in parallel: - -.. code-block:: python - - async def main(): - last, sent, download_path = await asyncio.gather( - client.get_messages('telegram', 10), - client.send_message('me', 'Using asyncio!'), - client.download_profile_photo('telegram') - ) - - loop.run_until_complete(main()) - - -This code will get the 10 last messages from `@telegram -`_, send one to the chat with yourself, and also -download the profile photo of the channel. `asyncio` will run all these -three tasks at the same time. You can run all the tasks you want this way. - -A different way would be: - -.. code-block:: python - - loop.create_task(client.get_messages('telegram', 10)) - loop.create_task(client.send_message('me', 'Using asyncio!')) - loop.create_task(client.download_profile_photo('telegram')) - -They will run in the background as long as the loop is running too. - -You can also `start an asyncio server -`_ -in the main script, and from another script, `connect to it -`_ -to achieve `Inter-Process Communication -`_. -You can get as creative as you want. You can program anything you want. -When you use a library, you're not limited to use only its methods. You can -combine all the libraries you want. People seem to forget this simple fact! - - -Why does client.start() work outside async? -=========================================== - -Because it's so common that it's really convenient to offer said -functionality by default. This means you can set up all your event -handlers and start the client without worrying about loops at all. - -Using the client in a ``with`` block, `start -`, `run_until_disconnected -`, and -`disconnect ` -all support this. - -Where can I read more? -====================== - -`Check out my blog post -`_ about `asyncio`, which -has some more examples and pictures to help you understand what happens -when the loop runs. diff --git a/readthedocs/concepts/botapi-vs-mtproto.rst b/readthedocs/concepts/botapi-vs-mtproto.rst deleted file mode 100644 index b42a1bcf..00000000 --- a/readthedocs/concepts/botapi-vs-mtproto.rst +++ /dev/null @@ -1,336 +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. - -Configuration of your bot, such as its available commands and auto-completion, -is configured through `@BotFather `_. - - -What is MTProto? -================ - -MTProto_ is Telegram's own protocol to communicate with their API when you -connect to their servers. - -Telethon is an alternative MTProto-based backend written entirely in Python -and much easier to setup and use. - -Both official applications and third-party clients (like your own -applications) logged in as either user or bots **can use MTProto** to -communicate directly with Telegram's API (which is not the HTTP bot API). - -When we talk about MTProto, we often mean "MTProto-based clients". - - -Advantages of MTProto over Bot API -================================== - -MTProto clients (like Telethon) connect directly to Telegram's servers, -which means there is no HTTP connection, no "polling" or "web hooks". This -means **less overhead**, since the protocol used between you and the server -is much more compact than HTTP requests with responses in wasteful JSON. - -Since there is a direct connection to Telegram's servers, even if their -Bot API endpoint is down, you can still have connection to Telegram directly. - -Using a MTProto client, you are also not limited to the public API that -they expose, and instead, **you have full control** of what your bot can do. -Telethon offers you all the power with often **much easier usage** than any -of the available Python Bot API wrappers. - -If your application ever needs user features because bots cannot do certain -things, you will be able to easily login as a user and even keep your bot -without having to learn a new library. - -If less overhead and full control didn't convince you to use Telethon yet, -check out the wiki page `MTProto vs HTTP Bot API`_ with a more exhaustive -and up-to-date list of differences. - - -Migrating from Bot API to Telethon -================================== - -It doesn't matter if you wrote your bot with requests_ and you were -making API requests manually, or if you used a wrapper library like -python-telegram-bot_ or pyTelegramBotAPI_. It's never too late to -migrate to Telethon! - -If you were using an asynchronous library like aiohttp_ or a wrapper like -aiogram_ or dumbot_, it will be even easier, because Telethon is also an -asynchronous library. - -Next, we will see some examples from the most popular libraries. - - -Migrating from python-telegram-bot ----------------------------------- - -Let's take their `echobot.py`_ example and shorten it a bit: - -.. code-block:: python - - from telegram.ext import Updater, CommandHandler, MessageHandler, Filters - - def start(update, context): - """Send a message when the command /start is issued.""" - update.message.reply_text('Hi!') - - def echo(update, context): - """Echo the user message.""" - update.message.reply_text(update.message.text) - - def main(): - """Start the bot.""" - updater = Updater("TOKEN") - dp = updater.dispatcher - dp.add_handler(CommandHandler("start", start)) - dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo)) - - updater.start_polling() - - updater.idle() - - if __name__ == '__main__': - main() - - -After using Telethon: - -.. code-block:: python - - from telethon import TelegramClient, events - - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def start(event): - """Send a message when the command /start is issued.""" - await event.respond('Hi!') - raise events.StopPropagation - - @bot.on(events.NewMessage) - async def echo(event): - """Echo the user message.""" - await event.respond(event.text) - - def main(): - """Start the bot.""" - bot.run_until_disconnected() - - if __name__ == '__main__': - main() - -Key differences: - -* The recommended way to do it imports fewer things. -* All handlers trigger by default, so we need ``events.StopPropagation``. -* Adding handlers, responding and running is a lot less verbose. -* Telethon needs ``async def`` and ``await``. -* The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``. - - -Migrating from pyTelegramBotAPI -------------------------------- - -Let's show another echobot from their README: - -.. code-block:: python - - import telebot - - bot = telebot.TeleBot("TOKEN") - - @bot.message_handler(commands=['start']) - def send_welcome(message): - bot.reply_to(message, "Howdy, how are you doing?") - - @bot.message_handler(func=lambda m: True) - def echo_all(message): - bot.reply_to(message, message.text) - - bot.polling() - -Now we rewrite it to use Telethon: - -.. code-block:: python - - from telethon import TelegramClient, events - - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def send_welcome(event): - await event.reply('Howdy, how are you doing?') - - @bot.on(events.NewMessage) - async def echo_all(event): - await event.reply(event.text) - - bot.run_until_disconnected() - -Key differences: - -* Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``. - Note that the ``event`` behaves just like their ``message``. -* Telethon also supports ``func=lambda m: True``, but it's not necessary. - - -Migrating from aiogram ----------------------- - -From their GitHub: - -.. code-block:: python - - from aiogram import Bot, Dispatcher, executor, types - - API_TOKEN = 'BOT TOKEN HERE' - - # Initialize bot and dispatcher - bot = Bot(token=API_TOKEN) - dp = Dispatcher(bot) - - @dp.message_handler(commands=['start']) - async def send_welcome(message: types.Message): - """ - This handler will be called when client send `/start` command. - """ - await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.") - - @dp.message_handler(regexp='(^cat[s]?$|puss)') - async def cats(message: types.Message): - with open('data/cats.jpg', 'rb') as photo: - await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺', - reply_to_message_id=message.message_id) - - @dp.message_handler() - async def echo(message: types.Message): - await bot.send_message(message.chat.id, message.text) - - if __name__ == '__main__': - executor.start_polling(dp, skip_updates=True) - - -After rewrite: - -.. code-block:: python - - from telethon import TelegramClient, events - - # Initialize bot and... just the bot! - bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - - @bot.on(events.NewMessage(pattern='/start')) - async def send_welcome(event): - await event.reply('Howdy, how are you doing?') - - @bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)')) - async def cats(event): - await event.reply('Cats is here 😺', file='data/cats.jpg') - - @bot.on(events.NewMessage) - async def echo_all(event): - await event.reply(event.text) - - if __name__ == '__main__': - bot.run_until_disconnected() - - -Key differences: - -* Telethon offers convenience methods to avoid retyping - ``bot.send_photo(message.chat.id, ...)`` all the time, - and instead let you type ``event.reply``. -* Sending files is **a lot** easier. The methods for sending - photos, documents, audios, etc. are all the same! - -Migrating from dumbot ---------------------- - -Showcasing their subclassing example: - -.. code-block:: python - - from dumbot import Bot - - class Subbot(Bot): - async def init(self): - self.me = await self.getMe() - - async def on_update(self, update): - await self.sendMessage( - chat_id=update.message.chat.id, - text='i am {}'.format(self.me.username) - ) - - Subbot(token).run() - -After rewriting: - -.. code-block:: python - - from telethon import TelegramClient, events - - class Subbot(TelegramClient): - def __init__(self, *a, **kw): - super().__init__(*a, **kw) - self.add_event_handler(self.on_update, events.NewMessage) - - async def connect(): - await super().connect() - self.me = await self.get_me() - - async def on_update(event): - await event.reply('i am {}'.format(self.me.username)) - - bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN') - bot.run_until_disconnected() - - -Key differences: - -* Telethon method names are ``snake_case``. -* dumbot does not offer friendly methods like ``update.reply``. -* Telethon does not have an implicit ``on_update`` handler, so - we need to manually register one. - - -.. _Telegram Bot API: https://core.telegram.org/bots/api -.. _Introduction to Bots: https://core.telegram.org/bots -.. _Bot FAQ: https://core.telegram.org/bots/faq -.. _tdlib: https://core.telegram.org/tdlib -.. _MTProto: https://core.telegram.org/mtproto -.. _MTProto vs HTTP Bot API: https://github.com/LonamiWebs/Telethon/wiki/MTProto-vs-HTTP-Bot-API -.. _requests: https://pypi.org/project/requests/ -.. _python-telegram-bot: https://python-telegram-bot.readthedocs.io -.. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI -.. _aiohttp: https://docs.aiohttp.org/en/stable -.. _aiogram: https://aiogram.readthedocs.io -.. _dumbot: https://github.com/Lonami/dumbot -.. _echobot.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py diff --git a/readthedocs/concepts/chats-vs-channels.rst b/readthedocs/concepts/chats-vs-channels.rst deleted file mode 100644 index 87281373..00000000 --- a/readthedocs/concepts/chats-vs-channels.rst +++ /dev/null @@ -1,169 +0,0 @@ -.. _chats-channels: - -================= -Chats vs Channels -================= - -Telegram's raw API can get very confusing sometimes, in particular when it -comes to talking about "chats", "channels", "groups", "megagroups", and all -those concepts. - -This section will try to explain what each of these concepts are. - - -Chats -===== - -A ``Chat`` can be used to talk about either the common "subclass" that both -chats and channels share, or the concrete :tl:`Chat` type. - -Technically, both :tl:`Chat` and :tl:`Channel` are a form of the `Chat type`_. - -**Most of the time**, the term :tl:`Chat` is used to talk about *small group -chats*. When you create a group through an official application, this is the -type that you get. Official applications refer to these as "Group". - -Both the bot API and Telethon will add a minus sign (negate) the real chat ID -so that you can tell at a glance, with just a number, the entity type. - -For example, if you create a chat with :tl:`CreateChatRequest`, the real chat -ID might be something like `123`. If you try printing it from a -`message.chat_id` you will see `-123`. This ID helps Telethon know you're -talking about a :tl:`Chat`. - - -Channels -======== - -Official applications create a *broadcast* channel when you create a new -channel (used to broadcast messages, only administrators can post messages). - -Official applications implicitly *migrate* an *existing* :tl:`Chat` to a -*megagroup* :tl:`Channel` when you perform certain actions (exceed user limit, -add a public username, set certain permissions, etc.). - -A ``Channel`` can be created directly with :tl:`CreateChannelRequest`, as -either a ``megagroup`` or ``broadcast``. - -Official applications use the term "channel" **only** for broadcast channels. - -The API refers to the different types of :tl:`Channel` with certain attributes: - -* A **broadcast channel** is a :tl:`Channel` with the ``channel.broadcast`` - attribute set to `True`. - -* A **megagroup channel** is a :tl:`Channel` with the ``channel.megagroup`` - attribute set to `True`. Official applications refer to this as "supergroup". - -* A **gigagroup channel** is a :tl:`Channel` with the ``channel.gigagroup`` - attribute set to `True`. Official applications refer to this as "broadcast - groups", and is used when a megagroup becomes very large and administrators - want to transform it into something where only they can post messages. - - -Both the bot API and Telethon will "concatenate" ``-100`` to the real chat ID -so that you can tell at a glance, with just a number, the entity type. - -For example, if you create a new broadcast channel, the real channel ID might -be something like `456`. If you try printing it from a `message.chat_id` you -will see `-1000000000456`. This ID helps Telethon know you're talking about a -:tl:`Channel`. - - -Converting IDs -============== - -You can convert between the "marked" identifiers (prefixed with a minus sign) -and the real ones with ``utils.resolve_id``. It will return a tuple with the -real ID, and the peer type (the class): - -.. code-block:: python - - from telethon import utils - real_id, peer_type = utils.resolve_id(-1000000000456) - - print(real_id) # 456 - print(peer_type) # - - peer = peer_type(real_id) - print(peer) # PeerChannel(channel_id=456) - - -The reverse operation can be done with ``utils.get_peer_id``: - -.. code-block:: python - - print(utils.get_peer_id(types.PeerChannel(456))) # -1000000000456 - - -Note that this function can also work with other types, like :tl:`Chat` or -:tl:`Channel` instances. - -If you need to convert other types like usernames which might need to perform -API calls to find out the identifier, you can use ``client.get_peer_id``: - - -.. code-block:: python - - print(await client.get_peer_id('me')) # your id - - -If there is no "mark" (no minus sign), Telethon will assume your identifier -refers to a :tl:`User`. If this is **not** the case, you can manually fix it: - - -.. code-block:: python - - from telethon import types - await client.send_message(types.PeerChannel(456), 'hello') - # ^^^^^^^^^^^^^^^^^ explicit peer type - - -A note on raw API -================= - -Certain methods only work on a :tl:`Chat`, and some others only work on a -:tl:`Channel` (and these may only work in broadcast, or megagroup). Your code -likely knows what it's working with, so it shouldn't be too much of an issue. - -If you need to find the :tl:`Channel` from a :tl:`Chat` that migrated to it, -access the `migrated_to` property: - -.. code-block:: python - - # chat is a Chat - channel = await client.get_entity(chat.migrated_to) - # channel is now a Channel - -Channels do not have a "migrated_from", but a :tl:`ChannelFull` does. You can -use :tl:`GetFullChannelRequest` to obtain this: - -.. code-block:: python - - from telethon import functions - full = await client(functions.channels.GetFullChannelRequest(your_channel)) - full_channel = full.full_chat - # full_channel is a ChannelFull - print(full_channel.migrated_from_chat_id) - -This way, you can also access the linked discussion megagroup of a broadcast channel: - -.. code-block:: python - - print(full_channel.linked_chat_id) # prints ID of linked discussion group or None - -You do not need to use ``client.get_entity`` to access the -``migrated_from_chat_id`` :tl:`Chat` or the ``linked_chat_id`` :tl:`Channel`. -They are in the ``full.chats`` attribute: - -.. code-block:: python - - if full_channel.migrated_from_chat_id: - migrated_from_chat = next(c for c in full.chats if c.id == full_channel.migrated_from_chat_id) - print(migrated_from_chat.title) - - if full_channel.linked_chat_id: - linked_group = next(c for c in full.chats if c.id == full_channel.linked_chat_id) - print(linked_group.username) - -.. _Chat type: https://tl.telethon.dev/types/chat.html diff --git a/readthedocs/concepts/entities.rst b/readthedocs/concepts/entities.rst deleted file mode 100644 index 40bfac30..00000000 --- a/readthedocs/concepts/entities.rst +++ /dev/null @@ -1,313 +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 - - # (These examples assume you are inside an "async def") - # - # Dialogs are the "conversations you have open". - # This method returns a list of Dialog, which - # has the .entity attribute and other information. - # - # This part is IMPORTANT, because it fills the entity cache. - dialogs = await client.get_dialogs() - - # All of these work and do the same. - username = await client.get_entity('username') - username = await client.get_entity('t.me/username') - username = await client.get_entity('https://telegram.dog/username') - - # Other kind of entities. - channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg') - contact = await client.get_entity('+34xxxxxxxxx') - friend = await client.get_entity(friend_id) - - # Getting entities through their ID (User, Chat or Channel) - entity = await client.get_entity(some_id) - - # You can be more explicit about the type for said ID by wrapping - # it inside a Peer instance. This is recommended but not necessary. - from telethon.tl.types import PeerUser, PeerChat, PeerChannel - - my_user = await client.get_entity(PeerUser(some_id)) - my_chat = await client.get_entity(PeerChat(some_id)) - my_channel = await client.get_entity(PeerChannel(some_id)) - - -.. note:: - - You **don't** need to get the entity before using it! Just let the - library do its job. Use a phone from your contacts, username, ID or - input entity (preferred but not necessary), whatever you already have. - -All methods in the :ref:`telethon-client` call `.get_input_entity() -` prior -to sending the request to save you from the hassle of doing so manually. -That way, convenience calls such as `client.send_message('username', 'hi!') -` -become possible. - -Every entity the library encounters (in any response to any call) will by -default be cached in the ``.session`` file (an SQLite database), to avoid -performing unnecessary API calls. If the entity cannot be found, additonal -calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be -made to obtain the required information. - - -Entities vs. Input Entities -=========================== - -.. note:: - - This section is informative, but worth reading. The library - will transparently handle all of these details for you. - -On top of the normal types, the API also make use of what they call their -``Input*`` versions of objects. The input version of an entity (e.g. -:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum -information that's required from Telegram to be able to identify -who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They -are named like this because they are input parameters in the requests. - -Entities' ID are the same for all user and bot accounts, however, the access -hash is **different for each account**, so trying to reuse the access hash -from one account in another will **not** work. - -Sometimes, Telegram only needs to indicate the type of the entity along -with their ID. For this purpose, :tl:`Peer` versions of the entities also -exist, which just have the ID. You cannot get the hash out of them since -you should not be needing it. The library probably has cached it before. - -Peers are enough to identify an entity, but they are not enough to make -a request with them. You need to know their hash before you can -"use them", and to know the hash you need to "encounter" them, let it -be in your dialogs, participants, message forwards, etc. - -.. note:: - - You *can* use peers with the library. Behind the scenes, they are - replaced with the input variant. Peers "aren't enough" on their own - but the library will do some more work to use the right type. - -As we just mentioned, API calls don't need to know the whole information -about the entities, only their ID and hash. For this reason, another method, -`client.get_input_entity() ` -is available. This will always use the cache while possible, making zero API -calls most of the time. When a request is made, if you provided the full -entity, e.g. an :tl:`User`, the library will convert it to the required -:tl:`InputPeer` automatically for you. - -**You should always favour** -`client.get_input_entity() ` -**over** -`client.get_entity() ` -for this reason! Calling the latter will always make an API call to get -the most recent information about said entity, but invoking requests don't -need this information, just the :tl:`InputPeer`. Only use -`client.get_entity() ` -if you need to get actual information, like the username, name, title, etc. -of the entity. - -To further simplify the workflow, since the version ``0.16.2`` of the -library, the raw requests you make to the API are also able to call -`client.get_input_entity() ` -wherever needed, so you can even do things like: - -.. code-block:: python - - await client(SendMessageRequest('username', 'hello')) - -The library will call the ``.resolve()`` method of the request, which will -resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if -you don't get this yet, but remember some of the details here are important. - - -Full Entities -============= - -In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its -variants for chats and channels), there is also the concept of :tl:`UserFull`. - -This full variant has additional information such as whether the user is -blocked, its notification settings, the bio or about of the user, etc. - -There is also :tl:`messages.ChatFull` which is the equivalent of full entities -for chats and channels, with also the about section of the channel. Note that -the ``users`` field only contains bots for the channel (so that clients can -suggest commands to use). - -You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat` -and :tl:`GetFullChannel` respectively. - - -Accessing Entities -================== - -Although it's explicitly noted in the documentation that messages -*subclass* `ChatGetter ` -and `SenderGetter `, -some people still don't get inheritance. - -When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`" -it means that the class you're looking at, *also* can act as the class it -bases. In this case, `ChatGetter ` -knows how to get the *chat* where a thing belongs to. - -So, a `Message ` is a -`ChatGetter `. -That means you can do this: - -.. code-block:: python - - message.is_private - message.chat_id - await message.get_chat() - # ...etc - -`SenderGetter ` is similar: - -.. code-block:: python - - message.user_id - await message.get_input_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 - - # (These examples assume you are inside an "async def") - async with client: - # Does it have a username? Use it! - entity = await client.get_entity(username) - - # Do you have a conversation open with them? Get dialogs. - await client.get_dialogs() - - # Are they participant of some group? Get them. - await client.get_participants('username') - - # Is the entity the original sender of a forwarded message? Get it. - await client.get_messages('username', 100) - - # NOW you can use the ID, anywhere! - await client.send_message(123456, 'Hi!') - - entity = await client.get_entity(123456) - print(entity) - -Once the library has "seen" the entity, you can use their **integer** ID. -You can't use entities from IDs the library hasn't seen. You must make the -library see them *at least once* and disconnect properly. You know where -the entities are and you must tell the library. It won't guess for you. diff --git a/readthedocs/concepts/errors.rst b/readthedocs/concepts/errors.rst deleted file mode 100644 index 5a847240..00000000 --- a/readthedocs/concepts/errors.rst +++ /dev/null @@ -1,155 +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). - -You should import the errors from ``telethon.errors`` like so: - -.. code-block:: python - - from telethon import errors - - try: - async with client.takeout() as takeout: - ... - - except errors.TakeoutInitDelayError as e: - # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ here we except TAKEOUT_INIT_DELAY - print('Must wait', e.seconds, 'before takeout') - - -There isn't any official list of all possible RPC errors, so the -`list of known errors`_ is provided on a best-effort basis. When new methods -are available, the list may be lacking since we simply don't know what errors -can raise from them. - -Once we do find out about a new error and what causes it, the list is -updated, so if you see an error without a specific class, do report it -(and what method caused it)!. - -This list is used to generate documentation for the `raw API page`_. -For example, if we want to know what errors can occur from -`messages.sendMessage`_ we can simply navigate to its raw API page -and find it has 24 known RPC errors at the time of writing. - - -Base Errors -=========== - -All the "base" errors are listed in :ref:`telethon-errors`. -Any other more specific error will be a subclass of these. - -If the library isn't aware of a specific error just yet, it will instead -raise one of these superclasses. This means you may find stuff like this: - -.. code-block:: text - - telethon.errors.rpcbaseerrors.BadRequestError: RPCError 400: MESSAGE_POLL_CLOSED (caused by SendVoteRequest) - -If you do, make sure to open an issue or send a pull request to update the -`list of known errors`_. - - -Common Errors -============= - -These are some of the errors you may normally need to deal with: - -- ``FloodWaitError`` (420), the same request was repeated many times. - Must wait ``.seconds`` (you can access this attribute). For example: - - .. code-block:: python - - ... - from telethon import errors - - try: - messages = await client.get_messages(chat) - print(messages[0].text) - except errors.FloodWaitError as e: - print('Have to sleep', e.seconds, 'seconds') - time.sleep(e.seconds) - -- ``SessionPasswordNeededError``, if you have setup two-steps - verification on Telegram and are trying to sign in. -- ``FilePartMissingError``, if you have tried to upload an empty file. -- ``ChatAdminRequiredError``, you don't have permissions to perform - said operation on a chat or channel. Try avoiding filters, i.e. when - searching messages. - -The generic classes for different error codes are: - -- ``InvalidDCError`` (303), the request must be repeated on another DC. -- ``BadRequestError`` (400), the request contained errors. -- ``UnauthorizedError`` (401), the user is not authorized yet. -- ``ForbiddenError`` (403), privacy violation error. -- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's! - -If the error is not recognised, it will only be an ``RPCError``. - -You can refer to all errors from Python through the ``telethon.errors`` -module. If you don't know what attributes they have, try printing their -dir (like ``print(dir(e))``). - - -Attributes -========== - -Some of the errors carry additional data in them. When they look like -``EMAIL_UNCONFIRMED_X``, the ``_X`` value will be accessible from the -error instance. The current list of errors that do this is the following: - -- ``EmailUnconfirmedError`` has ``.code_length``. -- ``FileMigrateError`` has ``.new_dc``. -- ``FilePartMissingError`` has ``.which``. -- ``FloodTestPhoneWaitError`` has ``.seconds``. -- ``FloodWaitError`` has ``.seconds``. -- ``InterdcCallErrorError`` has ``.dc``. -- ``InterdcCallRichErrorError`` has ``.dc``. -- ``NetworkMigrateError`` has ``.new_dc``. -- ``PhoneMigrateError`` has ``.new_dc``. -- ``SlowModeWaitError`` has ``.seconds``. -- ``TakeoutInitDelayError`` has ``.seconds``. -- ``UserMigrateError`` has ``.new_dc``. - - -Avoiding Limits -=============== - -Don't spam. You won't get ``FloodWaitError`` or your account banned or -deleted if you use the library *for legit use cases*. Make cool tools. -Don't spam! Nobody knows the exact limits for all requests since they -depend on a lot of factors, so don't bother asking. - -Still, if you do have a legit use case and still get those errors, the -library will automatically sleep when they are smaller than 60 seconds -by default. You can set different "auto-sleep" thresholds: - -.. code-block:: python - - client.flood_sleep_threshold = 0 # Don't auto-sleep - client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always - -You can also except it and act as you prefer: - -.. code-block:: python - - from telethon.errors import FloodWaitError - try: - ... - except FloodWaitError as e: - print('Flood waited for', e.seconds) - quit(1) - -VoIP numbers are very limited, and some countries are more limited too. - - -.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv -.. _raw API page: https://tl.telethon.dev/ -.. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html diff --git a/readthedocs/concepts/full-api.rst b/readthedocs/concepts/full-api.rst deleted file mode 100644 index ba5755d0..00000000 --- a/readthedocs/concepts/full-api.rst +++ /dev/null @@ -1,420 +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. - -.. contents:: - - -Introduction -============ - -The :ref:`telethon-client` doesn't offer a method for every single request -the Telegram API supports. However, it's very simple to *call* or *invoke* -any request defined in Telegram's API. - -This section will teach you how to use what Telethon calls the `TL reference`_. -The linked page contains a list and a way to search through *all* types -generated from the definition of Telegram's API (in ``.tl`` file format, -hence the name). These types include requests and constructors. - -.. note:: - - The reason to keep both https://tl.telethon.dev and this - documentation alive is that the former allows instant search results - as you type, and a "Copy import" button. If you like namespaces, you - can also do ``from telethon.tl import types, functions``. Both work. - -Telegram makes these ``.tl`` files public, which other implementations, such -as Telethon, can also use to generate code. These files are versioned under -what's called "layers". ``.tl`` files consist of thousands of definitions, -and newer layers often add, change, or remove them. Each definition refers -to either a Remote Procedure Call (RPC) function, or a type (which the -`TL reference`_ calls "constructors", as they construct particular type -instances). - -As such, the `TL reference`_ is a good place to go to learn about all possible -requests, types, and what they look like. If you're curious about what's been -changed between layers, you can refer to the `TL diff`_ site. - - -Navigating the TL reference -=========================== - -Functions ---------- - -"Functions" is the term used for the Remote Procedure Calls (RPC) that can be -sent to Telegram to ask it to perform something (e.g. "send message"). These -requests have an associated return type. These can be invoked ("called"): - -.. code-block:: python - - client = TelegramClient(...) - function_instance = SomeRequest(...) - - # Invoke the request - returned_type = await client(function_instance) - -Whenever you find the type for a function in the `TL reference`_, the page -will contain the following information: - -* What type of account can use the method. This information is regenerated - from time to time (by attempting to invoke the function under both account - types and finding out where it fails). Some requests can only be used by - bot accounts, others by user accounts, and others by both. -* The TL definition. This helps you get a feel for the what the function - looks like. This is not Python code. It just contains the definition in - a concise manner. -* "Copy import" button. Does what it says: it will copy the necessary Python - code to import the function to your system's clipboard for easy access. -* Returns. The returned type. When you invoke the function, this is what the - result will be. It also includes which of the constructors can be returned - inline, to save you a click. -* Parameters. The parameters accepted by the function, including their type, - whether they expect a list, and whether they're optional. -* Known RPC errors. A best-effort list of known errors the request may cause. - This list is not complete and may be out of date, but should provide an - overview of what could go wrong. -* Example. Autogenerated example, showcasing how you may want to call it. - Bear in mind that this is *autogenerated*. It may be spitting out non-sense. - The goal of this example is not to show you everything you can do with the - request, only to give you a feel for what it looks like to use it. - -It is very important to click through the links and navigate to get the full -picture. A specific page will show you what the specific function returns and -needs as input parameters. But it may reference other types, so you need to -navigate to those to learn what those contain or need. - -Types ------ - -"Types" as understood by TL are not actually generated in Telethon. -They would be the "abstract base class" of the constructors, but since Python -is duck-typed, there is hardly any need to generate mostly unnecessary code. -The page for a type contains: - -* Constructors. Every type will have one or more constructors. These - constructors *are* generated and can be immported and used. -* Requests returning this type. A helpful way to find out "what requests can - return this?". This is how you may learn what request you need to use to - obtain a particular instance of a type. -* Requests accepting this type as input. A helpful way to find out "what - requests can use this type as one of their input parameters?". This is how - you may learn where a type is used. -* Other types containing this type. A helpful way to find out "where else - does this type appear?". This is how you can walk back through nested - objects. - -Constructors ------------- - -Constructors are used to create instances of a particular type, and are also -returned when invoking requests. You will have to create instances yourself -when invoking requests that need a particular type as input. -The page for a constructor contains: - -* Belongs to. The parent type. This is a link back to the types page for the - specific constructor. It also contains the sibling constructors inline, to - save you a click. -* Members. Both the input parameters *and* fields the constructor contains. - - -Using the TL reference -====================== - -After you've found a request you want to send, a good start would be to simply -copy and paste the autogenerated example into your script. Then you can simply -tweak it to your needs. - -If you want to do it from scratch, first, make sure to import the request into -your code (either using the "Copy import" button near the top, or by manually -spelling out the package under ``telethon.tl.functions.*``). - -Then, start reading the parameters one by one. If the parameter cannot be -omitted, you **will** need to specify it, so make sure to spell it out as -an input parameter when constructing the request instance. Let's look at -`PingRequest`_ for example. First, we copy the import: - -.. code-block:: python - - from telethon.tl.functions import PingRequest - -Then, we look at the parameters: - - ping_id - long - -A single parameter, and it's a long (a integer number with a large range of -values). It doesn't say it can be omitted, so we must provide it, like so: - -.. code-block:: python - - PingRequest( - ping_id=48641868471 - ) - -(In this case, the ping ID is a random number. You often have to guess what -the parameter needs just by looking at the name.) - -Now that we have our request, we can invoke it: - -.. code-block:: python - - response = await client(PingRequest( - ping_id=48641868471 - )) - -To find out what ``response`` looks like, we can do as the autogenerated -example suggests and "stringify" the result as a pretty-printed string: - -.. code-block:: python - - print(result.stringify()) - -This will print out the following: - -.. code-block:: python - - Pong( - msg_id=781875678118, - ping_id=48641868471 - ) - -Which is a very easy way to get a feel for a response. You should nearly -always print the stringified result, at least once, when trying out requests, -to get a feel for what the response may look like. - -But of course, you don't need to do that. Without writing any code, you could -have navigated through the "Returns" link to learn ``PingRequest`` returns a -``Pong``, which only has one constructor, and the constructor has two members, -``msg_id`` and ``ping_id``. - -If you wanted to create your own ``Pong``, you would use both members as input -parameters: - -.. code-block:: python - - my_pong = Pong( - msg_id=781875678118, - ping_id=48641868471 - ) - -(Yes, constructing object instances can use the same code that ``.stringify`` -would return!) - -And if you wanted to access the ``msg_id`` member, you would simply access it -like any other attribute access in Python: - -.. code-block:: python - - print(response.msg_id) - - -Example walkthrough -=================== - -Say `client.send_message() -` didn't exist, -we could `use the search`_ to look for "message". There we would find -:tl:`SendMessageRequest`, which we can work with. - -Every request is a Python class, and has the parameters needed for you -to invoke it. You can also call ``help(request)`` for information on -what input parameters it takes. Remember to "Copy import to the -clipboard", or your script won't be aware of this class! Now we have: - -.. code-block:: python - - from telethon.tl.functions.messages import SendMessageRequest - -If you're going to use a lot of these, you may do: - -.. code-block:: python - - from telethon.tl import types, functions - # We now have access to 'functions.messages.SendMessageRequest' - -We see that this request must take at least two parameters, a ``peer`` -of type :tl:`InputPeer`, and a ``message`` which is just a Python -`str`\ ing. - -How can we retrieve this :tl:`InputPeer`? We have two options. We manually -construct one, for instance: - -.. code-block:: python - - from telethon.tl.types import InputPeerUser - - peer = InputPeerUser(user_id, user_hash) - -Or we call `client.get_input_entity() -`: - -.. code-block:: python - - import telethon - - async def main(): - peer = await client.get_input_entity('someone') - - client.loop.run_until_complete(main()) - -.. note:: - - Remember that ``await`` must occur inside an ``async def``. - Every full API example assumes you already know and do this. - - -When you're going to invoke an API method, most require you to pass an -:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using -`client.get_input_entity() ` -is more straightforward (and often immediate, if you've seen the user before, -know their ID, etc.). If you also **need** to have information about the whole -user, use `client.get_entity() ` -instead: - -.. code-block:: python - - entity = await client.get_entity('someone') - -In the later case, when you use the entity, the library will cast it to -its "input" version for you. If you already have the complete user and -want to cache its input version so the library doesn't have to do this -every time its used, simply call `telethon.utils.get_input_peer`: - -.. code-block:: python - - from telethon import utils - peer = utils.get_input_peer(entity) - - -.. note:: - - Since ``v0.16.2`` this is further simplified. The ``Request`` itself - will call `client.get_input_entity - ` for you when - required, but it's good to remember what's happening. - -After this small parenthesis about `client.get_entity -` versus -`client.get_input_entity() `, -we have everything we need. To invoke our -request we do: - -.. code-block:: python - - result = await client(SendMessageRequest(peer, 'Hello there!')) - -Message sent! Of course, this is only an example. There are over 250 -methods available as of layer 80, and you can use every single of them -as you wish. Remember to use the right types! To sum up: - -.. code-block:: python - - result = await client(SendMessageRequest( - await client.get_input_entity('username'), 'Hello there!' - )) - - -This can further be simplified to: - -.. code-block:: python - - result = await client(SendMessageRequest('username', 'Hello there!')) - # Or even - result = await client(SendMessageRequest(PeerChannel(id), 'Hello there!')) - -.. note:: - - Note that some requests have a "hash" parameter. This is **not** - your ``api_hash``! It likely isn't your self-user ``.access_hash`` either. - - It's a special hash used by Telegram to only send a difference of new data - that you don't already have with that request, so you can leave it to 0, - and it should work (which means no hash is known yet). - - For those requests having a "limit" parameter, you can often set it to - zero to signify "return default amount". This won't work for all of them - though, for instance, in "messages.search" it will actually return 0 items. - - -Requests in Parallel -==================== - -The library will automatically merge outgoing requests into a single -*container*. Telegram's API supports sending multiple requests in a -single container, which is faster because it has less overhead and -the server can run them without waiting for others. You can also -force using a container manually: - -.. code-block:: python - - async def main(): - - # Letting the library do it behind the scenes - await asyncio.wait([ - client.send_message('me', 'Hello'), - client.send_message('me', ','), - client.send_message('me', 'World'), - client.send_message('me', '.') - ]) - - # Manually invoking many requests at once - await client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ', '), - SendMessageRequest('me', 'World'), - SendMessageRequest('me', '.') - ]) - -Note that you cannot guarantee the order in which they are run. -Try running the above code more than one time. You will see the -order in which the messages arrive is different. - -If you use the raw API (the first option), you can use ``ordered`` -to tell the server that it should run the requests sequentially. -This will still be faster than going one by one, since the server -knows all requests directly: - -.. code-block:: python - - await client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ', '), - SendMessageRequest('me', 'World'), - SendMessageRequest('me', '.') - ], ordered=True) - -If any of the requests fails with a Telegram error (not connection -errors or any other unexpected events), the library will raise -`telethon.errors.common.MultiError`. You can ``except`` this -and still access the successful results: - -.. code-block:: python - - from telethon.errors import MultiError - - try: - await client([ - SendMessageRequest('me', 'Hello'), - SendMessageRequest('me', ''), - SendMessageRequest('me', 'World') - ], ordered=True) - except MultiError as e: - # The first and third requests worked. - first = e.results[0] - third = e.results[2] - # The second request failed. - second = e.exceptions[1] - -.. _TL reference: https://tl.telethon.dev -.. _TL diff: https://diff.telethon.dev -.. _PingRequest: https://tl.telethon.dev/methods/ping.html -.. _use the search: https://tl.telethon.dev/?q=message&redirect=no diff --git a/readthedocs/concepts/sessions.rst b/readthedocs/concepts/sessions.rst deleted file mode 100644 index 4ccd04c4..00000000 --- a/readthedocs/concepts/sessions.rst +++ /dev/null @@ -1,165 +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. - -While it's often not the case, it's possible that SQLite is slow enough to -be noticeable, in which case you can also use a different storage. Note that -this is rare and most people won't have this issue, but it's worth a mention. - -To use a custom session storage, simply pass the custom session instance to -:ref:`TelegramClient ` instead of -the session name. - -Telethon contains three implementations of the abstract ``Session`` class: - -.. currentmodule:: telethon.sessions - -* `MemorySession `: stores session data within memory. -* `SQLiteSession `: stores sessions within on-disk SQLite databases. Default. -* `StringSession `: stores session data within memory, - but can be saved as a string. - -You can import these ``from telethon.sessions``. For example, using the -`StringSession ` is done as follows: - -.. code-block:: python - - from telethon.sync import TelegramClient - from telethon.sessions import StringSession - - with TelegramClient(StringSession(string), api_id, api_hash) as client: - ... # use the client - - # Save the string session as a string; you should decide how - # you want to save this information (over a socket, remote - # database, print it and then paste the string in the code, - # etc.); the advantage is that you don't need to save it - # on the current disk as a separate file, and can be reused - # anywhere else once you log in. - string = client.session.save() - - # Note that it's also possible to save any other session type - # as a string by using ``StringSession.save(session_instance)``: - client = TelegramClient('sqlite-session', api_id, api_hash) - string = StringSession.save(client.session) - -There are other community-maintained implementations available: - -* `SQLAlchemy `_: - stores all sessions in a single database via SQLAlchemy. - -* `Redis `_: - stores all sessions in a single Redis data store. - -* `MongoDB `_: - stores the current session in a MongoDB database. - - -Creating your Own Storage -========================= - -The easiest way to create your own storage implementation is to use -`MemorySession ` as the base and check out how -`SQLiteSession ` or one of the community-maintained -implementations work. You can find the relevant Python files under the -``sessions/`` directory in the Telethon's repository. - -After you have made your own implementation, you can add it to the -community-maintained session implementation list above with a pull request. - - -String Sessions -=============== - -`StringSession ` are a convenient way to embed your -login credentials directly into your code for extremely easy portability, -since all they take is a string to be able to login without asking for your -phone and code (or faster start if you're using a bot token). - -The easiest way to generate a string session is as follows: - -.. code-block:: python - - from telethon.sync import TelegramClient - from telethon.sessions import StringSession - - with TelegramClient(StringSession(), api_id, api_hash) as client: - print(client.session.save()) - - -Think of this as a way to export your authorization key (what's needed -to login into your account). This will print a string in the standard -output (likely your terminal). - -.. warning:: - - **Keep this string safe!** Anyone with this string can use it - to login into your account and do anything they want to. - - This is similar to leaking your ``*.session`` files online, - but it is easier to leak a string than it is to leak a file. - - -Once you have the string (which is a bit long), load it into your script -somehow. You can use a normal text file and ``open(...).read()`` it or -you can save it in a variable directly: - -.. code-block:: python - - string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...' - with TelegramClient(StringSession(string), api_id, api_hash) as client: - client.loop.run_until_complete(client.send_message('me', 'Hi')) - - -These strings are really convenient for using in places like Heroku since -their ephemeral filesystem will delete external files once your application -is over. diff --git a/readthedocs/concepts/strings.rst b/readthedocs/concepts/strings.rst deleted file mode 100644 index a696b684..00000000 --- a/readthedocs/concepts/strings.rst +++ /dev/null @@ -1,88 +0,0 @@ -====================== -String-based Debugging -====================== - -Debugging is *really* important. Telegram's API is really big and there -are a lot of things that you should know. Such as, what attributes or fields -does a result have? Well, the easiest thing to do is printing it: - -.. code-block:: python - - entity = await client.get_entity('username') - print(entity) - -That will show a huge **string** similar to the following: - -.. code-block:: python - - Channel(id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty(), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None) - -That's a lot of text. But as you can see, all the properties are there. -So if you want the title you **don't use regex** or anything like -splitting ``str(entity)`` to get what you want. You just access the -attribute you need: - -.. code-block:: python - - title = entity.title - -Can we get better than the shown string, though? Yes! - -.. code-block:: python - - print(entity.stringify()) - -Will show a much better representation: - -.. code-block:: python - - Channel( - id=1066197625, - title='Telegram Usernames', - photo=ChatPhotoEmpty( - ), - date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), - version=0, - creator=False, - left=True, - broadcast=True, - verified=True, - megagroup=False, - restricted=False, - signatures=False, - min=False, - scam=False, - has_link=False, - has_geo=False, - slowmode_enabled=False, - access_hash=-6309373984955162244, - username='username', - restriction_reason=[ - ], - admin_rights=None, - banned_rights=None, - default_banned_rights=None, - participants_count=None - ) - - -Now it's easy to see how we could get, for example, -the ``year`` value. It's inside ``date``: - -.. code-block:: python - - channel_year = entity.date.year - -You don't need to print everything to see what all the possible values -can be. You can just search in http://tl.telethon.dev/. - -Remember that you can use Python's `isinstance -`_ -to check the type of something. For example: - -.. code-block:: python - - from telethon import types - - if isinstance(entity.photo, types.ChatPhotoEmpty): - print('Channel has no photo') diff --git a/readthedocs/concepts/updates.rst b/readthedocs/concepts/updates.rst deleted file mode 100644 index 249d143e..00000000 --- a/readthedocs/concepts/updates.rst +++ /dev/null @@ -1,228 +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 - - asyncio.run(main()) - - -You could also run `client.disconnected -` -until it completed. - -But if you don't want to ``await``, then you should know what you want -to be doing instead! What matters is that you shouldn't let your script -die. If you don't care about updates, you don't need any of this. - -Notice that unlike `client.disconnected -`, -`client.run_until_disconnected -` will -handle ``KeyboardInterrupt`` for you. This method is special and can -also be ran while the loop is running, so you can do this: - -.. code-block:: python - - async def main(): - await client.run_until_disconnected() - - loop.run_until_complete(main()) - -Sequential Updates -================== - -If you need to process updates sequentially (i.e. not in parallel), -you should set ``sequential_updates=True`` when creating the client: - -.. code-block:: python - - with TelegramClient(..., sequential_updates=True) as client: - ... diff --git a/readthedocs/conf.py b/readthedocs/conf.py deleted file mode 100644 index c5afe7b4..00000000 --- a/readthedocs/conf.py +++ /dev/null @@ -1,211 +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 = 'en' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'friendly' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -def skip(app, what, name, obj, would_skip, options): - if name.endswith('__'): - # We want to show special methods names, except some which add clutter - return name in { - '__init__', - '__abstractmethods__', - '__module__', - '__doc__', - '__dict__' - } - - return would_skip - - -def setup(app): - app.connect("autodoc-skip-member", skip) - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - 'collapse_navigation': True, - 'display_version': True, - 'navigation_depth': 3, -} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'globaltoc.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Telethondoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'Telethon.tex', 'Telethon Documentation', - author, 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'telethon', 'Telethon Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'Telethon', 'Telethon Documentation', - author, 'Telethon', 'One line description of project.', - 'Miscellaneous'), -] - diff --git a/readthedocs/custom_roles.py b/readthedocs/custom_roles.py deleted file mode 100644 index bf025fb8..00000000 --- a/readthedocs/custom_roles.py +++ /dev/null @@ -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 deleted file mode 100644 index c629034c..00000000 --- a/readthedocs/developing/coding-style.rst +++ /dev/null @@ -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 deleted file mode 100644 index f779be2b..00000000 --- a/readthedocs/developing/philosophy.rst +++ /dev/null @@ -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 deleted file mode 100644 index a3544a91..00000000 --- a/readthedocs/developing/project-structure.rst +++ /dev/null @@ -1,51 +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. - -If you clone the repository, you will have to run ``python setup.py gen`` -in order to generate the code. Installing the library runs the generator -too, but the mentioned command will just generate code. diff --git a/readthedocs/developing/telegram-api-in-other-languages.rst b/readthedocs/developing/telegram-api-in-other-languages.rst deleted file mode 100644 index 4e54126e..00000000 --- a/readthedocs/developing/telegram-api-in-other-languages.rst +++ /dev/null @@ -1,13 +0,0 @@ -=============================== -Telegram API in Other Languages -=============================== - -Telethon was made for **Python**, and it has inspired other libraries such as -`gramjs `__ (JavaScript) and `grammers -`__ (Rust). But there is a lot more beyond -those, made independently by different developers. - -If you're looking for something like Telethon but in a different programming -language, head over to `Telegram API in Other Languages in the official wiki -`__ -for a (mostly) up-to-date list. diff --git a/readthedocs/developing/test-servers.rst b/readthedocs/developing/test-servers.rst deleted file mode 100644 index 513eed54..00000000 --- a/readthedocs/developing/test-servers.rst +++ /dev/null @@ -1,41 +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' - ) - -Note that Telegram has changed the length of login codes multiple times in the -past, so if ``dc_id`` repeated five times does not work, try repeating it six -times. diff --git a/readthedocs/developing/testing.rst b/readthedocs/developing/testing.rst deleted file mode 100644 index badb7dc6..00000000 --- a/readthedocs/developing/testing.rst +++ /dev/null @@ -1,87 +0,0 @@ -===== -Tests -===== - -Telethon uses `Pytest `__, for testing, `Tox -`__ for environment setup, and -`pytest-asyncio `__ and `pytest-cov -`__ for asyncio and -`coverage `__ integration. - -While reading the full documentation for these is probably a good idea, there -is a lot to read, so a brief summary of these tools is provided below for -convienience. - -Brief Introduction to Pytest -============================ - -`Pytest `__ is a tool for discovering and running python -tests, as well as allowing modular reuse of test setup code using fixtures. - -Most Pytest tests will look something like this:: - - from module import my_thing, my_other_thing - - def test_my_thing(fixture): - assert my_thing(fixture) == 42 - - @pytest.mark.asyncio - async def test_my_thing(event_loop): - assert await my_other_thing(loop=event_loop) == 42 - -Note here: - - 1. The test imports one specific function. The role of unit tests is to test - that the implementation of some unit, like a function or class, works. - It's role is not so much to test that components interact well with each - other. I/O, such as connecting to remote servers, should be avoided. This - helps with quickly identifying the source of an error, finding silent - breakage, and makes it easier to cover all possible code paths. - - System or integration tests can also be useful, but are currently out of - scope of Telethon's automated testing. - - 2. A function ``test_my_thing`` is declared. Pytest searches for files - starting with ``test_``, classes starting with ``Test`` and executes any - functions or methods starting with ``test_`` it finds. - - 3. The function is declared with a parameter ``fixture``. Fixtures are used to - request things required to run the test, such as temporary directories, - free TCP ports, Connections, etc. Fixtures are declared by simply adding - the fixture name as parameter. A full list of available fixtures can be - found with the ``pytest --fixtures`` command. - - 4. The test uses a simple ``assert`` to test some condition is valid. Pytest - uses some magic to ensure that the errors from this are readable and easy - to debug. - - 5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It - starts a loop and executes a test function as coroutine. This should be - used for testing asyncio code. It also declares the ``event_loop`` - fixture, which will request an ``asyncio`` event loop. - -Brief Introduction to Tox -========================= - -`Tox `__ is a tool for automated setup -of virtual environments for testing. While the tests can be run directly by -just running ``pytest``, this only tests one specific python version in your -existing environment, which will not catch e.g. undeclared dependencies, or -version incompatabilities. - -Tox environments are declared in the ``tox.ini`` file. The default -environments, declared at the top, can be simply run with ``tox``. The option -``tox -e py36,flake`` can be used to request specific environments to be run. - -Brief Introduction to Pytest-cov -================================ - -Coverage is a useful metric for testing. It measures the lines of code and -branches that are exercised by the tests. The higher the coverage, the more -likely it is that any coding errors will be caught by the tests. - -A brief coverage report can be generated with the ``--cov`` option to ``tox``, -which will be passed on to ``pytest``. Additionally, the very useful HTML -report can be generated with ``--cov --cov-report=html``, which contains a -browsable copy of the source code, annotated with coverage information for each -line. diff --git a/readthedocs/developing/tips-for-porting-the-project.rst b/readthedocs/developing/tips-for-porting-the-project.rst deleted file mode 100644 index 69348f9d..00000000 --- a/readthedocs/developing/tips-for-porting-the-project.rst +++ /dev/null @@ -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 deleted file mode 100644 index 8e5259a7..00000000 --- a/readthedocs/developing/understanding-the-type-language.rst +++ /dev/null @@ -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 deleted file mode 100644 index 2c3823c3..00000000 --- a/readthedocs/examples/chats-and-channels.rst +++ /dev/null @@ -1,128 +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 - await client(JoinChannelRequest(channel)) - - # In the same way, you can also leave such channel - from telethon.tl.functions.channels import LeaveChannelRequest - await client(LeaveChannelRequest(input_channel)) - - -For more on channels, check the `channels namespace`__. - - -__ https://tl.telethon.dev/methods/channels/index.html - - -Joining a private chat or channel -================================= - -If all you have is a link like this one: -``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have -enough information to join! The part after the -``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this -example, is the ``hash`` of the chat or channel. Now you can use -:tl:`ImportChatInviteRequest` as follows: - -.. code-block:: python - - from telethon.tl.functions.messages import ImportChatInviteRequest - updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) - - -Adding someone else to such chat or channel -=========================================== - -If you don't want to add yourself, maybe because you're already in, -you can always add someone else with the :tl:`AddChatUserRequest`, which -use is very straightforward, or :tl:`InviteToChannelRequest` for channels: - -.. code-block:: python - - # For normal chats - from telethon.tl.functions.messages import AddChatUserRequest - - # Note that ``user_to_add`` is NOT the name of the parameter. - # It's the user you want to add (``user_id=user_to_add``). - await client(AddChatUserRequest( - chat_id, - user_to_add, - fwd_limit=10 # Allow the user to see the 10 last messages - )) - - # For channels (which includes megagroups) - from telethon.tl.functions.channels import InviteToChannelRequest - - await client(InviteToChannelRequest( - channel, - [users_to_add] - )) - -Note that this method will only really work for friends or bot accounts. -Trying to mass-add users with this approach will not work, and can put both -your account and group to risk, possibly being flagged as spam and limited. - - -Checking a link without joining -=============================== - -If you don't need to join but rather check whether it's a group or a -channel, you can use the :tl:`CheckChatInviteRequest`, which takes in -the hash of said channel or group. - - -Increasing View Count in a Channel -================================== - -It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and -while I don't understand why so many people ask this, the solution is to -use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: - -.. code-block:: python - - - # Obtain `channel' through dialogs or through client.get_entity() or anyhow. - # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. - - await client(GetMessagesViewsRequest( - peer=channel, - id=msg_ids, - increment=True - )) - - -Note that you can only do this **once or twice a day** per account, -running this in a loop will obviously not increase the views forever -unless you wait a day between each iteration. If you run it any sooner -than that, the views simply won't be increased. - -__ https://github.com/LonamiWebs/Telethon/issues/233 -__ https://github.com/LonamiWebs/Telethon/issues/305 -__ https://github.com/LonamiWebs/Telethon/issues/409 -__ https://github.com/LonamiWebs/Telethon/issues/447 diff --git a/readthedocs/examples/users.rst b/readthedocs/examples/users.rst deleted file mode 100644 index ea83871d..00000000 --- a/readthedocs/examples/users.rst +++ /dev/null @@ -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 = await client(GetFullUserRequest(user)) - # or even - full = await client(GetFullUserRequest('username')) - - bio = full.full_user.about - - -See :tl:`UserFull` to know what other fields you can access. - - -Updating your name and/or bio -============================= - -The first name, last name and bio (about) can all be changed with the same -request. Omitted fields won't change after invoking :tl:`UpdateProfile`: - -.. code-block:: python - - from telethon.tl.functions.account import UpdateProfileRequest - - await client(UpdateProfileRequest( - about='This is a test from Telethon' - )) - - -Updating your username -====================== - -You need to use :tl:`account.UpdateUsername`: - -.. code-block:: python - - from telethon.tl.functions.account import UpdateUsernameRequest - - await client(UpdateUsernameRequest('new_username')) - - -Updating your profile photo -=========================== - -The easiest way is to upload a new file and use that as the profile photo -through :tl:`UploadProfilePhoto`: - - -.. code-block:: python - - from telethon.tl.functions.photos import UploadProfilePhotoRequest - - await client(UploadProfilePhotoRequest( - await client.upload_file('/path/to/some/file') - )) diff --git a/readthedocs/examples/word-of-warning.rst b/readthedocs/examples/word-of-warning.rst deleted file mode 100644 index de91741f..00000000 --- a/readthedocs/examples/word-of-warning.rst +++ /dev/null @@ -1,17 +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 the -`wiki page of projects using Telethon `__. diff --git a/readthedocs/examples/working-with-messages.rst b/readthedocs/examples/working-with-messages.rst deleted file mode 100644 index 13ccea10..00000000 --- a/readthedocs/examples/working-with-messages.rst +++ /dev/null @@ -1,13 +0,0 @@ -===================== -Working with messages -===================== - -.. note:: - - These examples assume you have read :ref:`full-api`. - -This section has been `moved to the wiki`_, where it can be easily edited as new -features arrive and the API changes. Please refer to the linked page to learn how -to send spoilers, custom emoji, stickers, react to messages, and more things. - -.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages diff --git a/readthedocs/index.rst b/readthedocs/index.rst deleted file mode 100644 index f4b1d877..00000000 --- a/readthedocs/index.rst +++ /dev/null @@ -1,120 +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/chats-vs-channels - concepts/updates - concepts/sessions - concepts/full-api - concepts/errors - concepts/botapi-vs-mtproto - concepts/asyncio - -.. toctree:: - :hidden: - :caption: Full API Examples - - examples/word-of-warning - examples/chats-and-channels - examples/users - examples/working-with-messages - -.. toctree:: - :hidden: - :caption: Developing - - developing/philosophy.rst - developing/test-servers.rst - developing/project-structure.rst - developing/coding-style.rst - developing/testing.rst - developing/understanding-the-type-language.rst - developing/tips-for-porting-the-project.rst - developing/telegram-api-in-other-languages.rst - -.. toctree:: - :hidden: - :caption: Miscellaneous - - misc/changelog - misc/wall-of-shame.rst - misc/compatibility-and-convenience - -.. toctree:: - :hidden: - :caption: Telethon Modules - - modules/client - modules/events - modules/custom - modules/utils - modules/errors - modules/sessions - modules/network - modules/helpers diff --git a/readthedocs/make.bat b/readthedocs/make.bat deleted file mode 100644 index f51f7234..00000000 --- a/readthedocs/make.bat +++ /dev/null @@ -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 deleted file mode 100644 index 4b2de63c..00000000 --- a/readthedocs/misc/changelog.rst +++ /dev/null @@ -1,4329 +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 - -New Layer and housekeeping (v1.28) -================================== - -+------------------------+ -| Scheme layer used: 155 | -+------------------------+ - -Plenty of stale issues closed, as well as improvements for some others. - -Additions -~~~~~~~~~ - -* New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor. - This should help a bit in keeping memory usage in check. - -Enhancements -~~~~~~~~~~~~ - -* ``progress_callback`` is now called when dealing with albums. See the - documentation on `client.send_file() ` - for details. -* Update state and entities are now periodically saved, so that the information - isn't lost in the case of crash or unexpected script terminations. You should - still be calling ``disconnect`` or using the context-manager, though. -* The client should no longer unnecessarily call ``get_me`` every time it's started. - -Bug fixes -~~~~~~~~~ - -* Messages obtained via raw API could not be used in ``forward_messages``. -* ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details. - It is no longer possible for third-party applications, such as those made with - Telethon, to use those features. -* ``events.ChatAction`` should now work in more cases in groups with hidden members. -* Errors that occur at the connection level should now be properly propagated, so that - you can actually have a chance to handle them. -* Update handling should be more resilient. -* ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``. -* In patch ``v1.28.2``, :tl:`InputBotInlineMessageID64` can now be used - to edit inline messages. - - -.. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050 - - -New Layer and some Bug fixes (v1.27) -==================================== - -+------------------------+ -| Scheme layer used: 152 | -+------------------------+ - -Bug fixes -~~~~~~~~~ - -* When the account is logged-out, the library should now correctly propagate - an error through ``run_until_disconnected`` to let you handle it. -* The library no longer uses ``asyncio.get_event_loop()`` in newer Python - versions, which should get rid of some deprecation warnings. -* It could happen that bots would receive messages sent by themselves, - very often right after they deleted a message. This should happen far - less often now (but might still happen with unlucky timings). -* Maximum photo size for automatic image resizing is now larger. -* The initial request is now correctly wrapped in ``invokeWithoutUpdates`` - when updates are disabled after constructing the client instance. -* Using a ``pathlib.Path`` to download contacts and web documents should - now work correctly. - -New Layer and some Bug fixes (v1.26) -==================================== - -+------------------------+ -| Scheme layer used: 149 | -+------------------------+ - -This new layer includes things such as emoji status, more admin log events, -forum topics and message reactions, among other things. You can access these -using raw API. It also contains a few bug fixes. - -These were fixed in the v1.25 series: - -* ``client.edit_admin`` did not work on small group chats. -* ``client.get_messages`` could stop early in some channels. -* ``client.download_profile_photo`` now should work even if ``User.min``. -* ``client.disconnect`` should no longer hang when being called from within - an event handlers. -* ``client.get_dialogs`` now initializes the update state for channels. -* The message sender should not need to be fetched in more cases. -* Lowered the severity of some log messages to be less spammy. - -These are new to v1.26.0: - -* Layer update. -* New documented RPC errors. -* Sometimes the first message update to a channel could be missed if said - message was read immediately. -* ``client.get_dialogs`` would fail when the total count evenly divided - the chunk size of 100. -* ``client.get_messages`` could get stuck during a global search. -* Potentially fixed some issues when sending certain videos. -* Update handling should be more resilient. -* The client should handle having its auth key destroyed more gracefully. -* Fixed some issues when logging certain messages. - - -Bug fixes (v1.25.1) -=================== - -This version should fix some of the problems that came with the revamped -update handling. - -* Some inline URLs were not parsing correctly with markdown. -* ``events.Raw`` was handling :tl:`UpdateShort` which it shouldn't do. -* ``events.Album`` should now work again. -* ``CancelledError`` was being incorrectly logged as a fatal error. -* Some fixes to update handling primarly aimed for bot accounts. -* Update handling now can deal with more errors without crashing. -* Unhandled errors from update handling will now be propagated through - ``client.run_until_disconnected``. -* Invite links with ``+`` are now recognized. -* Added new known RPC errors. -* ``telethon.types`` could not be used as a module. -* 0-length message entities are now stripped to avoid errors. -* ``client.send_message`` was not returning a message with ``reply_to`` - in some cases. -* ``aggressive`` in ``client.iter_participants`` now does nothing (it did - not really work anymore anyway, and this should prevent other errors). -* ``client.iter_participants`` was failing in some groups. -* Text with HTML URLs could sometimes fail to parse. -* Added a hard timeout during disconnect in order to prevent the program - from freezing. - -Please be sure to report issues with update handling if you still encounter -some errors! - - -Update handling overhaul (v1.25) -================================ - -+------------------------+ -| Scheme layer used: 144 | -+------------------------+ - -I had plans to release v2 way earlier, but my motivation drained off, so that -didn't happen. The reason for another v1 release is that there was a clear -need to fix some things regarding update handling (which were present in v2). -I did not want to make this release. But with the release date for v2 still -being unclear, I find it necessary to release another v1 version. I apologize -for the delay (I should've done this a lot sooner but didn't because in my -head I would've pushed through and finished v2, but I underestimated how much -work that was and I probably experienced burn-out). - -I still don't intend to make new additions to the v1 series (beyond updating -the Telegram layer being used). I still have plans to finish v2 some day. -But in the meantime, new features, such as reactions, will have to be used -through raw API. - -This update also backports the update overhaul from v2. If you experience -issues with updates, please report them on the GitHub page for the project. -However, this new update handling should be more reliable, and ``catch_up`` -should actually work properly. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* In order for ``catch_up`` to work (new flag in the ``TelegramClient`` - constructor), sessions need to impleemnt the new ``get_update_states``. - Third-party session storages won't have this implemented by the time - this version released, so ``catch_up`` may not work with those. - -Rushed release to fix login (v1.24) -=================================== - -+------------------------+ -| Scheme layer used: 133 | -+------------------------+ - -This is a rushed release. It contains a layer recent enough to not fail with -``UPDATE_APP_TO_LOGIN``, but still not the latest, to avoid breaking more -than necessary. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The biggest change is user identifiers (and chat identifiers, and others) - **now use up to 64 bits**, rather than 32. If you were storing them in some - storage with fixed size, you may need to update (such as database tables - storing only integers). - -There have been other changes which I currently don't have the time to document. -You can refer to the following link to see them early: -https://github.com/LonamiWebs/Telethon/compare/v1.23.0...v1.24.0 - - -New schema and bug fixes (v1.23) -================================ - -+------------------------+ -| Scheme layer used: 130 | -+------------------------+ - -`View new and changed raw API methods `__. - -Enhancements -~~~~~~~~~~~~ - -* `client.pin_message() ` - can now pin on a single side in PMs. -* Iterating participants should now be less expensive floodwait-wise. - -Bug fixes -~~~~~~~~~ - -* The QR login URL was being encoded incorrectly. -* ``force_document`` was being ignored in inline queries for document. -* ``manage_call`` permission was accidentally set to ``True`` by default. - -New schema and bug fixes (v1.22) -================================ - -+------------------------+ -| Scheme layer used: 129 | -+------------------------+ - -`View new and changed raw API methods `__. - -Enhancements -~~~~~~~~~~~~ - -* You can now specify a message in `client.get_stats() - `. -* Metadata extraction from audio files with ``hachoir`` now recognises "artist". -* Get default chat permissions by not supplying a user to `client.get_permissions() - `. -* You may now use ``thumb`` when editing messages. - -Bug fixes -~~~~~~~~~ - -* Fixes regarding bot markup in messages. -* Gracefully handle :tl:`ChannelForbidden` in ``get_sender``. - -And from v1.21.1: - -* ``file.width`` and ``.height`` was not working correctly in photos. -* Raw API was mis-interpreting ``False`` values on boolean flag parameters. - -New schema and QoL improvements (v1.21) -======================================= - -+------------------------+ -| Scheme layer used: 125 | -+------------------------+ - -`View new and changed raw API methods `__. - -Not many changes in this release, mostly the layer change. Lately quite a few -people have been reporting `TypeNotFoundError`, which occurs when the server -**sends types that it shouldn't**. This can happen when Telegram decides to -add a new, incomplete layer, and then they change the layer without bumping -the layer number (so some constructor IDs no longer match and the error -occurs). This layer change -`should fix it `__. - -Additions -~~~~~~~~~ - -* `Message.click() ` now supports - a ``password`` parameter, needed when doing things like changing the owner - of a bot via `@BotFather `__. - -Enhancements -~~~~~~~~~~~~ - -* ``tgcrypto`` will now be used for encryption when installed. - -Bug fixes -~~~~~~~~~ - -* `Message.edit ` wasn't working in - your own chat on events other than ``NewMessage``. -* `client.delete_dialog() ` - was not working on chats. -* ``events.UserUpdate`` should now handle channels' typing status. -* :tl:`InputNotifyPeer` auto-cast should now work on other ``TLObject``. -* For some objects, ``False`` was not correctly serialized. - - -New schema and QoL improvements (v1.20) -======================================= - -+------------------------+ -| Scheme layer used: 124 | -+------------------------+ - -`View new and changed raw API methods `__. - -A bit late to the party, but Telethon now offers a convenient way to comment -on channel posts. It works very similar to ``reply_to``: - -.. code-block:: python - - client.send_message(channel, 'Great update!', comment_to=1134) - -This code will leave a comment to the channel post with ID ``1134`` in -``channel``. - -In addition, the library now logs warning or error messages to ``stderr`` by -default! You no longer should be left wondering "why isn't my event handler -working" if you forgot to configure logging. It took so long for this change -to arrive because nobody noticed that Telethon was using a -``logging.NullHandler`` when it really shouldn't have. - -If you want the old behaviour of no messages being logged, you can configure -`logging` to ``CRITICAL`` severity: - -.. code-block:: python - - import logging - logging.basicConfig(level=logging.CRITICAL) - -This is not considered a breaking change because ``stderr`` should only be -used for logging purposes, not to emit information others may consume (use -``stdout`` for that). - -Additions -~~~~~~~~~ - -* New ``comment_to`` parameter in `client.send_message() - `, and - `client.send_file() ` - to comment on channel posts. - -Enhancements -~~~~~~~~~~~~ - -* ``utils.resolve_invite_link`` handles the newer link format. -* Downloading files now retries once on `TimeoutError`, which has been - happening recently. It is not guaranteed to work, but it should help. -* Sending albums of photo URLs is now supported. -* EXIF metadata is respected when automatically resizing photos, so the - orientation information should no longer be lost. -* Downloading a thumbnail by index should now use the correct size ordering. - -Bug fixes -~~~~~~~~~ - -* Fixed a `KeyError` on certain cases with ``Conversation``. -* Thumbnails should properly render on more clients. Installing ``hachoir`` - may help. -* Message search was broken when using a certain combination of parameters. -* ``utils.resolve_id`` was misbehaving with some identifiers. -* Fix ``TypeNotFoundError`` was not being propagated, causing deadlocks. -* Invoking multiple requests at once with ``ordered=True`` was deadlocking. - - -New raw API call methods (v1.19) -================================ - -+------------------------+ -| Scheme layer used: 122 | -+------------------------+ - -Telegram has had group calls for some weeks now. This new version contains the -raw API methods needed to initiate and manage these group calls, however, the -library will likely **not offer ways to stream audio directly**. - -Telethon's focus is being an asyncio-based, pure-Python implementation to -interact with Telegram's API. Streaming audio is beyond the current scope of -the project and would be a big undertaking. - -However, that doesn't mean calls are not possible with Telethon. If you want -to help design a Python library to perform audio calls, which can then be used -with Telethon (so you can use Telethon + that new library to perform calls -with Telethon), please refer to `@pytgcallschat `__ -and join the relevant chat to discuss and help with the implementation! - -The above message was also `posted in the official Telegram group -`__, if you wish to discuss it further. - -With that out of the way, let's list the additions and bug fixes in this -release: - -Additions -~~~~~~~~~ - -* New ``has_left`` property for user permissions on `client.get_permissions() - `. - -Enhancements -~~~~~~~~~~~~ - -* Updated documentation and list of known RPC errors. -* The library now treats a lack of ping responses as a network error. -* `client.kick_participant() ` - now returns the service message about the user being kicked, so you can - delete it. - -Bug fixes -~~~~~~~~~ - -* When editing inline messages, the text parameter is preferred if provided. -* Additional senders are unconditionally disconnected when disconnecting the - main client, which should reduce the amount of asyncio warnings. -* Automatic reconnection with no retries was failing. -* :tl:`PhotoPathSize` is now ignored when determining a download size, since - this "size" is not a JPEG thumbnail unlike the rest. -* `events.ChatAction ` should misbehave - less. - - -New layer and QoL improvements (v1.18) -====================================== - -+------------------------+ -| Scheme layer used: 120 | -+------------------------+ - -Mostly fixes, and added some new things that can be done in this new layer. - -For proxy users, a pull request was merged that will use the ``python-socks`` -library when available for proxy support. This library natively supports -`asyncio`, so it should work better than the old ``pysocks``. ``pysocks`` will -still be used if the new library is not available, and both will be handled -transparently by Telethon so you don't need to worry about it. - -Additions -~~~~~~~~~ - -* New `client.set_proxy() - ` method - which lets you change the proxy without recreating the client. You will need - to reconnect for it to take effect, but you won't need to recreate the - client. This is also an external contribution. -* New method to unpin messages `client.unpin_message() - `. - -Enhancements -~~~~~~~~~~~~ - -* Empty peers are excluded from the list of dialogs. -* If the ``python-socks`` library is installed (new optional requirement), it - will be used instead of ``pysocks`` for proxy support. This should fix some - issues with proxy timeouts, because the new library natively supports - `asyncio`. -* `client.send_file() ` will - now group any media type, instead of sending non-image documents separatedly. - This lets you create music albums, for example. -* You can now search messages with a ``from_user`` that's not a user. This is - a Telegram feature, we know the name isn't great, but backwards-compatibility - has to be kept. - -Bug fixes -~~~~~~~~~ - -* Fixes related to conversation timeouts. -* Large dates (over year 2038) now wrap around a 32-bit integer, which is the - only way we can represent them to Telegram. Even if "wrong", it makes things - not crash, and it's the best we can do with 32-bit dates. -* The library was accidentally using a deprecated argument in one of its - friendly methods, producing a warning. -* Improvements to the way marked IDs are parsed. -* ``SlowModeWaitError`` floods are no longer cached. -* Getting the buttons for a message could fail sometimes. -* Getting the display name for "forbidden" chats now works. -* Better handling of errors in some internal methods. - - -Channel comments and Anonymous Admins (v1.17) -============================================= - -+------------------------+ -| Scheme layer used: 119 | -+------------------------+ - -New minor version, new layer change! This time is a good one to remind every -consumer of Python libraries that **you should always specify fixed versions -of your dependencies**! If you're using a ``requirements.txt`` file and you -want to stick with the old version (or any version) for the time being, you -can `use the following syntax `__: - -.. code-block:: text - - telethon~=1.16.0 - -This will install any version compatible with the written version (so, any in -the ``1.16`` series). Patch releases will never break your code (and if they -do, it's a bug). You can also use that syntax in ``pip install``. Your code -can't know what new versions will look like, so saying it will work with all -versions is a lie and will cause issues. - -The reason to bring this up is that Telegram has changed things again, and -with the introduction of anonymous administrators and channel comments, the -sender of a message may not be a :tl:`User`! To accomodate for this, the field -is now a :tl:`Peer` and not `int`. As a reminder, it's always a good idea to -use Telethon's friendly methods and custom properties, which have a higher -stability guarantee than accessing raw API fields. - -Even if you don't update, your code will still need to account for the fact -that the sender of a message might be one of the accounts Telegram introduced -to preserve backwards compatibility, because this is a server-side change, so -it's better to update and not lag behind. As it's mostly just a single person -driving the project on their free time, bug-fixes are not backported. - -This version also updates the format of SQLite sessions (the default), so -after upgrading and using an old session, the session will be updated, which -means trying to use it back in older versions of the library won't work. - -For backwards-compatibility sake, the library has introduced the properties -`Message.reply_to_msg_id ` -and `Message.to_id ` that behave -like they did before (Telegram has renamed and changed how these fields work). - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* ``Message.from_id`` is now a :tl:`Peer`, not `int`! If you want the marked - sender ID (much like old behaviour), replace all uses of ``.from_id`` with - ``.sender_id``. This will mostly work, but of course in old and new versions - you have to account for the fact that this sender may no longer be a user. -* You can no longer assign to `Message.reply_to_msg_id - ` and `Message.to_id - ` because these are now properties - that offer a "view" to the real value from a different field. -* Answering inline queries with a ``photo`` or ``document`` will now send the - photo or document used in the resulting message by default. Not sending the - media was technically a bug, but some people may be relying on this old - behaviour. You can use the old behaviour with ``include_media=False``. - -Additions -~~~~~~~~~ - -* New ``raise_last_call_error`` parameter in the client constructor to raise - the same error produced by the last failing call, rather than a generic - `ValueError`. -* New ``formatting_entities`` parameter in `client.send_message() - `, and - `client.send_file() ` - to bypass the parse mode and manually specify the formatting entities. -* New `client.get_permissions() ` - method to query a participant's permissions in a group or channel. This - request is slightly expensive in small group chats because it has to fetch - the entire chat to check just a user, so use of a cache is advised. -* `Message.click() ` now works on - normal polls! -* New ``local_addr`` parameter in the client constructor to use a specific - local network address when connecting to Telegram. -* `client.inline_query() ` now - lets you specify the chat where the query is being made from, which some - bots need to provide certain functionality. -* You can now get comments in a channel post with the ``reply_to`` parameter in - `client.iter_messages() `. - Comments are messages that "reply to" a specific channel message, hence the - name (which is consistent with how Telegram's API calls it). - -Enhancements -~~~~~~~~~~~~ - -* Updated documentation and list of known errors. -* If ``hachoir`` is available, the file metadata can now be extracted from - streams and in-memory bytes. -* The default parameters used to initialize a connection now match the format - of those used by Telegram Desktop. -* Specifying 0 retries will no longer cause the library to attempt to reconnect. -* The library should now be able to reliably download very large files. -* Global search should work more reliably now. -* Old usernames are evicted from cache, so getting entities by cached username - should now be more reliable. -* Slightly less noisy logs. -* Stability regarding transport-level errors (transport flood, authorization - key not found) should be improved. In particular, you should no longer be - getting unnecessarily logged out. -* Reconnection should no longer occur if the client gets logged out (for - example, another client revokes the session). - -Bug fixes -~~~~~~~~~ - -* In some cases, there were issues when using `events.Album - ` together with `events.Raw - `. -* For some channels, one of their channel photos would not show up in - `client.iter_profile_photos() `. -* In some cases, a request that failed to be sent would be forgotten, causing - the original caller to be "locked" forever for a response that would never - arrive. Failing requests should now consistently be automatically re-sent. -* The library should more reliably handle certain updates with "empty" data. -* Sending documents in inline queries should now work fine. -* Manually using `client.sign_up ` - should now work correctly, instead of claiming "code invalid". - -Special mention to some of the other changes in the 1.16.x series: - -* The ``thumb`` for ``download_media`` now supports both `str` and :tl:`VideoSize`. -* Thumbnails are sorted, so ``-1`` is always the largest. - - -Bug Fixes (v1.16.1) -=================== - -The last release added support to ``force_file`` on any media, including -things that were not possible before like ``.webp`` files. However, the -``force_document`` toggle commonly used for photos was applied "twice" -(one told the library to send it as a document, and then to send that -document as file), which prevented Telegram for analyzing the images. Long -story short, sending files to the stickers bot stopped working, but that's -been fixed now, and sending photos as documents include the size attribute -again as long as Telegram adds it. - -Enhancements -~~~~~~~~~~~~ - -* When trying to `client.start() ` to - another account if you were previously logged in, the library will now warn - you because this is probably not intended. To avoid the warning, make sure - you're logging in to the right account or logout from the other first. -* Sending a copy of messages with polls will now work when possible. -* The library now automatically retries on inter-dc call errors (which occur - when Telegram has internal issues). - -Bug Fixes -~~~~~~~~~ - -* The aforementioned issue with ``force_document``. -* Square brackets removed from IPv6 addresses. This may fix IPv6 support. - - -Channel Statistics (v1.16) -========================== - -+------------------------+ -| Scheme layer used: 116 | -+------------------------+ - -The newest Telegram update has a new method to also retrieve megagroup -statistics, which can now be used with `client.get_stats() -`. This way you'll be able -to access the raw data about your channel or megagroup statistics. - -The maximum file size limit has also been increased to 2GB on the server, -so you can send even larger files. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* Besides the obvious layer change, the ``loop`` argument **is now ignored**. - It has been deprecated since Python 3.8 and will be removed in Python 3.10, - and also caused some annoying warning messages when using certain parts of - the library. If you were (incorrectly) relying on using a different loop - from the one that was set, things may break. - -Enhancements -~~~~~~~~~~~~ - -* `client.upload_file() ` - now works better when streaming files (anything that has a ``.read()``), - instead of reading it all into memory when possible. - - -QR login (v1.15) -================ - -*Published at 2020/07/04* - -+------------------------+ -| Scheme layer used: 114 | -+------------------------+ - -The library now has a friendly method to perform QR-login, as detailed in -https://core.telegram.org/api/qr-login. It won't generate QR images, but it -provides a way for you to easily do so with any other library of your choice. - -Additions -~~~~~~~~~ - -* New `client.qr_login() `. -* `message.click ` now lets you - click on buttons requesting phone or location. - -Enhancements -~~~~~~~~~~~~ - -* Updated documentation and list of known errors. -* `events.Album ` should now handle albums from - different data centers more gracefully. -* `client.download_file() - ` now supports - `pathlib.Path` as the destination. - -Bug fixes -~~~~~~~~~ - -* No longer crash on updates received prior to logging in. -* Server-side changes caused clicking on inline buttons to trigger a different - error, which is now handled correctly. - - -Minor quality of life improvements (v1.14) -========================================== - -*Published at 2020/05/26* - -+------------------------+ -| Scheme layer used: 113 | -+------------------------+ - -Some nice things that were missing, along with the usual bug-fixes. - -Additions -~~~~~~~~~ - -* New `Message.dice ` property. -* The ``func=`` parameter of events can now be an ``async`` function. - -Bug fixes -~~~~~~~~~ - -* Fixed `client.action() ` - having an alias wrong. -* Fixed incorrect formatting of some errors. -* Probably more reliable detection of pin events in small groups. -* Fixed send methods on `client.conversation() - ` were not honoring - cancellation. -* Flood waits of zero seconds are handled better. -* Getting the pinned message in a chat was failing. -* Fixed the return value when forwarding messages if some were missing - and also the return value of albums. - -Enhancements -~~~~~~~~~~~~ - -* ``.tgs`` files are now recognised as animated stickers. -* The service message produced by `Message.pin() - ` is now returned. -* Sending a file with `client.send_file() - ` now works fine when - you pass an existing dice media (e.g. sending a message copy). -* `client.edit_permissions() ` - now has the ``embed_links`` parameter which was missing. - -Bug Fixes (v1.13) -================= - -*Published at 2020/04/25* - -+------------------------+ -| Scheme layer used: 112 | -+------------------------+ - -Bug fixes and layer bump. - -Bug fixes -~~~~~~~~~ - -* Passing ``None`` as the entity to `client.delete_messages() - ` would fail. -* When downloading a thumbnail, the name inferred was wrong. - -Bug Fixes (v1.12) -================= - -*Published at 2020/04/20* - -+------------------------+ -| Scheme layer used: 111 | -+------------------------+ - -Once again nothing major, but a few bug fixes and primarily the new layer -deserves a new minor release. - -Bug fixes -~~~~~~~~~ - -These were already included in the ``v1.11.3`` patch: - -* ``libssl`` check was failing on macOS. -* Getting input users would sometimes fail on `events.ChatAction - `. - -These bug fixes are available in this release and beyond: - -* Avoid another occurrence of `MemoryError`. -* Sending large files in albums would fail because it tried to cache them. -* The ``thumb`` was being ignored when sending files from :tl:`InputFile`. -* Fixed editing inline messages from callback queries in some cases. -* Proxy connection is now blocking which should help avoid some errors. - - -Bug Fixes (v1.11) -================= - -*Published at 2020/02/20* - -+------------------------+ -| Scheme layer used: 110 | -+------------------------+ - -It has been a while since the last release, and a few bug fixes have been -made since then. This release includes them and updates the scheme layer. - -Note that most of the bug-fixes are available in the ``v1.10.10`` patch. - -Bug fixes -~~~~~~~~~ - -* Fix ``MemoryError`` when casting certain media. -* Fix `client.get_entity() ` - on small group chats. -* `client.delete_dialog() ` - now handles deactivated chats more gracefully. -* Sending a message with ``file=`` would ignore some of the parameters. -* Errors are now un-pickle-able once again. -* Fixed some issues regarding markdown and HTML (un)parsing. - -The following are also present in ``v1.10.10``: - -* Fixed some issues with `events.Album `. -* Fixed some issues with `client.kick_participant() - ` and - `client.edit_admin() `. -* Fixed sending albums and more within `client.conversation() - `. -* Fixed some import issues. -* And a lot more minor stuff. - -Enhancements -~~~~~~~~~~~~ - -* Videos can now be included when sending albums. -* Getting updates after reconnect should be more reliable. -* Updated documentation and added more examples. -* More security checks during the generation of the authorization key. - -The following are also present in ``v1.10.10``: - -* URLs like ``t.me/@username`` are now valid. -* Auto-sleep now works for slow-mode too. -* Improved some error messages. -* Some internal improvements and updating. -* `client.pin_message() ` - now also works with message objects. -* Asynchronous file descriptors are now allowed during download and upload. - - -Scheduled Messages (v1.10) -========================== - -*Published at 2019/09/08* - -+------------------------+ -| Scheme layer used: 105 | -+------------------------+ - -You can now schedule messages to be sent (or edited, or forwarded…) at a later -time, which can also work as reminders for yourself when used in your own chat! - -.. code-block:: python - - from datetime import timedelta - - # Remind yourself to walk the dog in 10 minutes (after you play with Telethon's update) - await client.send_message('me', 'Walk the dog', - schedule=timedelta(minutes=10)) - - # Remind your friend tomorrow to update Telethon - await client.send_message(friend, 'Update Telethon!', - schedule=timedelta(days=1)) - -Additions -~~~~~~~~~ - -* New `Button.auth ` friendly button - you can use to ask users to login to your bot. -* Telethon's repository now contains ``*.nix`` expressions that you can use. -* New `client.kick_participant() ` - method to truly kick (not ban) participants. -* New ``schedule`` parameter in `client.send_message() - `, `client.edit_message() - `, `client.forward_messages() - ` and `client.send_file() - `. - -Bug fixes -~~~~~~~~~ - -* Fix calling ``flush`` on file objects which lack this attribute. -* Fix `CallbackQuery ` pattern. -* Fix `client.action() ` not returning - itself when used in a context manager (so the ``as`` would be `None`). -* Fix sending :tl:`InputKeyboardButtonUrlAuth` as inline buttons. -* Fix `client.edit_permissions() ` - defaults. -* Fix `Forward ` had its ``client`` as `None`. -* Fix (de)serialization of negative timestamps (caused by the information in some - sites with instant view, where the date could be very old). -* Fix HTML un-parsing. -* Fix ``to/from_id`` in private messages when using multiple clients. -* Stop disconnecting from `None` (incorrect logging). -* Fix double-read on double-connect. -* Fix `client.get_messages() ` - when being passed more than 100 IDs. -* Fix `Message.document ` - for documents coming from web-pages. - -Enhancements -~~~~~~~~~~~~ - -* Some documentation improvements, including the TL reference. -* Documentation now avoids ``telethon.sync``, which should hopefully be less confusing. -* Better error messages for flood wait. -* You can now `client.get_drafts() ` - for a single entity (which means you can now get a single draft from a single chat). -* New-style file IDs now work with Telethon. -* The ``progress_callback`` for `client.upload_file() - ` can now be an ``async def``. - - -Animated Stickers (v1.9) -======================== - -*Published at 2019/07/06* - -+------------------------+ -| Scheme layer used: 103 | -+------------------------+ - -With the layer 103, Telethon is now able to send and receive animated -stickers! These use the ``'application/x-tgsticker'`` mime-type and for -now, you can access its raw data, which is a gzipped JSON. - - -Additions -~~~~~~~~~ - -* New `events.Album ` to easily receive entire albums! -* New `client.edit_admin() ` - and `client.edit_permissions() ` - methods to more easily manage your groups. -* New ``pattern=`` in `CallbackQuery - `. -* New `conversation.cancel_all() - ` method, - to cancel all currently-active conversations in a particular chat. -* New `telethon.utils.encode_waveform` and `telethon.utils.decode_waveform` - methods as implemented by Telegram Desktop, which lets you customize how - voice notes will render. -* New ``ignore_pinned`` parameter in `client.iter_dialogs() - `. -* New `Message.mark_read() ` - method. -* You can now use strike-through in markdown with ``~~text~~``, and the - corresponding HTML tags for strike-through, quotes and underlined text. -* You can now nest entities, as in ``**__text__**``. - -Bug fixes -~~~~~~~~~ - -* Fixed downloading contacts. -* Fixed `client.iter_dialogs() - ` missing some under - certain circumstances. -* Fixed incredibly slow imports under some systems due to expensive path - resolution when searching for ``libssl``. -* Fixed captions when sending albums. -* Fixed invalid states in `Conversation - `. -* Fixes to some methods in utils regarding extensions. -* Fixed memory cycle in `Forward ` - which let you do things like the following: - - .. code-block:: python - - original_fwd = message.forward.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd.original_fwd - - Hopefully you didn't rely on that in your code. -* Fixed `File.ext ` not working on - unknown mime-types, despite the file name having the extension. -* Fixed ``ids=..., reverse=True`` in `client.iter_messages() - `. -* Fixed `Draft ` not being aware - of the entity. -* Added missing re-exports in ``telethon.sync``. - -Enhancements -~~~~~~~~~~~~ - -* Improved `conversation.cancel() - ` - behaviour. Now you can use it from anywhere. -* The ``progress_callback`` in `client.download_media() - ` - now lets you use ``async def``. -* Improved documentation and the online - method reference at https://tl.telethon.dev. - - -Documentation Overhaul (v1.8) -============================= - -*Published at 2019/05/30* - -+------------------------+ -| Scheme layer used: 100 | -+------------------------+ - -The documentation has been completely reworked from the ground up, -with awesome new quick references such as :ref:`client-ref` to help -you quickly find what you need! - -Raw methods also warn you when a friendly variant is available, so -that you don't accidentally make your life harder than it has to be. - -In addition, all methods in the client now are fully annotated with type -hints! More work needs to be done, but this should already help a lot when -using Telethon from any IDEs. - -You may have noticed that the patch versions between ``v1.7.2`` to ``v1.7.7`` -have not been documented. This is because patch versions should only contain -bug fixes, no new features or breaking changes. This hasn't been the case in -the past, but from now on, the library will try to adhere more strictly to -the `Semantic Versioning `_ principles. - -If you ever want to look at those bug fixes, please use the appropriated -``git`` command, such as ``git shortlog v1.7.1...v1.7.4``, but in general, -they probably just fixed your issue. - -With that out of the way, let's look at the full change set: - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer changed, so take note if you use the raw API, as it's usual. -* The way photos are downloaded changed during the layer update of the - previous version, and fixing that bug as a breaking change in itself. - `client.download_media() ` - now offers a different way to deal with thumbnails. - - -Additions -~~~~~~~~~ - -* New `Message.file ` property! - Now you can trivially access `message.file.id ` - to get the file ID of some media, or even ``print(message.file.name)``. -* Archiving dialogs with `Dialog.archive() ` - or `client.edit_folder() ` - is now possible. -* New cleaned-up method to stream downloads with `client.iter_download() - `, which offers - a lot of flexibility, such as arbitrary offsets for efficient seeking. -* `Dialog.delete() ` has existed - for a while, and now `client.delete_dialog() - ` exists too so you - can easily leave chats or delete dialogs without fetching all dialogs. -* Some people or chats have a lot of profile photos. You can now iterate - over all of them with the new `client.iter_profile_photos() - ` method. -* You can now annoy everyone with the new `Message.pin(notify=True) - `! The client has its own - variant too, called `client.pin_message() - `. - - -Bug fixes -~~~~~~~~~ - -* Correctly catch and raise all RPC errors. -* Downloading stripped photos wouldn't work correctly. -* Under some systems, ``libssl`` would fail to load earlier than - expected, causing the library to fail when being imported. -* `conv.get_response() ` - after ID 0 wasn't allowed when it should. -* `InlineBuilder ` only worked - with local files, but files from anywhere are supported. -* Accessing the text property from a raw-API call to fetch :tl:`Message` would fail - (any any other property that needed the client). -* Database is now upgraded if the version was lower, not different. - From now on, this should help with upgrades and downgrades slightly. -* Fixed saving ``pts`` and session-related stuff. -* Disconnection should not raise any errors. -* Invite links of the form ``tg://join?invite=`` now work. -* `client.iter_participants(search=...) ` - now works on private chats again. -* Iterating over messages in reverse with a date as offset wouldn't work. -* The conversation would behave weirdly when a timeout occurred. - - -Enhancements -~~~~~~~~~~~~ - -* ``telethon`` now re-export all the goodies that you commonly need when - using the library, so e.g. ``from telethon import Button`` will now work. -* ``telethon.sync`` now re-exports everything from ``telethon``, so that - you can trivially import from just one place everything that you need. -* More attempts at reducing CPU usage after automatically fetching missing - entities on events. This isn't a big deal, even if it sounds like one. -* Hexadecimal invite links are now supported. You didn't need them, but - they will now work. - -Internal Changes -~~~~~~~~~~~~~~~~ - -* Deterministic code generation. This is good for ``diff``. -* On Python 3.7 and above, we properly close the connection. -* A lot of micro-optimization. -* Fixes to bugs introduced while making this release. -* Custom commands on ``setup.py`` are nicer to use. - - - -Fix-up for Photo Downloads (v1.7.1) -=================================== - -*Published at 2019/04/24* - -Telegram changed the way thumbnails (which includes photos) are downloaded, -so you can no longer use a :tl:`PhotoSize` alone to download a particular -thumbnail size (this is a **breaking change**). - -Instead, you will have to specify the new ``thumb`` parameter in -`client.download_media() ` -to download a particular thumbnail size. This addition enables you to easily -download thumbnails from documents, something you couldn't do easily before. - - -Easier Events (v1.7) -==================== - -*Published at 2019/04/22* - -+-----------------------+ -| Scheme layer used: 98 | -+-----------------------+ - -If you have been using Telethon for a while, you probably know how annoying -the "Could not find the input entity for…" error can be. In this new version, -the library will try harder to find the input entity for you! - -That is, instead of doing: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(await event.get_input_sender()) - # ...... needs await, it's a method ^^^^^ ^^ - -You can now do: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(event.input_sender) - # ...... no await, it's a property! ^ - # It's also 12 characters shorter :) - -And even the following will hopefully work: - -.. code-block:: python - - @client.on(events.NewMessage) - async def handler(event): - await client.download_profile_photo(event.sender_id) - -A lot of people use IDs thinking this is the right way of doing it. Ideally, -you would always use ``input_*``, not ``sender`` or ``sender_id`` (and the -same applies to chats). But, with this change, IDs will work just the same as -``input_*`` inside events. - -**This feature still needs some more testing**, so please do open an issue -if you find strange behaviour. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer changed, and a lot of things did too. If you are using - raw API, you should be careful with this. In addition, some attributes - weren't of type ``datetime`` when they should be, which has been fixed. -* Due to the layer change, you can no longer download photos with just - their :tl:`PhotoSize`. Version 1.7.1 introduces a new way to download - thumbnails to work around this issue. -* `client.disconnect() - ` - is now asynchronous again. This means you need to ``await`` it. You - don't need to worry about this if you were using ``with client`` or - `client.run_until_disconnected - `. - This should prevent the "pending task was destroyed" errors. - -Additions -~~~~~~~~~ - -* New in-memory cache for input entities. This should mean a lot less - of disk look-ups. -* New `client.action ` method - to easily indicate that you are doing some chat action: - - .. code-block:: python - - async with client.action(chat, 'typing'): - await asyncio.sleep(2) # type for 2 seconds - await client.send_message(chat, 'Hello world! I type slow ^^') - - You can also easily use this for sending files, playing games, etc. - - -New bugs -~~~~~~~~ - -* Downloading photos is broken. This is fixed in v1.7.1. - -Bug fixes -~~~~~~~~~ - -* Fix sending photos from streams/bytes. -* Fix unhandled error when sending requests that were too big. -* Fix edits that arrive too early on conversations. -* Fix `client.edit_message() - ` - when trying to edit a file. -* Fix method calls on the objects returned by `client.iter_dialogs() - `. -* Attempt at fixing `client.iter_dialogs() - ` missing many dialogs. -* ``offset_date`` in `client.iter_messages() - ` was being - ignored in some cases. This has been worked around. -* Fix `callback_query.edit() - `. -* Fix `CallbackQuery(func=...) ` - was being ignored. -* Fix `UserUpdate ` not working for - "typing" (and uploading file, etc.) status. -* Fix library was not expecting ``IOError`` from PySocks. -* Fix library was raising a generic ``ConnectionError`` - and not the one that actually occurred. -* Fix the ``blacklist_chats`` parameter in `MessageRead - ` not working as intended. -* Fix `client.download_media(contact) - `. -* Fix mime type when sending ``mp3`` files. -* Fix forcibly getting the sender or chat from events would - not always return all their information. -* Fix sending albums with `client.send_file() - ` was not returning - the sent messages. -* Fix forwarding albums with `client.forward_messages() - `. -* Some fixes regarding filtering updates from chats. -* Attempt at preventing duplicated updates. -* Prevent double auto-reconnect. - - -Enhancements -~~~~~~~~~~~~ - -* Some improvements related to proxy connections. -* Several updates and improvements to the documentation, - such as optional dependencies now being properly listed. -* You can now forward messages from different chats directly with - `client.forward_messages `. - - -Tidying up Internals (v1.6) -=========================== - -*Published at 2019/02/27* - -+-----------------------+ -| Scheme layer used: 95 | -+-----------------------+ - -First things first, sorry for updating the layer in the previous patch -version. That should only be done between major versions ideally, but -due to how Telegram works, it's done between minor versions. However raw -API has and will always be considered "unsafe", this meaning that you -should always use the convenience client methods instead. These methods -don't cover the full API yet, so pull requests are welcome. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer update, of course. This didn't really need a mention here. -* You can no longer pass a ``batch_size`` when iterating over messages. - No other method exposed this parameter, and it was only meant for testing - purposes. Instead, it's now a private constant. -* ``client.iter_*`` methods no longer have a ``_total`` parameter which - was supposed to be private anyway. Instead, they return a new generator - object which has a ``.total`` attribute: - - .. code-block:: python - - it = client.iter_messages(chat) - for i, message in enumerate(it, start=1): - percentage = i / it.total - print('{:.2%} {}'.format(percentage, message.text)) - -Additions -~~~~~~~~~ - -* You can now pass ``phone`` and ``phone_code_hash`` in `client.sign_up - `, although you probably don't - need that. -* Thanks to the overhaul of all ``client.iter_*`` methods, you can now do: - - .. code-block:: python - - for message in reversed(client.iter_messages('me')): - print(message.text) - -Bug fixes -~~~~~~~~~ - -* Fix `telethon.utils.resolve_bot_file_id`, which wasn't working after - the layer update (so you couldn't send some files by bot file IDs). -* Fix sending albums as bot file IDs (due to image detection improvements). -* Fix `takeout() ` failing - when they need to download media from other DCs. -* Fix repeatedly calling `conversation.get_response() - ` when many - messages arrived at once (i.e. when several of them were forwarded). -* Fixed connecting with `ConnectionTcpObfuscated - `. -* Fix `client.get_peer_id('me') - `. -* Fix warning of "missing sqlite3" when in reality it just had wrong tables. -* Fix a strange error when using too many IDs in `client.delete_messages() - `. -* Fix `client.send_file ` - with the result of `client.upload_file - `. -* When answering inline results, their order was not being preserved. -* Fix `events.ChatAction ` - detecting user leaves as if they were kicked. - -Enhancements -~~~~~~~~~~~~ - -* Cleared up some parts of the documentation. -* Improved some auto-casts to make life easier. -* Improved image detection. Now you can easily send `bytes` - and streams of images as photos, unless you force document. -* Sending images as photos that are too large will now be resized - before uploading, reducing the time it takes to upload them and - also avoiding errors when the image was too large (as long as - ``pillow`` is installed). The images will remain unchanged if you - send it as a document. -* Treat ``errors.RpcMcgetFailError`` as a temporary server error - to automatically retry shortly. This works around most issues. - -Internal changes -~~~~~~~~~~~~~~~~ - -* New common way to deal with retries (``retry_range``). -* Cleaned up the takeout client. -* Completely overhauled asynchronous generators. - -Layer Update (v1.5.5) -===================== - -*Published at 2019/01/14* - -+-----------------------+ -| Scheme layer used: 93 | -+-----------------------+ - -There isn't an entry for v1.5.4 because it contained only one hot-fix -regarding loggers. This update is slightly bigger so it deserves mention. - -Additions -~~~~~~~~~ - -* New ``supports_streaming`` parameter in `client.send_file - `. - -Bug fixes -~~~~~~~~~ - -* Dealing with mimetypes should cause less issues in systems like Windows. -* Potentially fix alternative session storages that had issues with dates. - -Enhancements -~~~~~~~~~~~~ - -* Saner timeout defaults for conversations. -* ``Path``-like files are now supported for thumbnails. -* Added new hot-keys to the online documentation at - https://tl.telethon.dev/ such as ``/`` to search. - Press ``?`` to view them all. - - -Bug Fixes (v1.5.3) -================== - -*Published at 2019/01/14* - -Several bug fixes and some quality of life enhancements. - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* `message.edit ` now respects - the previous message buttons or link preview being hidden. If you want to - toggle them you need to explicitly set them. This is generally the desired - behaviour, but may cause some bots to have buttons when they shouldn't. - -Additions -~~~~~~~~~ - -* You can now "hide_via" when clicking on results from `client.inline_query - ` to @bing and @gif. -* You can now further configure the logger Telethon uses to suit your needs. - -Bug fixes -~~~~~~~~~ - -* Fixes for ReadTheDocs to correctly build the documentation. -* Fix :tl:`UserEmpty` not being expected when getting the input variant. -* The message object returned when sending a message with buttons wouldn't - always contain the :tl:`ReplyMarkup`. -* Setting email when configuring 2FA wasn't properly supported. -* ``utils.resolve_bot_file_id`` now works again for photos. - -Enhancements -~~~~~~~~~~~~ - -* Chat and channel participants can now be used as peers. -* Reworked README and examples at - https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples - - -Takeout Sessions (v1.5.2) -========================= - -*Published at 2019/01/05* - -You can now easily start takeout sessions (also known as data export sessions) -through `client.takeout() `. -Some of the requests will have lower flood limits when done through the -takeout session. - -Bug fixes -~~~~~~~~~ - -* The new `AdminLogEvent ` - had a bug that made it unusable. -* `client.iter_dialogs() ` - will now locally check for the offset date, since Telegram ignores it. -* Answering inline queries with media no works properly. You can now use - the library to create inline bots and send stickers through them! - - -object.to_json() (v1.5.1) -========================= - -*Published at 2019/01/03* - -The library already had a way to easily convert the objects the API returned -into dictionaries through ``object.to_dict()``, but some of the fields are -dates or `bytes` which JSON can't serialize directly. - -For convenience, a new ``object.to_json()`` has been added which will by -default format both of those problematic types into something sensible. - -Additions -~~~~~~~~~ - -* New `client.iter_admin_log() - ` method. - -Bug fixes -~~~~~~~~~ - -* `client.is_connected() - ` - would be wrong when the initial connection failed. -* Fixed ``UnicodeDecodeError`` when accessing the text of messages - with malformed offsets in their entities. -* Fixed `client.get_input_entity() - ` for integer IDs - that the client has not seen before. - -Enhancements -~~~~~~~~~~~~ - -* You can now configure the reply markup when using `Button - ` as a bot. -* More properties for `Message - ` to make accessing media convenient. -* Downloading to ``file=bytes`` will now return a `bytes` object - with the downloaded media. - - -Polls with the Latest Layer (v1.5) -================================== - -*Published at 2018/12/25* - -+-----------------------+ -| Scheme layer used: 91 | -+-----------------------+ - -This version doesn't really bring many new features, but rather focuses on -updating the code base to support the latest available Telegram layer, 91. -This layer brings polls, and you can create and manage them through Telethon! - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The layer change from 82 to 91 changed a lot of things in the raw API, - so be aware that if you rely on raw API calls, you may need to update - your code, in particular **if you work with files**. They have a new - ``file_reference`` parameter that you must provide. - -Additions -~~~~~~~~~ - -* New `client.is_bot() ` method. - -Bug fixes -~~~~~~~~~ - -* Markdown and HTML parsing now behave correctly with leading whitespace. -* HTTP connection should now work correctly again. -* Using ``caption=None`` would raise an error instead of setting no caption. -* ``KeyError`` is now handled properly when forwarding messages. -* `button.click() ` - now works as expected for :tl:`KeyboardButtonGame`. - -Enhancements -~~~~~~~~~~~~ - -* Some improvements to the search in the full API and generated examples. -* Using entities with ``access_hash = 0`` will now work in more cases. - -Internal changes -~~~~~~~~~~~~~~~~ - -* Some changes to the documentation and code generation. -* 2FA code was updated to work under the latest layer. - - -Error Descriptions in CSV files (v1.4.3) -======================================== - -*Published at 2018/12/04* - -While this may seem like a minor thing, it's a big usability improvement. - -Anyone who wants to update the documentation for known errors, or whether -some methods can be used as a bot, user or both, can now be easily edited. -Everyone is encouraged to help document this better! - -Bug fixes -~~~~~~~~~ - -* ``TimeoutError`` was not handled during automatic reconnects. -* Getting messages by ID using :tl:`InputMessageReplyTo` could fail. -* Fixed `message.get_reply_message - ` - as a bot when a user replied to a different bot. -* Accessing some document properties in a `Message - ` would fail. - -Enhancements -~~~~~~~~~~~~ - -* Accessing `events.ChatAction ` - properties such as input users may now work in more cases. - -Internal changes -~~~~~~~~~~~~~~~~ - -* Error descriptions and information about methods is now loaded - from a CSV file instead of being part of several messy JSON files. - - -Bug Fixes (v1.4.2) -================== - -*Published at 2018/11/24* - -This version also includes the v1.4.1 hot-fix, which was a single -quick fix and didn't really deserve an entry in the changelog. - -Bug fixes -~~~~~~~~~ - -* Authorization key wouldn't be saved correctly, requiring re-login. -* Conversations with custom events failed to be cancelled. -* Fixed ``telethon.sync`` when using other threads. -* Fix markdown/HTML parser from failing with leading/trailing whitespace. -* Fix accessing ``chat_action_event.input_user`` property. -* Potentially improved handling unexpected disconnections. - - -Enhancements -~~~~~~~~~~~~ - -* Better default behaviour for `client.send_read_acknowledge - `. -* Clarified some points in the documentation. -* Clearer errors for ``utils.get_peer*``. - - -Connection Overhaul (v1.4) -========================== - -*Published at 2018/11/03* - -Yet again, a lot of work has been put into reworking the low level connection -classes. This means ``asyncio.open_connection`` is now used correctly and the -errors it can produce are handled properly. The separation between packing, -encrypting and network is now abstracted away properly, so reasoning about -the code is easier, making it more maintainable. - -As a user, you shouldn't worry about this, other than being aware that quite -a few changes were made in the insides of the library and you should report -any issues that you encounter with this version if any. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* The threaded version of the library will no longer be maintained, primarily - because it never was properly maintained anyway. If you have old code, stick - with old versions of the library, such as ``0.19.1.6``. -* Timeouts no longer accept ``timedelta``. Simply use seconds. -* The ``callback`` parameter from `telethon.tl.custom.button.Button.inline()` - was removed, since it had always been a bad idea. Adding the callback there - meant a lot of extra work for every message sent, and only registering it - after the first message was sent! Instead, use - `telethon.events.callbackquery.CallbackQuery`. - - -Additions -~~~~~~~~~ - -* New `dialog.delete() ` method. -* New `conversation.cancel() - ` method. -* New ``retry_delay`` delay for the client to be used on auto-reconnection. - - -Bug fixes -~~~~~~~~~ - -* Fixed `Conversation.wait_event() - `. -* Fixed replying with photos/documents on inline results. -* `client.is_user_authorized() - ` now works - correctly after `client.log_out() - `. -* `dialog.is_group ` now works for - :tl:`ChatForbidden`. -* Not using ``async with`` when needed is now a proper error. -* `events.CallbackQuery ` - with string regex was not working properly. -* `client.get_entity('me') ` - now works again. -* Empty codes when signing in are no longer valid. -* Fixed file cache for in-memory sessions. - - -Enhancements -~~~~~~~~~~~~ - -* Support ``next_offset`` in `inline_query.answer() - `. -* Support ```` mentions in HTML parse mode. -* New auto-casts for :tl:`InputDocument` and :tl:`InputChatPhoto`. -* Conversations are now exclusive per-chat by default. -* The request that caused a RPC error is now shown in the error message. -* New full API examples in the generated documentation. -* Fixed some broken links in the documentation. -* `client.disconnect() - ` - is now synchronous, but you can still ``await`` it for consistency - or compatibility. - - -Event Templates (v1.3) -====================== - -*Published at 2018/09/22* - - -If you have worked with Flask templates, you will love this update, -since it gives you the same features but even more conveniently: - -.. code-block:: python - - # handlers/welcome.py - from telethon import events - - @events.register(events.NewMessage('(?i)hello')) - async def handler(event): - client = event.client - await event.respond('Hi!') - await client.send_message('me', 'Sent hello to someone') - - -This will `register ` the ``handler`` callback -to handle new message events. Note that you didn't add this to any client -yet, and this is the key point: you don't need a client to define handlers! -You can add it later: - -.. code-block:: python - - # main.py - from telethon import TelegramClient - import handlers.welcome - - with TelegramClient(...) as client: - # This line adds the handler we defined before for new messages - client.add_event_handler(handlers.welcome.handler) - client.run_until_disconnected() - - -This should help you to split your big code base into a more modular design. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -* ``.sender`` is the ``.chat`` when the message is sent in a broadcast - channel. This makes sense, because the sender of the message was the - channel itself, but you now must take into consideration that it may - be either a :tl:`User` or :tl:`Channel` instead of being `None`. - - -Additions -~~~~~~~~~ - -* New ``MultiError`` class when invoking many requests at once - through ``client([requests])``. -* New custom ``func=`` on all events. These will receive the entire - event, and a good usage example is ``func=lambda e: e.is_private``. -* New ``.web_preview`` field on messages. The ``.photo`` and ``.document`` - will also return the media in the web preview if any, for convenience. -* Callback queries now have a ``.chat`` in most circumstances. - - -Bug fixes -~~~~~~~~~ - -* Running code with `python3 -O` would remove critical code from asserts. -* Fix some rare ghost disconnections after reconnecting. -* Fix strange behavior for `send_message(chat, Message, reply_to=foo) - `. -* The ``loop=`` argument was being pretty much ignored. -* Fix ``MemorySession`` file caching. -* The logic for getting entities from their username is now correct. -* Fixes for sending stickers from ``.webp`` files in Windows, again. -* Fix disconnection without being logged in. -* Retrieving media from messages would fail. -* Getting some messages by ID on private chats. - - -Enhancements -~~~~~~~~~~~~ - -* `iter_participants ` - will now use its ``search=`` as a symbol set when ``aggressive=True``, - so you can do ``client.get_participants(group, aggressive=True, - search='абвгдеёжзийклмнопрст')``. -* The ``StringSession`` supports custom encoding. -* Callbacks for `telethon.client.auth.AuthMethods.start` can be ``async``. - - -Internal changes -~~~~~~~~~~~~~~~~ - -* Cherry-picked a commit to use ``asyncio.open_connection`` in the lowest - level of the library. Do open issues if this causes trouble, but it should - otherwise improve performance and reliability. -* Building and resolving events overhaul. - - -Conversations, String Sessions and More (v1.2) -============================================== - -*Published at 2018/08/14* - - -This is a big release! Quite a few things have been added to the library, -such as the new `Conversation `. -This makes it trivial to get tokens from `@BotFather `_: - -.. code-block:: python - - from telethon.tl import types - - with client.conversation('BotFather') as conv: - conv.send_message('/mybots') - message = conv.get_response() - message.click(0) - message = conv.get_edit() - message.click(0) - message = conv.get_edit() - for _, token in message.get_entities_text(types.MessageEntityCode): - print(token) - - -In addition to that, you can now easily load and export session files -without creating any on-disk file thanks to the ``StringSession``: - -.. code-block:: python - - from telethon.sessions import StringSession - string = StringSession.save(client.session) - -Check out :ref:`sessions` for more details. - -For those who aren't able to install ``cryptg``, the support for ``libssl`` -has been added back. While interfacing ``libssl`` is not as fast, the speed -when downloading and sending files should really be noticeably faster. - -While those are the biggest things, there are still more things to be -excited about. - - -Additions -~~~~~~~~~ - -- The mentioned method to start a new `client.conversation - `. -- Implemented global search through `client.iter_messages - ` - with `None` entity. -- New `client.inline_query ` - method to perform inline queries. -- Bot-API-style ``file_id`` can now be used to send files and download media. - You can also access `telethon.utils.resolve_bot_file_id` and - `telethon.utils.pack_bot_file_id` to resolve and create these - file IDs yourself. Note that each user has its own ID for each file - so you can't use a bot's ``file_id`` with your user, except stickers. -- New `telethon.utils.get_peer`, useful when you expect a :tl:`Peer`. - -Bug fixes -~~~~~~~~~ - -- UTC timezone for `telethon.events.userupdate.UserUpdate`. -- Bug with certain input parameters when iterating messages. -- RPC errors without parent requests caused a crash, and better logging. -- ``incoming = outgoing = True`` was not working properly. -- Getting a message's ID was not working. -- File attributes not being inferred for ``open()``'ed files. -- Use ``MemorySession`` if ``sqlite3`` is not installed by default. -- Self-user would not be saved to the session file after signing in. -- `client.catch_up() ` - seems to be functional again. - - -Enhancements -~~~~~~~~~~~~ - -- Updated documentation. -- Invite links will now use cache, so using them as entities is cheaper. -- You can reuse message buttons to send new messages with those buttons. -- ``.to_dict()`` will now work even on invalid ``TLObject``'s. - - -Better Custom Message (v1.1.1) -============================== - -*Published at 2018/07/23* - -The `custom.Message ` class has been -rewritten in a cleaner way and overall feels less hacky in the library. -This should perform better than the previous way in which it was patched. - -The release is primarily intended to test this big change, but also fixes -**Python 3.5.2 compatibility** which was broken due to a trailing comma. - - -Bug fixes -~~~~~~~~~ - -- Using ``functools.partial`` on event handlers broke updates - if they had uncaught exceptions. -- A bug under some session files where the sender would export - authorization for the same data center, which is unsupported. -- Some logical bugs in the custom message class. - - -Bot Friendly (v1.1) -=================== - -*Published at 2018/07/21* - -Two new event handlers to ease creating normal bots with the library, -namely `events.InlineQuery ` -and `events.CallbackQuery ` -for handling ``@InlineBot queries`` or reacting to a button click. For -this second option, there is an even better way: - -.. code-block:: python - - from telethon.tl.custom import Button - - async def callback(event): - await event.edit('Thank you!') - - bot.send_message(chat, 'Hello!', - buttons=Button.inline('Click me', callback)) - - -You can directly pass the callback when creating the button. - -This is fine for small bots but it will add the callback every time -you send a message, so you probably should do this instead once you -are done testing: - -.. code-block:: python - - markup = bot.build_reply_markup(Button.inline('Click me', callback)) - bot.send_message(chat, 'Hello!', buttons=markup) - - -And yes, you can create more complex button layouts with lists: - -.. code-block:: python - - from telethon import events - - global phone = '' - - @bot.on(events.CallbackQuery) - async def handler(event): - global phone - if event.data == b'<': - phone = phone[:-1] - else: - phone += event.data.decode('utf-8') - - await event.answer('Phone is now {}'.format(phone)) - - markup = bot.build_reply_markup([ - [Button.inline('1'), Button.inline('2'), Button.inline('3')], - [Button.inline('4'), Button.inline('5'), Button.inline('6')], - [Button.inline('7'), Button.inline('8'), Button.inline('9')], - [Button.inline('+'), Button.inline('0'), Button.inline('<')], - ]) - bot.send_message(chat, 'Enter a phone', buttons=markup) - - -(Yes, there are better ways to do this). Now for the rest of things: - - -Additions -~~~~~~~~~ - -- New `custom.Button ` class - to help you create inline (or normal) reply keyboards. You - must sign in as a bot to use the ``buttons=`` parameters. -- New events usable if you sign in as a bot: `events.InlineQuery - ` and `events.CallbackQuery - `. -- New ``silent`` parameter when sending messages, usable in broadcast channels. -- Documentation now has an entire section dedicate to how to use - the client's friendly methods at *(removed broken link)*. - -Bug fixes -~~~~~~~~~ - -- Empty ``except`` are no longer used which means - sending a keyboard interrupt should now work properly. -- The ``pts`` of incoming updates could be `None`. -- UTC timezone information is properly set for read ``datetime``. -- Some infinite recursion bugs in the custom message class. -- :tl:`Updates` was being dispatched to raw handlers when it shouldn't. -- Using proxies and HTTPS connection mode may now work properly. -- Less flood waits when downloading media from different data centers, - and the library will now detect them even before sending requests. - -Enhancements -~~~~~~~~~~~~ - -- Interactive sign in now supports signing in with a bot token. -- ``timedelta`` is now supported where a date is expected, which - means you can e.g. ban someone for ``timedelta(minutes=5)``. -- Events are only built once and reused many times, which should - save quite a few CPU cycles if you have a lot of the same type. -- You can now click inline buttons directly if you know their data. - -Internal changes -~~~~~~~~~~~~~~~~ - -- When downloading media, the right sender is directly - used without previously triggering migrate errors. -- Code reusing for getting the chat and the sender, - which easily enables this feature for new types. - - -New HTTP(S) Connection Mode (v1.0.4) -==================================== - -*Published at 2018/07/09* - -This release implements the HTTP connection mode to the library, which -means certain proxies that only allow HTTP connections should now work -properly. You can use it doing the following, like any other mode: - -.. code-block:: python - - from telethon import TelegramClient, sync - from telethon.network import ConnectionHttp - - client = TelegramClient(..., connection=ConnectionHttp) - with client: - client.send_message('me', 'Hi!') - - -Additions -~~~~~~~~~ - -- ``add_mark=`` is now back on ``utils.get_input_peer`` and also on - `client.get_input_entity() `. -- New `client.get_peer_id ` - convenience for ``utils.get_peer_id(await client.get_input_entity(peer))``. - - -Bug fixes -~~~~~~~~~ - -- If several `TLMessage` in a `MessageContainer` exceeds 1MB, it will no - longer be automatically turned into one. This basically means that e.g. - uploading 10 file parts at once will work properly again. -- Documentation fixes and some missing ``await``. -- Revert named argument for `client.forward_messages - ` - -Enhancements -~~~~~~~~~~~~ - -- New auto-casts to :tl:`InputNotifyPeer` and ``chat_id``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Outgoing `TLMessage` are now pre-packed so if there's an error when - serializing the raw requests, the library will no longer swallow it. - This also means re-sending packets doesn't need to re-pack their bytes. - - - -Iterate Messages in Reverse (v1.0.3) -==================================== - -*Published at 2018/07/04* - -+-----------------------+ -| Scheme layer used: 82 | -+-----------------------+ - -Mostly bug fixes, but now there is a new parameter on `client.iter_messages -` to support reversing -the order in which messages are returned. - -Additions -~~~~~~~~~ - -- The mentioned ``reverse`` parameter when iterating over messages. -- A new ``sequential_updates`` parameter when creating the client - for updates to be processed sequentially. This is useful when you - need to make sure that all updates are processed in order, such - as a script that only forwards incoming messages somewhere else. - -Bug fixes -~~~~~~~~~ - -- Count was always `None` for `message.button_count - `. -- Some fixes when disconnecting upon dropping the client. -- Support for Python 3.4 in the sync version, and fix media download. -- Some issues with events when accessing the input chat or their media. -- Hachoir wouldn't automatically close the file after reading its metadata. -- Signing in required a named ``code=`` parameter, but usage - without a name was really widespread so it has been reverted. - - -Bug Fixes (v1.0.2) -================== - -*Published at 2018/06/28* - -Updated some asserts and parallel downloads, as well as some fixes for sync. - - -Bug Fixes (v1.0.1) -================== - -*Published at 2018/06/27* - -And as usual, every major release has a few bugs that make the library -unusable! This quick update should fix those, namely: - -Bug fixes -~~~~~~~~~ - -- `client.start() ` was completely - broken due to a last-time change requiring named arguments everywhere. -- Since the rewrite, if your system clock was wrong, the connection would - get stuck in an infinite "bad message" loop of responses from Telegram. -- Accessing the buttons of a custom message wouldn't work in channels, - which lead to fix a completely different bug regarding starting bots. -- Disconnecting could complain if the magic ``telethon.sync`` was imported. -- Successful automatic reconnections now ask Telegram to send updates to us - once again as soon as the library is ready to listen for them. - - -Synchronous magic (v1.0) -======================== - -*Published at 2018/06/27* - -.. important:: - - If you come from Telethon pre-1.0 you **really** want to read - :ref:`compatibility-and-convenience` to port your scripts to - the new version. - -The library has been around for well over a year. A lot of improvements have -been made, a lot of user complaints have been fixed, and a lot of user desires -have been implemented. It's time to consider the public API as stable, and -remove some of the old methods that were around until now for compatibility -reasons. But there's one more surprise! - -There is a new magic ``telethon.sync`` module to let you use **all** the -methods in the :ref:`TelegramClient ` (and the types returned -from its functions) in a synchronous way, while using `asyncio` behind -the scenes! This means you're now able to do both of the following: - -.. code-block:: python - - import asyncio - - async def main(): - await client.send_message('me', 'Hello!') - - asyncio.run(main()) - - # ...can be rewritten as: - - from telethon import sync - client.send_message('me', 'Hello!') - -Both ways can coexist (you need to ``await`` if the loop is running). - -You can also use the magic ``sync`` module in your own classes, and call -``sync.syncify(cls)`` to convert all their ``async def`` into magic variants. - - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- ``message.get_fwd_sender`` is now in `message.forward - `. -- ``client.idle`` is now `client.run_until_disconnected() - ` -- ``client.add_update_handler`` is now `client.add_event_handler - ` -- ``client.remove_update_handler`` is now `client.remove_event_handler - ` -- ``client.list_update_handlers`` is now `client.list_event_handlers - ` -- ``client.get_message_history`` is now `client.get_messages - ` -- ``client.send_voice_note`` is now `client.send_file - ` with ``is_voice=True``. -- ``client.invoke()`` is now ``client(...)``. -- ``report_errors`` has been removed since it's currently not used, - and ``flood_sleep_threshold`` is now part of the client. -- The ``update_workers`` and ``spawn_read_thread`` arguments are gone. - Simply remove them from your code when you create the client. -- Methods with a lot of arguments can no longer be used without specifying - their argument. Instead you need to use named arguments. This improves - readability and not needing to learn the order of the arguments, which - can also change. - - -Additions -~~~~~~~~~ - -- `client.send_file ` now - accepts external ``http://`` and ``https://`` URLs. -- You can use the :ref:`TelegramClient ` inside of ``with`` - blocks, which will `client.start() ` - and `disconnect() ` - the client for you: - - .. code-block:: python - - from telethon import TelegramClient, sync - - with TelegramClient(name, api_id, api_hash) as client: - client.send_message('me', 'Hello!') - - Convenience at its maximum! You can even chain the `.start() - ` method since - it returns the instance of the client: - - .. code-block:: python - - with TelegramClient(name, api_id, api_hash).start(bot_token=token) as bot: - bot.send_message(chat, 'Hello!') - - -Bug fixes -~~~~~~~~~ - -- There were some ``@property async def`` left, and some ``await property``. -- "User joined" event was being treated as "User was invited". -- SQLite's cursor should not be closed properly after usage. -- ``await`` the updates task upon disconnection. -- Some bug in Python 3.5.2's `asyncio` causing 100% CPU load if you - forgot to call `client.disconnect() - `. - The method is called for you on object destruction, but you still should - disconnect manually or use a ``with`` block. -- Some fixes regarding disconnecting on client deletion and properly - saving the authorization key. -- Passing a class to `message.get_entities_text - ` now works properly. -- Iterating messages from a specific user in private messages now works. - -Enhancements -~~~~~~~~~~~~ - -- Both `client.start() ` and - `client.run_until_disconnected() - ` can - be ran in both a synchronous way (without starting the loop manually) - or from an ``async def`` where they need to have an ``await``. - - -Core Rewrite in asyncio (v1.0-rc1) -================================== - -*Published at 2018/06/24* - -+-----------------------+ -| Scheme layer used: 81 | -+-----------------------+ - -This version is a major overhaul of the library internals. The core has -been rewritten, cleaned up and refactored to fix some oddities that have -been growing inside the library. - -This means that the code is easier to understand and reason about, -including the code flow such as conditions, exceptions, where to -reconnect, how the library should behave, and separating different -retry types such as disconnections or call fails, but it also means -that **some things will necessarily break** in this version. - -All requests that touch the network are now methods and need to -have their ``await`` (or be ran until their completion). - -Also, the library finally has the simple logo it deserved: a carefully -hand-written ``.svg`` file representing a T following Python's colours. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- If you relied on internals like the ``MtProtoSender`` and the - ``TelegramBareClient``, both are gone. They are now `MTProtoSender - ` and `TelegramBaseClient - ` and they behave - differently. -- Underscores have been renamed from filenames. This means - ``telethon.errors.rpc_error_list`` won't work, but you should - have been using `telethon.errors` all this time instead. -- `client.connect ` - no longer returns `True` on success. Instead, you should ``except`` the - possible ``ConnectionError`` and act accordingly. This makes it easier to - not ignore the error. -- You can no longer set ``retries=n`` when calling a request manually. The - limit works differently now, and it's done on a per-client basis. -- Accessing `.sender `, - `.chat ` and similar may *not* work - in events anymore, since previously they could access the network. The new - rule is that properties are not allowed to make API calls. You should use - `.get_sender() `, - `.get_chat() ` instead while - using events. You can safely access properties if you get messages through - `client.get_messages() ` - or other methods in the client. -- The above point means ``reply_message`` is now `.get_reply_message() - `, and ``fwd_from_entity`` - is now `get_fwd_sender() `. - Also ``forward`` was gone in the previous version, and you should be using - ``fwd_from`` instead. - - -Additions -~~~~~~~~~ - -- Telegram's Terms Of Service are now accepted when creating a new account. - This can possibly help avoid bans. This has no effect for accounts that - were created before. -- The `method reference `_ now shows - which methods can be used if you sign in with a ``bot_token``. -- There's a new `client.disconnected - ` future - which you can wait on. When a disconnection occurs, you will now, instead - letting it happen in the background. -- More configurable retries parameters, such as auto-reconnection, retries - when connecting, and retries when sending a request. -- You can filter `events.NewMessage ` - by sender ID, and also whether they are forwards or not. -- New ``ignore_migrated`` parameter for `client.iter_dialogs - `. - -Bug fixes -~~~~~~~~~ - -- Several fixes to `telethon.events.newmessage.NewMessage`. -- Removed named ``length`` argument in ``to_bytes`` for PyPy. -- Raw events failed due to not having ``._set_client``. -- `message.get_entities_text - ` properly - supports filtering, even if there are no message entities. -- `message.click ` works better. -- The server started sending :tl:`DraftMessageEmpty` which the library - didn't handle correctly when getting dialogs. -- The "correct" chat is now always returned from returned messages. -- ``to_id`` was not validated when retrieving messages by their IDs. -- ``'__'`` is no longer considered valid in usernames. -- The ``fd`` is removed from the reader upon closing the socket. This - should be noticeable in Windows. -- :tl:`MessageEmpty` is now handled when searching messages. -- Fixed a rare infinite loop bug in `client.iter_dialogs - ` for some people. -- Fixed ``TypeError`` when there is no `.sender - `. - -Enhancements -~~~~~~~~~~~~ - -- You can now delete over 100 messages at once with `client.delete_messages - `. -- Signing in now accounts for ``AuthRestartError`` itself, and also handles - ``PasswordHashInvalidError``. -- ``__all__`` is now defined, so ``from telethon import *`` imports sane - defaults (client, events and utils). This is however discouraged and should - be used only in quick scripts. -- ``pathlib.Path`` is now supported for downloading and uploading media. -- Messages you send to yourself are now considered outgoing, unless they - are forwarded. -- The documentation has been updated with a brand new `asyncio` crash - course to encourage you use it. You can still use the threaded version - if you want though. -- ``.name`` property is now properly supported when sending and downloading - files. -- Custom ``parse_mode``, which can now be set per-client, support - :tl:`MessageEntityMentionName` so you can return those now. -- The session file is saved less often, which could result in a noticeable - speed-up when working with a lot of incoming updates. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- The flow for sending a request is as follows: the ``TelegramClient`` creates - a ``MTProtoSender`` with a ``Connection``, and the sender starts send and - receive loops. Sending a request means enqueueing it in the sender, which - will eventually pack and encrypt it with its ``ConnectionState`` instead - of using the entire ``Session`` instance. When the data is packed, it will - be sent over the ``Connection`` and ultimately over the ``TcpClient``. - -- Reconnection occurs at the ``MTProtoSender`` level, and receiving responses - follows a similar process, but now ``asyncio.Future`` is used for the results - which are no longer part of all ``TLObject``, instead are part of the - ``TLMessage`` which simplifies things. - -- Objects can no longer be ``content_related`` and instead subclass - ``TLRequest``, making the separation of concerns easier. - -- The ``TelegramClient`` has been split into several mixin classes to avoid - having a 3,000-lines-long file with all the methods. - -- More special cases in the ``MTProtoSender`` have been cleaned up, and also - some attributes from the ``Session`` which didn't really belong there since - they weren't being saved. - -- The ``telethon_generator/`` can now convert ``.tl`` files into ``.json``, - mostly as a proof of concept, but it might be useful for other people. - - -Custom Message class (v0.19.1) -============================== - -*Published at 2018/06/03* - -+-----------------------+ -| Scheme layer used: 80 | -+-----------------------+ - - -This update brings a new `telethon.tl.custom.message.Message` object! - -All the methods in the `telethon.telegram_client.TelegramClient` that -used to return a :tl:`Message` will now return this object instead, which -means you can do things like the following: - -.. code-block:: python - - msg = client.send_message(chat, 'Hello!') - msg.edit('Hello there!') - msg.reply('Good day!') - print(msg.sender) - -Refer to its documentation to see all you can do, again, click -`telethon.tl.custom.message.Message` to go to its page. - - -Breaking Changes -~~~~~~~~~~~~~~~~ - -- The `telethon.network.connection.common.Connection` class is now an ABC, - and the old ``ConnectionMode`` is now gone. Use a specific connection (like - `telethon.network.connection.tcpabridged.ConnectionTcpAbridged`) instead. - -Additions -~~~~~~~~~ - -- You can get messages by their ID with - `telethon.telegram_client.TelegramClient.get_messages`'s ``ids`` parameter: - - .. code-block:: python - - message = client.get_messages(chats, ids=123) # Single message - message_list = client.get_messages(chats, ids=[777, 778]) # Multiple - -- More convenience properties for `telethon.tl.custom.dialog.Dialog`. -- New default `telethon.telegram_client.TelegramClient.parse_mode`. -- You can edit the media of messages that already have some media. -- New dark theme in the online ``tl`` reference, check it out at - https://tl.telethon.dev/. - -Bug fixes -~~~~~~~~~ - -- Some IDs start with ``1000`` and these would be wrongly treated as channels. -- Some short usernames like ``@vote`` were being ignored. -- `telethon.telegram_client.TelegramClient.iter_messages`'s ``from_user`` - was failing if no filter had been set. -- `telethon.telegram_client.TelegramClient.iter_messages`'s ``min_id/max_id`` - was being ignored by Telegram. This is now worked around. -- `telethon.telegram_client.TelegramClient.catch_up` would fail with empty - states. -- `telethon.events.newmessage.NewMessage` supports ``incoming=False`` - to indicate ``outgoing=True``. - -Enhancements -~~~~~~~~~~~~ - -- You can now send multiple requests at once while preserving the order: - - .. code-block:: python - - from telethon.tl.functions.messages import SendMessageRequest - client([SendMessageRequest(chat, 'Hello 1!'), - SendMessageRequest(chat, 'Hello 2!')], ordered=True) - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``without rowid`` is not used in SQLite anymore. -- Unboxed serialization would fail. -- Different default limit for ``iter_messages`` and ``get_messages``. -- Some clean-up in the ``telethon_generator/`` package. - - -Catching up on Updates (v0.19) -============================== - -*Published at 2018/05/07* - -+-----------------------+ -| Scheme layer used: 76 | -+-----------------------+ - -This update prepares the library for catching up with updates with the new -`telethon.telegram_client.TelegramClient.catch_up` method. This feature needs -more testing, but for now it will let you "catch up" on some old updates that -occurred while the library was offline, and brings some new features and bug -fixes. - - -Additions -~~~~~~~~~ - -- Add ``search``, ``filter`` and ``from_user`` parameters to - `telethon.telegram_client.TelegramClient.iter_messages`. -- `telethon.telegram_client.TelegramClient.download_file` now - supports a `None` path to return the file in memory and - return its `bytes`. -- Events now have a ``.original_update`` field. - -Bug fixes -~~~~~~~~~ - -- Fixed a race condition when receiving items from the network. -- A disconnection is made when "retries reached 0". This hasn't been - tested but it might fix the bug. -- ``reply_to`` would not override :tl:`Message` object's reply value. -- Add missing caption when sending :tl:`Message` with media. - -Enhancements -~~~~~~~~~~~~ - -- Retry automatically on ``RpcCallFailError``. This error happened a lot - when iterating over many messages, and retrying often fixes it. -- Faster `telethon.telegram_client.TelegramClient.iter_messages` by - sleeping only as much as needed. -- `telethon.telegram_client.TelegramClient.edit_message` now supports - omitting the entity if you pass a :tl:`Message`. -- `telethon.events.raw.Raw` can now be filtered by type. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The library now distinguishes between MTProto and API schemas. -- :tl:`State` is now persisted to the session file. -- Connection won't retry forever. -- Fixed some errors and cleaned up the generation of code. -- Fixed typos and enhanced some documentation in general. -- Add auto-cast for :tl:`InputMessage` and :tl:`InputLocation`. - - -Pickle-able objects (v0.18.3) -============================= - -*Published at 2018/04/15* - - -Now you can use Python's ``pickle`` module to serialize ``RPCError`` and -any other ``TLObject`` thanks to **@vegeta1k95**! A fix that was fairly -simple, but still might be useful for many people. - -As a side note, the documentation at https://tl.telethon.dev -now lists known ``RPCError`` for all requests, so you know what to expect. -This required a major rewrite, but it was well worth it! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- `telethon.telegram_client.TelegramClient.forward_messages` now returns - a single item instead of a list if the input was also a single item. - -Additions -~~~~~~~~~ - -- New `telethon.events.messageread.MessageRead` event, to find out when - and who read which messages as soon as it happens. -- Now you can access ``.chat_id`` on all events and ``.sender_id`` on some. - -Bug fixes -~~~~~~~~~ - -- Possibly fix some bug regarding lost ``GzipPacked`` requests. -- The library now uses the "real" layer 75, hopefully. -- Fixed ``.entities`` name collision on updates by making it private. -- ``AUTH_KEY_DUPLICATED`` is handled automatically on connection. -- Markdown parser's offset uses ``match.start()`` to allow custom regex. -- Some filter types (as a type) were not supported by - `telethon.telegram_client.TelegramClient.iter_participants`. -- `telethon.telegram_client.TelegramClient.remove_event_handler` works. -- `telethon.telegram_client.TelegramClient.start` works on all terminals. -- :tl:`InputPeerSelf` case was missing from - `telethon.telegram_client.TelegramClient.get_input_entity`. - -Enhancements -~~~~~~~~~~~~ - -- The ``parse_mode`` for messages now accepts a callable. -- `telethon.telegram_client.TelegramClient.download_media` accepts web previews. -- `telethon.tl.custom.dialog.Dialog` instances can now be casted into - :tl:`InputPeer`. -- Better logging when reading packages "breaks". -- Better and more powerful ``setup.py gen`` command. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The library won't call ``.get_dialogs()`` on entity not found. Instead, - it will ``raise ValueError()`` so you can properly ``except`` it. -- Several new examples and updated documentation. -- ``py:obj`` is the default Sphinx's role which simplifies ``.rst`` files. -- ``setup.py`` now makes use of ``python_requires``. -- Events now live in separate files. -- Other minor changes. - - -Several bug fixes (v0.18.2) -=========================== - -*Published at 2018/03/27* - -Just a few bug fixes before they become too many. - -Additions -~~~~~~~~~ - -- Getting an entity by its positive ID should be enough, regardless of their - type (whether it's an ``User``, a ``Chat`` or a ``Channel``). Although - wrapping them inside a ``Peer`` is still recommended, it's not necessary. -- New ``client.edit_2fa`` function to change your Two Factor Authentication - settings. -- ``.stringify()`` and string representation for custom ``Dialog/Draft``. - -Bug fixes -~~~~~~~~~ - -- Some bug regarding ``.get_input_peer``. -- ``events.ChatAction`` wasn't picking up all the pins. -- ``force_document=True`` was being ignored for albums. -- Now you're able to send ``Photo`` and ``Document`` as files. -- Wrong access to a member on chat forbidden error for ``.get_participants``. - An empty list is returned instead. -- ``me/self`` check for ``.get[_input]_entity`` has been moved up so if - someone has "me" or "self" as their name they won't be retrieved. - - -Iterator methods (v0.18.1) -========================== - -*Published at 2018/03/17* - -All the ``.get_`` methods in the ``TelegramClient`` now have a ``.iter_`` -counterpart, so you can do operations while retrieving items from them. -For instance, you can ``client.iter_dialogs()`` and ``break`` once you -find what you're looking for instead fetching them all at once. - -Another big thing, you can get entities by just their positive ID. This -may cause some collisions (although it's very unlikely), and you can (should) -still be explicit about the type you want. However, it's a lot more convenient -and less confusing. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- The library only offers the default ``SQLiteSession`` again. - See :ref:`sessions` for more on how to use a different storage from now on. - -Additions -~~~~~~~~~ - -- Events now override ``__str__`` and implement ``.stringify()``, just like - every other ``TLObject`` does. -- ``events.ChatAction`` now has :meth:`respond`, :meth:`reply` and - :meth:`delete` for the message that triggered it. -- :meth:`client.iter_participants` (and its :meth:`client.get_participants` - counterpart) now expose the ``filter`` argument, and the returned users - also expose the ``.participant`` they are. -- You can now use :meth:`client.remove_event_handler` and - :meth:`client.list_event_handlers` similar how you could with normal updates. -- New properties on ``events.NewMessage``, like ``.video_note`` and ``.gif`` - to access only specific types of documents. -- The ``Draft`` class now exposes ``.text`` and ``.raw_text``, as well as a - new :meth:`Draft.send` to send it. - -Bug fixes -~~~~~~~~~ - -- ``MessageEdited`` was ignoring ``NewMessage`` constructor arguments. -- Fixes for ``Event.delete_messages`` which wouldn't handle ``MessageService``. -- Bot API style IDs not working on :meth:`client.get_input_entity`. -- :meth:`client.download_media` didn't support ``PhotoSize``. - -Enhancements -~~~~~~~~~~~~ - -- Less RPC are made when accessing the ``.sender`` and ``.chat`` of some - events (mostly those that occur in a channel). -- You can send albums larger than 10 items (they will be sliced for you), - as well as mixing normal files with photos. -- ``TLObject`` now have Python type hints. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Several documentation corrections. -- :meth:`client.get_dialogs` is only called once again when an entity is - not found to avoid flood waits. - - -Sessions overhaul (v0.18) -========================= - -*Published at 2018/03/04* - -+-----------------------+ -| Scheme layer used: 75 | -+-----------------------+ - -The ``Session``'s have been revisited thanks to the work of **@tulir** and -they now use an `ABC `__ so you -can easily implement your own! - -The default will still be a ``SQLiteSession``, but you might want to use -the new ``AlchemySessionContainer`` if you need. Refer to the section of -the documentation on :ref:`sessions` for more. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``events.MessageChanged`` doesn't exist anymore. Use the new - ``events.MessageEdited`` and ``events.MessageDeleted`` instead. - -Additions -~~~~~~~~~ - -- The mentioned addition of new session types. -- You can omit the event type on ``client.add_event_handler`` to use ``Raw``. -- You can ``raise StopPropagation`` of events if you added several of them. -- ``.get_participants()`` can now get up to 90,000 members from groups with - 100,000 if when ``aggressive=True``, "bypassing" Telegram's limit. -- You now can access ``NewMessage.Event.pattern_match``. -- Multiple captions are now supported when sending albums. -- ``client.send_message()`` has an optional ``file=`` parameter, so - you can do ``events.reply(file='/path/to/photo.jpg')`` and similar. -- Added ``.input_`` versions to ``events.ChatAction``. -- You can now access the public ``.client`` property on ``events``. -- New ``client.forward_messages``, with its own wrapper on ``events``, - called ``event.forward_to(...)``. - - -Bug fixes -~~~~~~~~~ - -- Silly bug regarding ``client.get_me(input_peer=True)``. -- ``client.send_voice_note()`` was missing some parameters. -- ``client.send_file()`` plays better with streams now. -- Incoming messages from bots weren't working with whitelists. -- Markdown's URL regex was not accepting newlines. -- Better attempt at joining background update threads. -- Use the right peer type when a marked integer ID is provided. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- Resolving ``events.Raw`` is now a no-op. -- Logging calls in the ``TcpClient`` to spot errors. -- ``events`` resolution is postponed until you are successfully connected, - so you can attach them before starting the client. -- When an entity is not found, it is searched in *all* dialogs. This might - not always be desirable but it's more comfortable for legitimate uses. -- Some non-persisting properties from the ``Session`` have been moved out. - - -Further easing library usage (v0.17.4) -====================================== - -*Published at 2018/02/24* - -Some new things and patches that already deserved their own release. - - -Additions -~~~~~~~~~ - -- New ``pattern`` argument to ``NewMessage`` to easily filter messages. -- New ``.get_participants()`` convenience method to get members from chats. -- ``.send_message()`` now accepts a ``Message`` as the ``message`` parameter. -- You can now ``.get_entity()`` through exact name match instead username. -- Raise ``ProxyConnectionError`` instead looping forever so you can - ``except`` it on your own code and behave accordingly. - -Bug fixes -~~~~~~~~~ - -- ``.parse_username`` would fail with ``www.`` or a trailing slash. -- ``events.MessageChanged`` would fail with ``UpdateDeleteMessages``. -- You can now send ``b'byte strings'`` directly as files again. -- ``.send_file()`` was not respecting the original captions when passing - another message (or media) as the file. -- Downloading media from a different data center would always log a warning - for the first time. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Use ``req_pq_multi`` instead ``req_pq`` when generating ``auth_key``. -- You can use ``.get_me(input_peer=True)`` if all you need is your self ID. -- New addition to the interactive client example to show peer information. -- Avoid special casing ``InputPeerSelf`` on some ``NewMessage`` events, so - you can always safely rely on ``.sender`` to get the right ID. - - -New small convenience functions (v0.17.3) -========================================= - -*Published at 2018/02/18* - -More bug fixes and a few others addition to make events easier to use. - -Additions -~~~~~~~~~ - -- Use ``hachoir`` to extract video and audio metadata before upload. -- New ``.add_event_handler``, ``.add_update_handler`` now deprecated. - -Bug fixes -~~~~~~~~~ - -- ``bot_token`` wouldn't work on ``.start()``, and changes to ``password`` - (now it will ask you for it if you don't provide it, as docstring hinted). -- ``.edit_message()`` was ignoring the formatting (e.g. markdown). -- Added missing case to the ``NewMessage`` event for normal groups. -- Accessing the ``.text`` of the ``NewMessage`` event was failing due - to a bug with the markdown unparser. - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``libssl`` is no longer an optional dependency. Use ``cryptg`` instead, - which you can find on https://pypi.org/project/cryptg/. - - - -New small convenience functions (v0.17.2) -========================================= - -*Published at 2018/02/15* - -Primarily bug fixing and a few welcomed additions. - -Additions -~~~~~~~~~ - -- New convenience ``.edit_message()`` method on the ``TelegramClient``. -- New ``.edit()`` and ``.delete()`` shorthands on the ``NewMessage`` event. -- Default to markdown parsing when sending and editing messages. -- Support for inline mentions when sending and editing messages. They work - like inline urls (e.g. ``[text](@username)``) and also support the Bot-API - style (see `here `__). - -Bug fixes -~~~~~~~~~ - -- Periodically send ``GetStateRequest`` automatically to keep the server - sending updates even if you're not invoking any request yourself. -- HTML parsing was failing due to not handling surrogates properly. -- ``.sign_up`` was not accepting ``int`` codes. -- Whitelisting more than one chat on ``events`` wasn't working. -- Video files are sent as a video by default unless ``force_document``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- More ``logging`` calls to help spot some bugs in the future. -- Some more logic to retrieve input entities on events. -- Clarified a few parts of the documentation. - - -Updates as Events (v0.17.1) -=========================== - -*Published at 2018/02/09* - -Of course there was more work to be done regarding updates, and it's here! -The library comes with a new ``events`` module (which you will often import -as ``from telethon import TelegramClient, events``). This are pretty much -all the additions that come with this version change, but they are a nice -addition. Refer to *(removed broken link)* to get started with events. - - -Trust the Server with Updates (v0.17) -===================================== - -*Published at 2018/02/03* - -The library trusts the server with updates again. The library will *not* -check for duplicates anymore, and when the server kicks us, it will run -``GetStateRequest`` so the server starts sending updates again (something -it wouldn't do unless you invoked something, it seems). But this update -also brings a few more changes! - -Additions -~~~~~~~~~ - -- ``TLObject``'s override ``__eq__`` and ``__ne__``, so you can compare them. -- Added some missing cases on ``.get_input_entity()`` and peer functions. -- ``obj.to_dict()`` now has a ``'_'`` key with the type used. -- ``.start()`` can also sign up now. -- More parameters for ``.get_message_history()``. -- Updated list of RPC errors. -- HTML parsing thanks to **@tulir**! It can be used similar to markdown: - ``client.send_message(..., parse_mode='html')``. - - -Enhancements -~~~~~~~~~~~~ - -- ``client.send_file()`` now accepts ``Message``'s and - ``MessageMedia``'s as the ``file`` parameter. -- Some documentation updates and fixed to clarify certain things. -- New exact match feature on https://tl.telethon.dev. -- Return as early as possible from ``.get_input_entity()`` and similar, - to avoid penalizing you for doing this right. - -Bug fixes -~~~~~~~~~ - -- ``.download_media()`` wouldn't accept a ``Document`` as parameter. -- The SQLite is now closed properly on disconnection. -- IPv6 addresses shouldn't use square braces. -- Fix regarding ``.log_out()``. -- The time offset wasn't being used (so having wrong system time would - cause the library not to work at all). - - -New ``.resolve()`` method (v0.16.2) -=================================== - -*Published at 2018/01/19* - -The ``TLObject``'s (instances returned by the API and ``Request``'s) have -now acquired a new ``.resolve()`` method. While this should be used by the -library alone (when invoking a request), it means that you can now use -``Peer`` types or even usernames where a ``InputPeer`` is required. The -object now has access to the ``client``, so that it can fetch the right -type if needed, or access the session database. Furthermore, you can -reuse requests that need "autocast" (e.g. you put :tl:`User` but ``InputPeer`` -was needed), since ``.resolve()`` is called when invoking. Before, it was -only done on object construction. - -Additions -~~~~~~~~~ - -- Album support. Just pass a list, tuple or any iterable to ``.send_file()``. - - -Enhancements -~~~~~~~~~~~~ - -- ``.start()`` asks for your phone only if required. -- Better file cache. All files under 10MB, once uploaded, should never be - needed to be re-uploaded again, as the sent media is cached to the session. - - -Bug fixes -~~~~~~~~~ - -- ``setup.py`` now calls ``gen_tl`` when installing the library if needed. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- The mentioned ``.resolve()`` to perform "autocast", more powerful. -- Upload and download methods are no longer part of ``TelegramBareClient``. -- Reuse ``.on_response()``, ``.__str__`` and ``.stringify()``. - Only override ``.on_response()`` if necessary (small amount of cases). -- Reduced "autocast" overhead as much as possible. - You shouldn't be penalized if you've provided the right type. - - -MtProto 2.0 (v0.16.1) -===================== - -*Published at 2018/01/11* - -+-----------------------+ -| Scheme layer used: 74 | -+-----------------------+ - -The library is now using MtProto 2.0! This shouldn't really affect you -as an end user, but at least it means the library will be ready by the -time MtProto 1.0 is deprecated. - -Additions -~~~~~~~~~ - -- New ``.start()`` method, to make the library avoid boilerplate code. -- ``.send_file`` accepts a new optional ``thumbnail`` parameter, and - returns the ``Message`` with the sent file. - - -Bug fixes -~~~~~~~~~ - -- The library uses again only a single connection. Less updates are - be dropped now, and the performance is even better than using temporary - connections. -- ``without rowid`` will only be used on the ``*.session`` if supported. -- Phone code hash is associated with phone, so you can change your mind - when calling ``.sign_in()``. - - -Internal changes -~~~~~~~~~~~~~~~~ - -- File cache now relies on the hash of the file uploaded instead its path, - and is now persistent in the ``*.session`` file. Report any bugs on this! -- Clearer error when invoking without being connected. -- Markdown parser doesn't work on bytes anymore (which makes it cleaner). - - -Sessions as sqlite databases (v0.16) -==================================== - -*Published at 2017/12/28* - -In the beginning, session files used to be pickle. This proved to be bad -as soon as one wanted to add more fields. For this reason, they were -migrated to use JSON instead. But this proved to be bad as soon as one -wanted to save things like entities (usernames, their ID and hash), so -now it properly uses -`sqlite3 `__, -which has been well tested, to save the session files! Calling -``.get_input_entity`` using a ``username`` no longer will need to fetch -it first, so it's really 0 calls again. Calling ``.get_entity`` will -always fetch the most up to date version. - -Furthermore, nearly everything has been documented, thus preparing the -library for `Read the Docs `__ (although there -are a few things missing I'd like to polish first), and the -`logging `__ are now -better placed. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``.get_dialogs()`` now returns a **single list** instead a tuple - consisting of a **custom class** that should make everything easier - to work with. -- ``.get_message_history()`` also returns a **single list** instead a - tuple, with the ``Message`` instances modified to make them more - convenient. - -Both lists have a ``.total`` attribute so you can still know how many -dialogs/messages are in total. - -Additions -~~~~~~~~~ - -- The mentioned use of ``sqlite3`` for the session file. -- ``.get_entity()`` now supports lists too, and it will make as little - API calls as possible if you feed it ``InputPeer`` types. Usernames - will always be resolved, since they may have changed. -- ``.set_proxy()`` method, to avoid having to create a new - ``TelegramClient``. -- More ``date`` types supported to represent a date parameter. - -Bug fixes -~~~~~~~~~ - -- Empty strings weren't working when they were a flag parameter (e.g., - setting no last name). -- Fix invalid assertion regarding flag parameters as well. -- Avoid joining the background thread on disconnect, as it would be - `None` due to a race condition. -- Correctly handle `None` dates when downloading media. -- ``.download_profile_photo`` was failing for some channels. -- ``.download_media`` wasn't handling ``Photo``. - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``date`` was being serialized as local date, but that was wrong. -- ``date`` was being represented as a ``float`` instead of an ``int``. -- ``.tl`` parser wasn't stripping inline comments. -- Removed some redundant checks on ``update_state.py``. -- Use a `synchronized - queue `__ instead a - hand crafted version. -- Use signed integers consistently (e.g. ``salt``). -- Always read the corresponding ``TLObject`` from API responses, except - for some special cases still. -- A few more ``except`` low level to correctly wrap errors. -- More accurate exception types. -- ``invokeWithLayer(initConnection(X))`` now wraps every first request - after ``.connect()``. - -As always, report if you have issues with some of the changes! - -IPv6 support (v0.15.5) -====================== - -*Published at 2017/11/16* - -+-----------------------+ -| Scheme layer used: 73 | -+-----------------------+ - -It's here, it has come! The library now **supports IPv6**! Just pass -``use_ipv6=True`` when creating a ``TelegramClient``. Note that I could -*not* test this feature because my machine doesn't have IPv6 setup. If -you know IPv6 works in your machine but the library doesn't, please -refer to `#425 `_. - -Additions -~~~~~~~~~ - -- IPv6 support. -- New method to extract the text surrounded by ``MessageEntity``\ 's, - in the ``extensions.markdown`` module. - -Enhancements -~~~~~~~~~~~~ - -- Markdown parsing is Done Right. -- Reconnection on failed invoke. Should avoid "number of retries - reached 0" (#270). -- Some missing autocast to ``Input*`` types. -- The library uses the ``NullHandler`` for ``logging`` as it should - have always done. -- ``TcpClient.is_connected()`` is now more reliable. - -.. bug-fixes-1: - -Bug fixes -~~~~~~~~~ - -- Getting an entity using their phone wasn't actually working. -- Full entities aren't saved unless they have an ``access_hash``, to - avoid some `None` errors. -- ``.get_message_history`` was failing when retrieving items that had - messages forwarded from a channel. - -General enhancements (v0.15.4) -============================== - -*Published at 2017/11/04* - -+-----------------------+ -| Scheme layer used: 72 | -+-----------------------+ - -This update brings a few general enhancements that are enough to deserve -a new release, with a new feature: beta **markdown-like parsing** for -``.send_message()``! - -.. additions-1: - -Additions -~~~~~~~~~ - -- ``.send_message()`` supports ``parse_mode='md'`` for **Markdown**! It - works in a similar fashion to the official clients (defaults to - double underscore/asterisk, like ``**this**``). Please report any - issues with emojies or enhancements for the parser! -- New ``.idle()`` method so your main thread can do useful job (listen - for updates). -- Add missing ``.to_dict()``, ``__str__`` and ``.stringify()`` for - ``TLMessage`` and ``MessageContainer``. - -.. bug-fixes-2: - -Bug fixes -~~~~~~~~~ - -- The list of known peers could end "corrupted" and have users with - ``access_hash=None``, resulting in ``struct`` error for it not being - an integer. You shouldn't encounter this issue anymore. -- The warning for "added update handler but no workers set" wasn't - actually working. -- ``.get_input_peer`` was ignoring a case for ``InputPeerSelf``. -- There used to be an exception when logging exceptions (whoops) on - update handlers. -- "Downloading contacts" would produce strange output if they had - semicolons (``;``) in their name. -- Fix some cyclic imports and installing dependencies from the ``git`` - repository. -- Code generation was using f-strings, which are only supported on - Python ≥3.6. - -Internal changes -~~~~~~~~~~~~~~~~ - -- The ``auth_key`` generation has been moved from ``.connect()`` to - ``.invoke()``. There were some issues were ``.connect()`` failed and - the ``auth_key`` was `None` so this will ensure to have a valid - ``auth_key`` when needed, even if ``BrokenAuthKeyError`` is raised. -- Support for higher limits on ``.get_history()`` and - ``.get_dialogs()``. -- Much faster integer factorization when generating the required - ``auth_key``. Thanks @delivrance for making me notice this, and for - the pull request. - -Bug fixes with updates (v0.15.3) -================================ - -*Published at 2017/10/20* - -Hopefully a very ungrateful bug has been removed. When you used to -invoke some request through update handlers, it could potentially enter -an infinite loop. This has been mitigated and it's now safe to invoke -things again! A lot of updates were being dropped (all those gzipped), -and this has been fixed too. - -More bug fixes include a `correct -parsing `__ -of certain TLObjects thanks to @stek29, and -`some `__ -`wrong -calls `__ -that would cause the library to crash thanks to @andr-04, and the -``ReadThread`` not re-starting if you were already authorized. - -Internally, the ``.to_bytes()`` function has been replaced with -``__bytes__`` so now you can do ``bytes(tlobject)``. - -Bug fixes and new small features (v0.15.2) -========================================== - -*Published at 2017/10/14* - -This release primarly focuses on a few bug fixes and enhancements. -Although more stuff may have broken along the way. - -Enhancements -~~~~~~~~~~~~ - -- You will be warned if you call ``.add_update_handler`` with no - ``update_workers``. -- New customizable threshold value on the session to determine when to - automatically sleep on flood waits. See - ``client.session.flood_sleep_threshold``. -- New ``.get_drafts()`` method with a custom ``Draft`` class by @JosXa. -- Join all threads when calling ``.disconnect()``, to assert no - dangling thread is left alive. -- Larger chunk when downloading files should result in faster - downloads. -- You can use a callable key for the ``EntityDatabase``, so it can be - any filter you need. - -.. bug-fixes-3: - -Bug fixes -~~~~~~~~~ - -- ``.get_input_entity`` was failing for IDs and other cases, also - making more requests than it should. -- Use ``basename`` instead ``abspath`` when sending a file. You can now - also override the attributes. -- ``EntityDatabase.__delitem__`` wasn't working. -- ``.send_message()`` was failing with channels. -- ``.get_dialogs(limit=None)`` should now return all the dialogs - correctly. -- Temporary fix for abusive duplicated updates. - -.. enhancements-1: - -.. internal-changes-1: - -Internal changes -~~~~~~~~~~~~~~~~ - -- MsgsAck is now sent in a container rather than its own request. -- ``.get_input_photo`` is now used in the generated code. -- ``.process_entities`` was being called from more places than only - ``__call__``. -- ``MtProtoSender`` now relies more on the generated code to read - responses. - -Custom Entity Database (v0.15.1) -================================ - -*Published at 2017/10/05* - -The main feature of this release is that Telethon now has a custom -database for all the entities you encounter, instead depending on -``@lru_cache`` on the ``.get_entity()`` method. - -The ``EntityDatabase`` will, by default, **cache** all the users, chats -and channels you find in memory for as long as the program is running. -The session will, by default, save all key-value pairs of the entity -identifiers and their hashes (since Telegram may send an ID that it -thinks you already know about, we need to save this information). - -You can **prevent** the ``EntityDatabase`` from saving users by setting -``client.session.entities.enabled = False``, and prevent the ``Session`` -from saving input entities at all by setting -``client.session.save_entities = False``. You can also clear the cache -for a certain user through -``client.session.entities.clear_cache(entity=None)``, which will clear -all if no entity is given. - - -Additions -~~~~~~~~~ - -- New method to ``.delete_messages()``. -- New ``ChannelPrivateError`` class. - -Enhancements -~~~~~~~~~~~~ - -- ``.sign_in`` accepts phones as integers. -- Changing the IP to which you connect to is as simple as - ``client.session.server_address = 'ip'``, since now the - server address is always queried from the session. - -Bug fixes -~~~~~~~~~ - -- ``.get_dialogs()`` doesn't fail on Windows anymore, and returns the - right amount of dialogs. -- ``GeneralProxyError`` should be passed to the main thread - again, so that you can handle it. - -Updates Overhaul Update (v0.15) -=============================== - -*Published at 2017/10/01* - -After hundreds of lines changed on a major refactor, *it's finally -here*. It's the **Updates Overhaul Update**; let's get right into it! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- ``.create_new_connection()`` is gone for good. No need to deal with - this manually since new connections are now handled on demand by the - library itself. - -Enhancements -~~~~~~~~~~~~ - -- You can **invoke** requests from **update handlers**. And **any other - thread**. A new temporary will be made, so that you can be sending - even several requests at the same time! -- **Several worker threads** for your updates! By default, `None` - will spawn. I recommend you to work with ``update_workers=4`` to get - started, these will be polling constantly for updates. -- You can also change the number of workers at any given time. -- The library can now run **in a single thread** again, if you don't - need to spawn any at all. Simply set ``spawn_read_thread=False`` when - creating the ``TelegramClient``! -- You can specify ``limit=None`` on ``.get_dialogs()`` to get **all** - of them[1]. -- **Updates are expanded**, so you don't need to check if the update - has ``.updates`` or an inner ``.update`` anymore. -- All ``InputPeer`` entities are **saved in the session** file, but you - can disable this by setting ``save_entities=False``. -- New ``.get_input_entity`` method, which makes use of the above - feature. You **should use this** when a request needs a - ``InputPeer``, rather than the whole entity (although both work). -- Assert that either all or None dependent-flag parameters are set - before sending the request. -- Phone numbers can have dashes, spaces, or parenthesis. They'll be - removed before making the request. -- You can override the phone and its hash on ``.sign_in()``, if you're - creating a new ``TelegramClient`` on two different places. - -Bug fixes -~~~~~~~~~ - -- ``.log_out()`` was consuming all retries. It should work just fine - now. -- The session would fail to load if the ``auth_key`` had been removed - manually. -- ``Updates.check_error`` was popping wrong side, although it's been - completely removed. -- ``ServerError``\ 's will be **ignored**, and the request will - immediately be retried. -- Cross-thread safety when saving the session file. -- Some things changed on a matter of when to reconnect, so please - report any bugs! - -.. internal-changes-2: - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``TelegramClient`` is now only an abstraction over the - ``TelegramBareClient``, which can only do basic things, such as - invoking requests, working with files, etc. If you don't need any of - the abstractions the ``TelegramClient``, you can now use the - ``TelegramBareClient`` in a much more comfortable way. -- ``MtProtoSender`` is not thread-safe, but it doesn't need to be since - a new connection will be spawned when needed. -- New connections used to be cached and then reused. Now only their - sessions are saved, as temporary connections are spawned only when - needed. -- Added more RPC errors to the list. - -**[1]:** Broken due to a condition which should had been the opposite -(sigh), fixed 4 commits ahead on -https://github.com/LonamiWebs/Telethon/commit/62ea77cbeac7c42bfac85aa8766a1b5b35e3a76c. - --------------- - -**That's pretty much it**, although there's more work to be done to make -the overall experience of working with updates *even better*. Stay -tuned! - -Serialization bug fixes (v0.14.2) -================================= - -*Published at 2017/09/29* - -Bug fixes -~~~~~~~~~ - -- **Important**, related to the serialization. Every object or request - that had to serialize a ``True/False`` type was always being serialized - as `false`! -- Another bug that didn't allow you to leave as `None` flag parameters - that needed a list has been fixed. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Other internal changes include a somewhat more readable ``.to_bytes()`` - function and pre-computing the flag instead using bit shifting. The - ``TLObject.constructor_id`` has been renamed to ``TLObject.CONSTRUCTOR_ID``, - and ``.subclass_of_id`` is also uppercase now. - -Farewell, BinaryWriter (v0.14.1) -================================ - -*Published at 2017/09/28* - -Version ``v0.14`` had started working on the new ``.to_bytes()`` method -to dump the ``BinaryWriter`` and its usage on the ``.on_send()`` when -serializing TLObjects, and this release finally removes it. The speed up -when serializing things to bytes should now be over twice as fast -wherever it's needed. - -Bug fixes -~~~~~~~~~ - -- This version is again compatible with Python 3.x versions **below 3.5** - (there was a method call that was Python 3.5 and above). - -Internal changes -~~~~~~~~~~~~~~~~ - -- Using proper classes (including the generated code) for generating - authorization keys and to write out ``TLMessage``\ 's. - - -Several requests at once and upload compression (v0.14) -======================================================= - -*Published at 2017/09/27* - -New major release, since I've decided that these two features are big -enough: - -Additions -~~~~~~~~~ - -- Requests larger than 512 bytes will be **compressed through - gzip**, and if the result is smaller, this will be uploaded instead. -- You can now send **multiple requests at once**, they're simply - ``*var_args`` on the ``.invoke()``. Note that the server doesn't - guarantee the order in which they'll be executed! - -Internally, another important change. The ``.on_send`` function on the -``TLObjects`` is **gone**, and now there's a new ``.to_bytes()``. From -my tests, this has always been over twice as fast serializing objects, -although more replacements need to be done, so please report any issues. - -Enhancements -~~~~~~~~~~~~ -- Implemented ``.get_input_media`` helper methods. Now you can even use - another message as input media! - - -Bug fixes -~~~~~~~~~ - -- Downloading media from CDNs wasn't working (wrong - access to a parameter). -- Correct type hinting. -- Added a tiny sleep when trying to perform automatic reconnection. -- Error reporting is done in the background, and has a shorter timeout. -- ``setup.py`` used to fail with wrongly generated code. - -Quick fix-up (v0.13.6) -====================== - -*Published at 2017/09/23* - -Before getting any further, here's a quick fix-up with things that -should have been on ``v0.13.5`` but were missed. Specifically, the -**timeout when receiving** a request will now work properly. - -Some other additions are a tiny fix when **handling updates**, which was -ignoring some of them, nicer ``__str__`` and ``.stringify()`` methods -for the ``TLObject``\ 's, and not stopping the ``ReadThread`` if you try -invoking something there (now it simply returns `None`). - -Attempts at more stability (v0.13.5) -==================================== - -*Published at 2017/09/23* - -Yet another update to fix some bugs and increase the stability of the -library, or, at least, that was the attempt! - -This release should really **improve the experience with the background -thread** that the library starts to read things from the network as soon -as it can, but I can't spot every use case, so please report any bug -(and as always, minimal reproducible use cases will help a lot). - -.. bug-fixes-4: - -Bug fixes -~~~~~~~~~ - -- ``setup.py`` was failing on Python < 3.5 due to some imports. -- Duplicated updates should now be ignored. -- ``.send_message`` would crash in some cases, due to having a typo - using the wrong object. -- ``"socket is None"`` when calling ``.connect()`` should not happen - anymore. -- ``BrokenPipeError`` was still being raised due to an incorrect order - on the ``try/except`` block. - -.. enhancements-2: - -Enhancements -~~~~~~~~~~~~ - -- **Type hinting** for all the generated ``Request``\ 's and - ``TLObjects``! IDEs like PyCharm will benefit from this. -- ``ProxyConnectionError`` should properly be passed to the main thread - for you to handle. -- The background thread will only be started after you're authorized on - Telegram (i.e. logged in), and several other attempts at polishing - the experience with this thread. -- The ``Connection`` instance is only created once now, and reused - later. -- Calling ``.connect()`` should have a better behavior now (like - actually *trying* to connect even if we seemingly were connected - already). -- ``.reconnect()`` behavior has been changed to also be more consistent - by making the assumption that we'll only reconnect if the server has - disconnected us, and is now private. - -.. other-changes-1: - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``TLObject.__repr__`` doesn't show the original TL definition - anymore, it was a lot of clutter. If you have any complaints open an - issue and we can discuss it. -- Internally, the ``'+'`` from the phone number is now stripped, since - it shouldn't be included. -- Spotted a new place where ``BrokenAuthKeyError`` would be raised, and - it now is raised there. - -More bug fixes and enhancements (v0.13.4) -========================================= - -*Published at 2017/09/18* - -.. new-stuff-1: - -Additions -~~~~~~~~~ - -- ``TelegramClient`` now exposes a ``.is_connected()`` method. -- Initial authorization on a new data center will retry up to 5 times - by default. -- Errors that couldn't be handled on the background thread will be - raised on the next call to ``.invoke()`` or ``updates.poll()``. - -.. bugs-fixed-1: - -Bug fixes -~~~~~~~~~~ - -- Now you should be able to sign in even if you have - ``process_updates=True`` and no previous session. -- Some errors and methods are documented a bit clearer. -- ``.send_message()`` could randomly fail, as the returned type was not - expected. -- ``TimeoutError`` is now ignored, since the request will be retried up - to 5 times by default. -- "-404" errors (``BrokenAuthKeyError``\ 's) are now detected when - first connecting to a new data center. -- ``BufferError`` is handled more gracefully, in the same way as - ``InvalidCheckSumError``\ 's. -- Attempt at fixing some "NoneType has no attribute…" errors (with the - ``.sender``). - -Internal changes -~~~~~~~~~~~~~~~~ - -- Calling ``GetConfigRequest`` is now made less often. -- The ``initial_query`` parameter from ``.connect()`` is gone, as it's - not needed anymore. -- Renamed ``all_tlobjects.layer`` to ``all_tlobjects.LAYER`` (since - it's a constant). -- The message from ``BufferError`` is now more useful. - -Bug fixes and enhancements (v0.13.3) -==================================== - -*Published at 2017/09/14* - -.. bugs-fixed-2: - -Bug fixes -~~~~~~~~~ - -- **Reconnection** used to fail because it tried invoking things from - the ``ReadThread``. -- Inferring **random ids** for ``ForwardMessagesRequest`` wasn't - working. -- Downloading media from **CDNs** failed due to having forgotten to - remove a single line. -- ``TcpClient.close()`` now has a **``threading.Lock``**, so - ``NoneType has no close()`` should not happen. -- New **workaround** for ``msg seqno too low/high``. Also, both - ``Session.id/seq`` are not saved anymore. - -.. enhancements-3: - -Enhancements -~~~~~~~~~~~~ - -- **Request will be retried** up to 5 times by default rather than - failing on the first attempt. -- ``InvalidChecksumError``\ 's are now **ignored** by the library. -- ``TelegramClient.get_entity()`` is now **public**, and uses the - ``@lru_cache()`` decorator. -- New method to **``.send_voice_note()``**\ 's. -- Methods to send message and media now support a **``reply_to`` - parameter**. -- ``.send_message()`` now returns the **full message** which was just - sent. - -New way to work with updates (v0.13.2) -====================================== - -*Published at 2017/09/08* - -This update brings a new way to work with updates, and it's begging for -your **feedback**, or better names or ways to do what you can do now. - -Please refer to the `wiki/Usage -Modes `__ for -an in-depth description on how to work with updates now. Notice that you -cannot invoke requests from within handlers anymore, only the -``v.0.13.1`` patch allowed you to do so. - -Bug fixes -~~~~~~~~~ - -- Periodic pings are back. -- The username regex mentioned on ``UsernameInvalidError`` was invalid, - but it has now been fixed. -- Sending a message to a phone number was failing because the type used - for a request had changed on layer 71. -- CDN downloads weren't working properly, and now a few patches have been - applied to ensure more reliability, although I couldn't personally test - this, so again, report any feedback. - -Invoke other requests from within update callbacks (v0.13.1) -============================================================ - -*Published at 2017/09/04* - -.. warning:: - - This update brings some big changes to the update system, - so please read it if you work with them! - -A silly "bug" which hadn't been spotted has now been fixed. Now you can -invoke other requests from within your update callbacks. However **this -is not advised**. You should post these updates to some other thread, -and let that thread do the job instead. Invoking a request from within a -callback will mean that, while this request is being invoked, no other -things will be read. - -Internally, the generated code now resides under a *lot* less files, -simply for the sake of avoiding so many unnecessary files. The generated -code is not meant to be read by anyone, simply to do its job. - -Unused attributes have been removed from the ``TLObject`` class too, and -``.sign_up()`` returns the user that just logged in in a similar way to -``.sign_in()`` now. - -Connection modes (v0.13) -======================== - -*Published at 2017/09/04* - -+-----------------------+ -| Scheme layer used: 71 | -+-----------------------+ - -The purpose of this release is to denote a big change, now you can -connect to Telegram through different `**connection -modes** `__. -Also, a **second thread** will *always* be started when you connect a -``TelegramClient``, despite whether you'll be handling updates or -ignoring them, whose sole purpose is to constantly read from the -network. - -The reason for this change is as simple as *"reading and writing -shouldn't be related"*. Even when you're simply ignoring updates, this -way, once you send a request you will only need to read the result for -the request. Whatever Telegram sent before has already been read and -outside the buffer. - -.. additions-2: - -Additions -~~~~~~~~~ - -- The mentioned different connection modes, and a new thread. -- You can modify the ``Session`` attributes through the - ``TelegramClient`` constructor (using ``**kwargs``). -- ``RPCError``\ 's now belong to some request you've made, which makes - more sense. -- ``get_input_*`` now handles `None` (default) parameters more - gracefully (it used to crash). - -.. enhancements-4: - -Enhancements -~~~~~~~~~~~~ - -- The low-level socket doesn't use a handcrafted timeout anymore, which - should benefit by avoiding the arbitrary ``sleep(0.1)`` that there - used to be. -- ``TelegramClient.sign_in`` will call ``.send_code_request`` if no - ``code`` was provided. - -Deprecation -~~~~~~~~~~~ - -- ``.sign_up`` does *not* take a ``phone`` argument anymore. Change - this or you will be using ``phone`` as ``code``, and it will fail! - The definition looks like - ``def sign_up(self, code, first_name, last_name='')``. -- The old ``JsonSession`` finally replaces the original ``Session`` - (which used pickle). If you were overriding any of these, you should - only worry about overriding ``Session`` now. - -Added verification for CDN file (v0.12.2) -========================================= - -*Published at 2017/08/28* - -Since the Content Distributed Network (CDN) is not handled by Telegram -itself, the owners may tamper these files. Telegram sends their sha256 -sum for clients to implement this additional verification step, which -now the library has. If any CDN has altered the file you're trying to -download, ``CdnFileTamperedError`` will be raised to let you know. - -Besides this. ``TLObject.stringify()`` was showing bytes as lists (now -fixed) and RPC errors are reported by default: - - In an attempt to help everyone who works with the Telegram API, - Telethon will by default report all Remote Procedure Call errors to - `PWRTelegram `__, a public database anyone can - query, made by `Daniil `__. All the information - sent is a GET request with the error code, error message and method used. - - -.. note:: - - If you still would like to opt out, simply set - ``client.session.report_errors = False`` to disable this feature. - However Daniil would really thank you if you helped him (and everyone) - by keeping it on! - -CDN support (v0.12.1) -===================== - -*Published at 2017/08/24* - -The biggest news for this update are that downloading media from CDN's -(you'll often encounter this when working with popular channels) now -**works**. - -Bug fixes -~~~~~~~~~ - -- The method used to download documents crashed because - two lines were swapped. -- Determining the right path when downloading any file was - very weird, now it's been enhanced. -- The ``.sign_in()`` method didn't support integer values for the code! - Now it does again. - -Some important internal changes are that the old way to deal with RSA -public keys now uses a different module instead the old strange -hand-crafted version. - -Hope the new, super simple ``README.rst`` encourages people to use -Telethon and make it better with either suggestions, or pull request. -Pull requests are *super* appreciated, but showing some support by -leaving a star also feels nice ⭐️. - -Newbie friendly update (v0.12) -============================== - -*Published at 2017/08/22* - -+-----------------------+ -| Scheme layer used: 70 | -+-----------------------+ - -This update is overall an attempt to make Telethon a bit more user -friendly, along with some other stability enhancements, although it -brings quite a few changes. - -Breaking changes -~~~~~~~~~~~~~~~~ - -- The ``TelegramClient`` methods ``.send_photo_file()``, - ``.send_document_file()`` and ``.send_media_file()`` are now a - **single method** called ``.send_file()``. It's also important to - note that the **order** of the parameters has been **swapped**: first - to *who* you want to send it, then the file itself. - -- The same applies to ``.download_msg_media()``, which has been renamed - to ``.download_media()``. The method now supports a ``Message`` - itself too, rather than only ``Message.media``. The specialized - ``.download_photo()``, ``.download_document()`` and - ``.download_contact()`` still exist, but are private. - -Additions -~~~~~~~~~ - -- Updated to **layer 70**! -- Both downloading and uploading now support **stream-like objects**. -- A lot **faster initial connection** if ``sympy`` is installed (can be - installed through ``pip``). -- ``libssl`` will also be used if available on your system (likely on - Linux based systems). This speed boost should also apply to uploading - and downloading files. -- You can use a **phone number** or an **username** for methods like - ``.send_message()``, ``.send_file()``, and all the other quick-access - methods provided by the ``TelegramClient``. - -.. bug-fixes-5: - -Bug fixes -~~~~~~~~~ - -- Crashing when migrating to a new layer and receiving old updates - should not happen now. -- ``InputPeerChannel`` is now casted to ``InputChannel`` automtically - too. -- ``.get_new_msg_id()`` should now be thread-safe. No promises. -- Logging out on macOS caused a crash, which should be gone now. -- More checks to ensure that the connection is flagged correctly as - either connected or not. - -.. note:: - - Downloading files from CDN's will **not work** yet (something new - that comes with layer 70). - --------------- - -That's it, any new idea or suggestion about how to make the project even -more friendly is highly appreciated. - -.. note:: - - Did you know that you can pretty print any result Telegram returns - (called ``TLObject``\ 's) by using their ``.stringify()`` function? - Great for debugging! - -get_input_* now works with vectors (v0.11.5) -============================================= - -*Published at 2017/07/11* - -Quick fix-up of a bug which hadn't been encountered until now. Auto-cast -by using ``get_input_*`` now works. - -get_input_* everywhere (v0.11.4) -================================= - -*Published at 2017/07/10* - -For some reason, Telegram doesn't have enough with the -`InputPeer `__. -There also exist -`InputChannel `__ -and -`InputUser `__! -You don't have to worry about those anymore, it's handled internally -now. - -Besides this, every Telegram object now features a new default -``.__str__`` look, and also a `.stringify() -method `__ -to pretty format them, if you ever need to inspect them. - -The library now uses `the DEBUG -level `__ -everywhere, so no more warnings or information messages if you had -logging enabled. - -The ``no_webpage`` parameter from ``.send_message`` `has been -renamed `__ -to ``link_preview`` for clarity, so now it does the opposite (but has a -clearer intention). - -Quick .send_message() fix (v0.11.3) -=================================== - -*Published at 2017/07/05* - -A very quick follow-up release to fix a tiny bug with -``.send_message()``, no new features. - -Callable TelegramClient (v0.11.2) -================================= - -*Published at 2017/07/04* - -+-----------------------+ -| Scheme layer used: 68 | -+-----------------------+ - -There is a new preferred way to **invoke requests**, which you're -encouraged to use: - -.. code:: python - - # New! - result = client(SomeRequest()) - - # Old. - result = client.invoke(SomeRequest()) - -Existing code will continue working, since the old ``.invoke()`` has not -been deprecated. - -When you ``.create_new_connection()``, it will also handle -``FileMigrateError``\ 's for you, so you don't need to worry about those -anymore. - -.. bugs-fixed-3: - -Bugs fixes -~~~~~~~~~~ - -- Fixed some errors when installing Telethon via ``pip`` (for those - using either source distributions or a Python version ≤ 3.5). -- ``ConnectionResetError`` didn't flag sockets as closed, but now it - does. - -On a more technical side, ``msg_id``\ 's are now more accurate. - -Improvements to the updates (v0.11.1) -===================================== - -*Published at 2017/06/24* - -Receiving new updates shouldn't miss any anymore, also, periodic pings -are back again so it should work on the long run. - -On a different order of things, ``.connect()`` also features a timeout. -Notice that the ``timeout=`` is **not** passed as a **parameter** -anymore, and is instead specified when creating the ``TelegramClient``. - -Bug fixes -~~~~~~~~~ - -- Fixed some name class when a request had a ``.msg_id`` parameter. -- The correct amount of random bytes is now used in DH request -- Fixed ``CONNECTION_APP_VERSION_EMPTY`` when using temporary sessions. -- Avoid connecting if already connected. - -Support for parallel connections (v0.11) -======================================== - -*Published at 2017/06/16* - -*This update brings a lot of changes, so it would be nice if you could* -**read the whole change log**! - -Breaking changes -~~~~~~~~~~~~~~~~ - -- Every Telegram error has now its **own class**, so it's easier to - fine-tune your ``except``\ 's. -- Markdown parsing is **not part** of Telethon itself anymore, although - there are plans to support it again through a some external module. -- The ``.list_sessions()`` has been moved to the ``Session`` class - instead. -- The ``InteractiveTelegramClient`` is **not** shipped with ``pip`` - anymore. - -Additions -~~~~~~~~~ - -- A new, more **lightweight class** has been added. The - ``TelegramBareClient`` is now the base of the normal - ``TelegramClient``, and has the most basic features. -- New method to ``.create_new_connection()``, which can be ran **in - parallel** with the original connection. This will return the - previously mentioned ``TelegramBareClient`` already connected. -- Any file object can now be used to download a file (for instance, a - ``BytesIO()`` instead a file name). -- Vales like ``random_id`` are now **automatically inferred**, so you - can save yourself from the hassle of writing - ``generate_random_long()`` everywhere. Same applies to - ``.get_input_peer()``, unless you really need the extra performance - provided by skipping one ``if`` if called manually. -- Every type now features a new ``.to_dict()`` method. - -.. bug-fixes-6: - -Bug fixes -~~~~~~~~~ - -- Received errors are acknowledged to the server, so they don't happen - over and over. -- Downloading media on different data centers is now up to **x2 - faster**, since there used to be an ``InvalidDCError`` for each file - part tried to be downloaded. -- Lost messages are now properly skipped. -- New way to handle the **result of requests**. The old ``ValueError`` - "*The previously sent request must be resent. However, no request was - previously sent (possibly called from a different thread).*" *should* - not happen anymore. - -Internal changes -~~~~~~~~~~~~~~~~ - -- Some fixes to the ``JsonSession``. -- Fixed possibly crashes if trying to ``.invoke()`` a ``Request`` while - ``.reconnect()`` was being called on the ``UpdatesThread``. -- Some improvements on the ``TcpClient``, such as not switching between - blocking and non-blocking sockets. -- The code now uses ASCII characters only. -- Some enhancements to ``.find_user_or_chat()`` and - ``.get_input_peer()``. - -JSON session file (v0.10.1) -=========================== - -*Published at 2017/06/07* - -This version is primarily for people to **migrate** their ``.session`` -files, which are *pickled*, to the new *JSON* format. Although slightly -slower, and a bit more vulnerable since it's plain text, it's a lot more -resistant to upgrades. - -.. warning:: - - You **must** upgrade to this version before any higher one if you've - used Telethon ≤ v0.10. If you happen to upgrade to an higher version, - that's okay, but you will have to manually delete the ``*.session`` file, - and logout from that session from an official client. - -Additions -~~~~~~~~~ - -- New ``.get_me()`` function to get the **current** user. -- ``.is_user_authorized()`` is now more reliable. -- New nice button to copy the ``from telethon.tl.xxx.yyy import Yyy`` - on the online documentation. -- **More error codes** added to the ``errors`` file. - -Enhancements -~~~~~~~~~~~~ - -- Everything on the documentation is now, theoretically, **sorted - alphabetically**. -- No second thread is spawned unless one or more update handlers are added. - -Full support for different DCs and ++stable (v0.10) -=================================================== - -*Published at 2017/06/03* - -Working with **different data centers** finally *works*! On a different -order of things, **reconnection** is now performed automatically every -time Telegram decides to kick us off their servers, so now Telethon can -really run **forever and ever**! In theory. - -Enhancements -~~~~~~~~~~~~ - -- **Documentation** improvements, such as showing the return type. -- The ``msg_id too low/high`` error should happen **less often**, if - any. -- Sleeping on the main thread is **not done anymore**. You will have to - ``except FloodWaitError``\ 's. -- You can now specify your *own application version*, device model, - system version and language code. -- Code is now more *pythonic* (such as making some members private), - and other internal improvements (which affect the **updates - thread**), such as using ``logger`` instead a bare ``print()`` too. - -This brings Telethon a whole step closer to ``v1.0``, though more things -should preferably be changed. - -Stability improvements (v0.9.1) -=============================== - -*Published at 2017/05/23* - -Telethon used to crash a lot when logging in for the very first time. -The reason for this was that the reconnection (or dead connections) were -not handled properly. Now they are, so you should be able to login -directly, without needing to delete the ``*.session`` file anymore. -Notice that downloading from a different DC is still a WIP. - -Enhancements -~~~~~~~~~~~~ - -- Updates thread is only started after a successful login. -- Files meant to be ran by the user now use **shebangs** and - proper permissions. -- In-code documentation now shows the returning type. -- **Relative import** is now used everywhere, so you can rename - ``telethon`` to anything else. -- **Dead connections** are now **detected** instead entering an infinite loop. -- **Sockets** can now be **closed** (and re-opened) properly. -- Telegram decided to update the layer 66 without increasing the number. - This has been fixed and now we're up-to-date again. - -General improvements (v0.9) -=========================== - -*Published at 2017/05/19* - -+-----------------------+ -| Scheme layer used: 66 | -+-----------------------+ - -Additions -~~~~~~~~~ - -- The **documentation**, available online - `here `__, has a new search bar. -- Better **cross-thread safety** by using ``threading.Event``. -- More improvements for running Telethon during a **long period of time**. - -Bug fixes -~~~~~~~~~ - -- **Avoid a certain crash on login** (occurred if an unexpected object - ID was received). -- Avoid crashing with certain invalid UTF-8 strings. -- Avoid crashing on certain terminals by using known ASCII characters - where possible. -- The ``UpdatesThread`` is now a daemon, and should cause less issues. -- Temporary sessions didn't actually work (with ``session=None``). - -Internal changes -~~~~~~~~~~~~~~~~ - -- ``.get_dialogs(count=`` was renamed to ``.get_dialogs(limit=``. - -Bot login and proxy support (v0.8) -================================== - -*Published at 2017/04/14* - -Additions -~~~~~~~~~ - -- **Bot login**, thanks to @JuanPotato for hinting me about how to do - it. -- **Proxy support**, thanks to @exzhawk for implementing it. -- **Logging support**, used by passing ``--telethon-log=DEBUG`` (or - ``INFO``) as a command line argument. - -Bug fixes -~~~~~~~~~ - -- Connection fixes, such as avoiding connection until ``.connect()`` is - explicitly invoked. -- Uploading big files now works correctly. -- Fix uploading big files. -- Some fixes on the updates thread, such as correctly sleeping when required. - -Long-run bug fix (v0.7.1) -========================= - -*Published at 2017/02/19* - -If you're one of those who runs Telethon for a long time (more than 30 -minutes), this update by @strayge will be great for you. It sends -periodic pings to the Telegram servers so you don't get disconnected and -you can still send and receive updates! - -Two factor authentication (v0.7) -================================ - -*Published at 2017/01/31* - -+-----------------------+ -| Scheme layer used: 62 | -+-----------------------+ - -If you're one of those who love security the most, these are good news. -You can now use two factor authentication with Telethon too! As internal -changes, the coding style has been improved, and you can easily use -custom session objects, and various little bugs have been fixed. - -Updated pip version (v0.6) -========================== - -*Published at 2016/11/13* - -+-----------------------+ -| Scheme layer used: 57 | -+-----------------------+ - -This release has no new major features. However, it contains some small -changes that make using Telethon a little bit easier. Now those who have -installed Telethon via ``pip`` can also take advantage of changes, such -as less bugs, creating empty instances of ``TLObjects``, specifying a -timeout and more! - -Ready, pip, go! (v0.5) -====================== - -*Published at 2016/09/18* - -Telethon is now available as a **`Python -package `__**! Those are -really exciting news (except, sadly, the project structure had to change -*a lot* to be able to do that; but hopefully it won't need to change -much more, any more!) - -Not only that, but more improvements have also been made: you're now -able to both **sign up** and **logout**, watch a pretty -"Uploading/Downloading… x%" progress, and other minor changes which make -using Telethon **easier**. - -Made InteractiveTelegramClient cool (v0.4) -========================================== - -*Published at 2016/09/12* - -Yes, really cool! I promise. Even though this is meant to be a -*library*, that doesn't mean it can't have a good *interactive client* -for you to try the library out. This is why now you can do many, many -things with the ``InteractiveTelegramClient``: - -- **List dialogs** (chats) and pick any you wish. -- **Send any message** you like, text, photos or even documents. -- **List** the **latest messages** in the chat. -- **Download** any message's media (photos, documents or even contacts!). -- **Receive message updates** as you talk (i.e., someone sent you a message). - -It actually is a usable-enough client for your day by day. You could -even add ``libnotify`` and pop, you're done! A great cli-client with -desktop notifications. - -Also, being able to download and upload media implies that you can do -the same with the library itself. Did I need to mention that? Oh, and -now, with even less bugs! I hope. - -Media revolution and improvements to update handling! (v0.3) -============================================================ - -*Published at 2016/09/11* - -Telegram is more than an application to send and receive messages. You -can also **send and receive media**. Now, this implementation also gives -you the power to upload and download media from any message that -contains it! Nothing can now stop you from filling up all your disk -space with all the photos! If you want to, of course. - -Handle updates in their own thread! (v0.2) -========================================== - -*Published at 2016/09/10* - -This version handles **updates in a different thread** (if you wish to -do so). This means that both the low level ``TcpClient`` and the -not-so-low-level ``MtProtoSender`` are now multi-thread safe, so you can -use them with more than a single thread without worrying! - -This also implies that you won't need to send a request to **receive an -update** (is someone typing? did they send me a message? has someone -gone offline?). They will all be received **instantly**. - -Some other cool examples of things that you can do: when someone tells -you "*Hello*", you can automatically reply with another "*Hello*" -without even needing to type it by yourself :) - -However, be careful with spamming!! Do **not** use the program for that! - -First working alpha version! (v0.1) -=================================== - -*Published at 2016/09/06* - -+-----------------------+ -| Scheme layer used: 55 | -+-----------------------+ - -There probably are some bugs left, which haven't yet been found. -However, the majority of code works and the application is already -usable! Not only that, but also uses the latest scheme as of now *and* -handles way better the errors. This tag is being used to mark this -release as stable enough. diff --git a/readthedocs/misc/compatibility-and-convenience.rst b/readthedocs/misc/compatibility-and-convenience.rst deleted file mode 100644 index ab9a103a..00000000 --- a/readthedocs/misc/compatibility-and-convenience.rst +++ /dev/null @@ -1,185 +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() - - asyncio.run(main()) - - -The ``telethon.sync`` magic module essentially wraps every method behind: - -.. code-block:: python - - asyncio.run(main()) - -With some other tricks, so that you don't have to write it yourself every time. -That's the overhead you pay if you import it, and what you save if you don't. - -Learning -======== - -You know the library uses `asyncio` everywhere, and you want to learn -how to do things right. Even though `asyncio` is its own topic, the -documentation wants you to learn how to use Telethon correctly, and for -that, you need to use `asyncio` correctly too. For this reason, there -is a section called :ref:`mastering-asyncio` that will introduce you to -the `asyncio` world, with links to more resources for learning how to -use it. Feel free to check that section out once you have read the rest. diff --git a/readthedocs/misc/wall-of-shame.rst b/readthedocs/misc/wall-of-shame.rst deleted file mode 100644 index 87be0464..00000000 --- a/readthedocs/misc/wall-of-shame.rst +++ /dev/null @@ -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 deleted file mode 100644 index de5502c9..00000000 --- a/readthedocs/modules/client.rst +++ /dev/null @@ -1,103 +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 - - from telethon import TelegramClient - - client = TelegramClient(name, api_id, api_hash) - - async def main(): - # Now you can use all client methods listed below, like for example... - await client.send_message('me', 'Hello to myself!') - - with client: - client.loop.run_until_complete(main()) - - -You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. -Together they are the `TelegramClient ` and -you can access all of their methods. - -See :ref:`client-ref` for a short summary. - -.. automodule:: telethon.client.telegramclient - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.telegrambaseclient - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.account - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.auth - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.bots - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.buttons - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.chats - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.dialogs - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.downloads - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.messageparse - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.messages - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.updates - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.uploads - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.client.users - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/custom.rst b/readthedocs/modules/custom.rst deleted file mode 100644 index 074b2161..00000000 --- a/readthedocs/modules/custom.rst +++ /dev/null @@ -1,163 +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: - - -ParticipantPermissions -====================== - -.. automodule:: telethon.tl.custom.participantpermissions - :members: - :undoc-members: - :show-inheritance: - - -QRLogin -======= - -.. automodule:: telethon.tl.custom.qrlogin - :members: - :undoc-members: - :show-inheritance: - - -SenderGetter -============ - -.. automodule:: telethon.tl.custom.sendergetter - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/errors.rst b/readthedocs/modules/errors.rst deleted file mode 100644 index 79a1243d..00000000 --- a/readthedocs/modules/errors.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. _telethon-errors: - -========== -API Errors -========== - -These are the base errors that Telegram's API may raise. - -See :ref:`rpc-errors` for a more in-depth explanation on how to handle all -known possible errors and learning to determine what a method may raise. - -.. automodule:: telethon.errors.common - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: telethon.errors.rpcbaseerrors - :members: - :undoc-members: - :show-inheritance: diff --git a/readthedocs/modules/events.rst b/readthedocs/modules/events.rst deleted file mode 100644 index 4344156a..00000000 --- a/readthedocs/modules/events.rst +++ /dev/null @@ -1,70 +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.album - :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 deleted file mode 100644 index cffe53e5..00000000 --- a/readthedocs/modules/helpers.rst +++ /dev/null @@ -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 deleted file mode 100644 index 3395fa51..00000000 --- a/readthedocs/modules/network.rst +++ /dev/null @@ -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 deleted file mode 100644 index 86ae22a4..00000000 --- a/readthedocs/modules/sessions.rst +++ /dev/null @@ -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 deleted file mode 100644 index 2fab89a2..00000000 --- a/readthedocs/modules/utils.rst +++ /dev/null @@ -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 deleted file mode 100644 index 287fecaf..00000000 --- a/readthedocs/quick-references/client-reference.rst +++ /dev/null @@ -1,202 +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 a usage example or further description of the arguments, be -sure to follow the links. - -.. contents:: - -TelegramClient -============== - -This is a summary of the methods and -properties you will find at :ref:`telethon-client`. - -Auth ----- - -.. currentmodule:: telethon.client.auth.AuthMethods - -.. autosummary:: - :nosignatures: - - start - send_code_request - sign_in - qr_login - log_out - edit_2fa - -Base ----- - -.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient - -.. autosummary:: - :nosignatures: - - connect - disconnect - is_connected - disconnected - loop - set_proxy - -Messages --------- - -.. py:currentmodule:: telethon.client.messages.MessageMethods - -.. autosummary:: - :nosignatures: - - send_message - edit_message - delete_messages - forward_messages - iter_messages - get_messages - pin_message - unpin_message - send_read_acknowledge - -Uploads -------- - -.. py:currentmodule:: telethon.client.uploads.UploadMethods - -.. autosummary:: - :nosignatures: - - send_file - upload_file - -Downloads ---------- - -.. currentmodule:: telethon.client.downloads.DownloadMethods - -.. autosummary:: - :nosignatures: - - download_media - download_profile_photo - download_file - iter_download - -Dialogs -------- - -.. py:currentmodule:: telethon.client.dialogs.DialogMethods - -.. autosummary:: - :nosignatures: - - iter_dialogs - get_dialogs - edit_folder - iter_drafts - get_drafts - delete_dialog - conversation - -Users ------ - -.. py:currentmodule:: telethon.client.users.UserMethods - -.. autosummary:: - :nosignatures: - - get_me - is_bot - is_user_authorized - get_entity - get_input_entity - get_peer_id - -Chats ------ - -.. currentmodule:: telethon.client.chats.ChatMethods - -.. autosummary:: - :nosignatures: - - iter_participants - get_participants - kick_participant - iter_admin_log - get_admin_log - iter_profile_photos - get_profile_photos - edit_admin - edit_permissions - get_permissions - get_stats - action - -Parse Mode ----------- - -.. py:currentmodule:: telethon.client.messageparse.MessageParseMethods - -.. autosummary:: - :nosignatures: - - parse_mode - -Updates -------- - -.. py:currentmodule:: telethon.client.updates.UpdateMethods - -.. autosummary:: - :nosignatures: - - on - run_until_disconnected - add_event_handler - remove_event_handler - list_event_handlers - catch_up - set_receive_updates - -Bots ----- - -.. currentmodule:: telethon.client.bots.BotMethods - -.. autosummary:: - :nosignatures: - - inline_query - -Buttons -------- - -.. currentmodule:: telethon.client.buttons.ButtonMethods - -.. autosummary:: - :nosignatures: - - build_reply_markup - -Account -------- - -.. currentmodule:: telethon.client.account.AccountMethods - -.. autosummary:: - :nosignatures: - - takeout - end_takeout diff --git a/readthedocs/quick-references/events-reference.rst b/readthedocs/quick-references/events-reference.rst deleted file mode 100644 index eb83cce2..00000000 --- a/readthedocs/quick-references/events-reference.rst +++ /dev/null @@ -1,247 +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 on certain chat actions, such as chat title changes, -user join or leaves, pinned messages, photo changes, etc. - -Full documentation for the `ChatAction -`. - -.. currentmodule:: telethon.events.chataction.ChatAction.Event - -.. autosummary:: - :nosignatures: - - added_by - kicked_by - user - input_user - user_id - users - input_users - user_ids - - respond - reply - delete - get_pinned_message - get_added_by - get_kicked_by - get_user - get_input_user - get_users - get_input_users - - -UserUpdate -========== - -Occurs whenever a user goes online, starts typing, etc. - -Full documentation for the `UserUpdate -`. - -.. currentmodule:: telethon.events.userupdate.UserUpdate.Event - -.. autosummary:: - :nosignatures: - - user - input_user - user_id - - get_user - get_input_user - - typing - uploading - recording - playing - cancel - geo - audio - round - video - contact - document - photo - last_seen - until - online - recently - within_weeks - within_months - - -CallbackQuery -============= - -Occurs whenever you sign in as a bot and a user -clicks one of the inline buttons on your messages. - -Full documentation for the `CallbackQuery -`. - -.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event - -.. autosummary:: - :nosignatures: - - id - message_id - data - chat_instance - via_inline - - respond - reply - edit - delete - answer - get_message - -InlineQuery -=========== - -Occurs whenever you sign in as a bot and a user -sends an inline query such as ``@bot query``. - -Full documentation for the `InlineQuery -`. - -.. currentmodule:: telethon.events.inlinequery.InlineQuery.Event - -.. autosummary:: - :nosignatures: - - id - text - offset - geo - builder - - answer - -Album -===== - -Occurs whenever you receive an entire album. - -Full documentation for the `Album -`. - -.. currentmodule:: telethon.events.album.Album.Event - -.. autosummary:: - :nosignatures: - - grouped_id - text - raw_text - is_reply - forward - - get_reply_message - respond - reply - forward_to - edit - delete - mark_read - pin - -Raw -=== - -Raw events are not actual events. Instead, they are the raw -:tl:`Update` object that Telegram sends. You normally shouldn't -need these. diff --git a/readthedocs/quick-references/faq.rst b/readthedocs/quick-references/faq.rst deleted file mode 100644 index 7b714f41..00000000 --- a/readthedocs/quick-references/faq.rst +++ /dev/null @@ -1,423 +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: - await client.send_message(chat, 'Hi') - except errors.FloodWaitError as e: - # e.seconds is how many seconds you have - # to wait before making the request again. - print('Flood for', e.seconds) - - -My account was deleted/limited when using the library -===================================================== - -First and foremost, **this is not a problem exclusive to Telethon. -Any third-party library is prone to cause the accounts to appear banned.** -Even official applications can make Telegram ban an account under certain -circumstances. Third-party libraries such as Telethon are a lot easier to -use, and as such, they are misused to spam, which causes Telegram to learn -certain patterns and ban suspicious activity. - -There is no point in Telethon trying to circumvent this. Even if it succeeded, -spammers would then abuse the library again, and the cycle would repeat. - -The library will only do things that you tell it to do. If you use -the library with bad intentions, Telegram will hopefully ban you. - -However, you may also be part of a limited country, such as Iran or Russia. -In that case, we have bad news for you. Telegram is much more likely to ban -these numbers, as they are often used to spam other accounts, likely through -the use of libraries like this one. The best advice we can give you is to not -abuse the API, like calling many requests really quickly. - -We have also had reports from Kazakhstan and China, where connecting -would fail. To solve these connection problems, you should use a proxy. - -Telegram may also ban virtual (VoIP) phone numbers, -as again, they're likely to be used for spam. - -More recently (year 2023 onwards), Telegram has started putting a lot more -measures to prevent spam (with even additions such as anonymous participants -in groups or the inability to fetch group members at all). This means some -of the anti-spam measures have gotten more aggressive. - -The recommendation has usually been to use the library only on well-established -accounts (and not an account you just created), and to not perform actions that -could be seen as abuse. Telegram decides what those actions are, and they're -free to change how they operate at any time. - -If you want to check if your account has been limited, -simply send a private message to `@SpamBot`_ through Telegram itself. -You should notice this by getting errors like ``PeerFloodError``, -which means you're limited, for instance, -when sending a message to some accounts but not others. - -For more discussion, please see `issue 297`_. - - -How can I use a proxy? -====================== - -This was one of the first things described in :ref:`signing-in`. - - -How do I access a field? -======================== - -This is basic Python knowledge. You should use the dot operator: - -.. code-block:: python - - me = await client.get_me() - print(me.username) - # ^ we used the dot operator to access the username attribute - - result = await client(functions.photos.GetUserPhotosRequest( - user_id='me', - offset=0, - max_id=0, - limit=100 - )) - - # Working with list is also pretty basic - print(result.photos[0].sizes[-1].type) - # ^ ^ ^ ^ ^ - # | | | | \ type - # | | | \ last size - # | | \ list of sizes - # access | \ first photo from the list - # the... \ list of photos - # - # To print all, you could do (or mix-and-match): - for photo in result.photos: - for size in photo.sizes: - print(size.type) - - -AttributeError: 'coroutine' object has no attribute 'id' -======================================================== - -You either forgot to: - -.. code-block:: python - - import telethon.sync - # ^^^^^ import sync - -Or: - -.. code-block:: python - - async def handler(event): - me = await client.get_me() - # ^^^^^ note the await - print(me.username) - - -sqlite3.OperationalError: database is locked -============================================ - -An older process is still running and is using the same ``'session'`` file. - -This error occurs when **two or more clients use the same session**, -that is, when you write the same session name to be used in the client: - -* You have an older process using the same session file. -* You have two different scripts running (interactive sessions count too). -* You have two clients in the same script running at the same time. - -The solution is, if you need two clients, use two sessions. If the -problem persists and you're on Linux, you can use ``fuser my.session`` -to find out the process locking the file. As a last resort, you can -reboot your system. - -If you really dislike SQLite, use a different session storage. There -is an entire section covering that at :ref:`sessions`. - - -event.chat or event.sender is None -================================== - -Telegram doesn't always send this information in order to save bandwidth. -If you need the information, you should fetch it yourself, since the library -won't do unnecessary work unless you need to: - -.. code-block:: python - - async def handler(event): - chat = await event.get_chat() - sender = await event.get_sender() - - -File download is slow or sending files takes too long -===================================================== - -The communication with Telegram is encrypted. Encryption requires a lot of -math, and doing it in pure Python is very slow. ``cryptg`` is a library which -containns the encryption functions used by Telethon. If it is installed (via -``pip install cryptg``), it will automatically be used and should provide -a considerable speed boost. You can know whether it's used by configuring -``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``. - -Note that the library does *not* download or upload files in parallel, which -can also help with the speed of downloading or uploading a single file. There -are snippets online implementing that. The reason why this is not built-in -is because the limiting factor in the long run are ``FloodWaitError``, and -using parallel download or uploads only makes them occur sooner. - - -What does "Server sent a very new message with ID" mean? -======================================================== - -You may also see this error as "Server sent a very old message with ID". - -This is a security feature from Telethon that cannot be disabled and is -meant to protect you against replay attacks. - -When this message is incorrectly reported as a "bug", -the most common patterns seem to be: - -* Your system time is incorrect. -* The proxy you're using may be interfering somehow. -* The Telethon session is being used or has been used from somewhere else. - Make sure that you created the session from Telethon, and are not using the - same session anywhere else. If you need to use the same account from - multiple places, login and use a different session for each place you need. - - -What does "Server replied with a wrong session ID" mean? -======================================================== - -This is a security feature from Telethon that cannot be disabled and is -meant to protect you against unwanted session reuse. - -When this message is reported as a "bug", the most common patterns seem to be: - -* The proxy you're using may be interfering somehow. -* The Telethon session is being used or has been used from somewhere else. - Make sure that you created the session from Telethon, and are not using the - same session anywhere else. If you need to use the same account from - multiple places, login and use a different session for each place you need. -* You may be using multiple connections to the Telegram server, which seems - to confuse Telegram. - -Most of the time it should be safe to ignore this warning. If the library -still doesn't behave correctly, make sure to check if any of the above bullet -points applies in your case and try to work around it. - -If the issue persists and there is a way to reliably reproduce this error, -please add a comment with any additional details you can provide to -`issue 3759`_, and perhaps some additional investigation can be done -(but it's unlikely, as Telegram *is* sending unexpected data). - - -What does "Could not find a matching Constructor ID for the TLObject" mean? -=========================================================================== - -Telegram uses "layers", which you can think of as "versions" of the API they -offer. When Telethon reads responses that the Telegram servers send, these -need to be deserialized (into what Telethon calls "TLObjects"). - -Every Telethon version understands a single Telegram layer. When Telethon -connects to Telegram, both agree on the layer to use. If the layers don't -match, Telegram may send certain objects which Telethon no longer understands. - -When this message is reported as a "bug", the most common patterns seem to be -that he Telethon session is being used or has been used from somewhere else. -Make sure that you created the session from Telethon, and are not using the -same session anywhere else. If you need to use the same account from -multiple places, login and use a different session for each place you need. - - -What does "Task was destroyed but it is pending" mean? -====================================================== - -Your script likely finished abruptly, the ``asyncio`` event loop got -destroyed, and the library did not get a chance to properly close the -connection and close the session. - -Make sure you're either using the context manager for the client or always -call ``await client.disconnect()`` (by e.g. using a ``try/finally``). - - -What does "The asyncio event loop must not change after connection" mean? -========================================================================= - -Telethon uses ``asyncio``, and makes use of things like tasks and queues -internally to manage the connection to the server and match responses to the -requests you make. Most of them are initialized after the client is connected. - -For example, if the library expects a result to a request made in loop A, but -you attempt to get that result in loop B, you will very likely find a deadlock. -To avoid a deadlock, the library checks to make sure the loop in use is the -same as the one used to initialize everything, and if not, it throws an error. - -The most common cause is ``asyncio.run``, since it creates a new event loop. -If you ``asyncio.run`` a function to create the client and set it up, and then -you ``asyncio.run`` another function to do work, things won't work, so the -library throws an error early to let you know something is wrong. - -Instead, it's often a good idea to have a single ``async def main`` and simply -``asyncio.run()`` it and do all the work there. From it, you're also able to -call other ``async def`` without having to touch ``asyncio.run`` again: - -.. code-block:: python - - # It's fine to create the client outside as long as you don't connect - client = TelegramClient(...) - - async def main(): - # Now the client will connect, so the loop must not change from now on. - # But as long as you do all the work inside main, including calling - # other async functions, things will work. - async with client: - .... - - if __name__ == '__main__': - asyncio.run(main()) - -Be sure to read the ``asyncio`` documentation if you want a better -understanding of event loop, tasks, and what functions you can use. - - -What does "bases ChatGetter" mean? -================================== - -In Python, classes can base others. This is called `inheritance -`_. What it means is that -"if a class bases another, you can use the other's methods too". - -For example, `Message ` *bases* -`ChatGetter `. In turn, -`ChatGetter ` defines -things like `obj.chat_id `. - -So if you have a message, you can access that too: - -.. code-block:: python - - # ChatGetter has a chat_id property, and Message bases ChatGetter. - # Thus you can use ChatGetter properties and methods from Message - print(message.chat_id) - - -Telegram has a lot to offer, and inheritance helps the library reduce -boilerplate, so it's important to know this concept. For newcomers, -this may be a problem, so we explain what it means here in the FAQ. - -Can I send files by ID? -======================= - -When people talk about IDs, they often refer to one of two things: -the integer ID inside media, and a random-looking long string. - -You cannot use the integer ID to send media. Generally speaking, sending media -requires a combination of ID, ``access_hash`` and ``file_reference``. -The first two are integers, while the last one is a random ``bytes`` sequence. - -* The integer ``id`` will always be the same for every account, so every user - or bot looking at a particular media file, will see a consistent ID. -* The ``access_hash`` will always be the same for a given account, but - different accounts will each see their own, different ``access_hash``. - This makes it impossible to get media object from one account and use it in - another. The other account must fetch the media object itself. -* The ``file_reference`` is random for everyone and will only work for a few - hours before it expires. It must be refetched before the media can be used - (to either resend the media or download it). - -The second type of "`file ID `_" -people refer to is a concept from the HTTP Bot API. It's a custom format which -encodes enough information to use the media. - -Telethon provides an old version of these HTTP Bot API-style file IDs via -``message.file.id``, however, this feature is no longer maintained, so it may -not work. It will be removed in future versions. Nonetheless, it is possible -to find a different Python package (or write your own) to parse these file IDs -and construct the necessary input file objects to send or download the media. - - -Can I use Flask with the library? -================================= - -Yes, if you know what you are doing. However, you will probably have a -lot of headaches to get threads and asyncio to work together. Instead, -consider using `Quart `_, an asyncio-based -alternative to `Flask `_. - -Check out `quart_login.py`_ for an example web-application based on Quart. - -Can I use Anaconda/Spyder/IPython with the library? -=================================================== - -Yes, but these interpreters run the asyncio event loop implicitly, -which interferes with the ``telethon.sync`` magic module. - -If you use them, you should **not** import ``sync``: - -.. code-block:: python - - # Change any of these...: - from telethon import TelegramClient, sync, ... - from telethon.sync import TelegramClient, ... - - # ...with this: - from telethon import TelegramClient, ... - -You are also more likely to get "sqlite3.OperationalError: database is locked" -with them. If they cause too much trouble, just write your code in a ``.py`` -file and run that, or use the normal ``python`` interpreter. - -.. _logging: https://docs.python.org/3/library/logging.html -.. _@SpamBot: https://t.me/SpamBot -.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297 -.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759 -.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy diff --git a/readthedocs/quick-references/objects-reference.rst b/readthedocs/quick-references/objects-reference.rst deleted file mode 100644 index 51ed4607..00000000 --- a/readthedocs/quick-references/objects-reference.rst +++ /dev/null @@ -1,353 +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 - mark_read - pin - download_media - get_entities_text - get_buttons - - -File -==== - -The `File ` type is a wrapper object -returned by `Message.file `, -and you can use it to easily access a document's attributes, such as -its name, bot-API style file ID, etc. - -.. currentmodule:: telethon.tl.custom.file.File - -.. autosummary:: - :nosignatures: - - id - name - ext - mime_type - width - height - size - duration - title - performer - emoji - sticker_set - - -Conversation -============ - -The `Conversation ` object -is returned by the `client.conversation() -` method to easily -send and receive responses like a normal conversation. - -It bases `ChatGetter `. - -.. currentmodule:: telethon.tl.custom.conversation.Conversation - -.. autosummary:: - :nosignatures: - - send_message - send_file - mark_read - get_response - get_reply - get_edit - wait_read - wait_event - cancel - cancel_all - - -AdminLogEvent -============= - -The `AdminLogEvent ` object -is returned by the `client.iter_admin_log() -` method to easily iterate -over past "events" (deleted messages, edits, title changes, leaving members…) - -These are all the properties you can find in it: - -.. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent - -.. autosummary:: - :nosignatures: - - id - date - user_id - action - old - new - changed_about - changed_title - changed_username - changed_photo - changed_sticker_set - changed_message - deleted_message - changed_admin - changed_restrictions - changed_invites - joined - joined_invite - left - changed_hide_history - changed_signatures - changed_pin - changed_default_banned_rights - stopped_poll - - -Button -====== - -The `Button ` class is used when you login -as a bot account to send messages with reply markup, such as inline buttons -or custom keyboards. - -These are the static methods you can use to create instances of the markup: - -.. currentmodule:: telethon.tl.custom.button.Button - -.. autosummary:: - :nosignatures: - - inline - switch_inline - url - auth - text - request_location - request_phone - request_poll - clear - force_reply - - -InlineResult -============ - -The `InlineResult ` object -is returned inside a list by the `client.inline_query() -` method to make an inline -query to a bot that supports being used in inline mode, such as -`@like `_. - -Note that the list returned is in fact a *subclass* of a list called -`InlineResults `, which, -in addition of being a list (iterator, indexed access, etc.), has extra -attributes and methods. - -These are the constants for the types, properties and methods you -can find the individual results: - -.. currentmodule:: telethon.tl.custom.inlineresult.InlineResult - -.. autosummary:: - :nosignatures: - - ARTICLE - PHOTO - GIF - VIDEO - VIDEO_GIF - AUDIO - DOCUMENT - LOCATION - VENUE - CONTACT - GAME - type - message - title - description - url - photo - document - click - download_media - - -Dialog -====== - -The `Dialog ` object is returned when -you call `client.iter_dialogs() `. - -.. currentmodule:: telethon.tl.custom.dialog.Dialog - -.. autosummary:: - :nosignatures: - - send_message - archive - delete - - -Draft -====== - -The `Draft ` object is returned when -you call `client.iter_drafts() `. - -.. currentmodule:: telethon.tl.custom.draft.Draft - -.. autosummary:: - :nosignatures: - - entity - input_entity - get_entity - get_input_entity - text - raw_text - is_empty - set_message - send - delete - - -Utils -===== - -The `telethon.utils` module has plenty of methods that make using the -library a lot easier. Only the interesting ones will be listed here. - -.. currentmodule:: telethon.utils - -.. autosummary:: - :nosignatures: - - get_display_name - get_extension - get_inner_text - get_peer_id - resolve_id - pack_bot_file_id - resolve_bot_file_id - resolve_invite_link diff --git a/readthedocs/requirements.txt b/readthedocs/requirements.txt deleted file mode 100644 index 97c7493d..00000000 --- a/readthedocs/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -telethon \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2b650ec4..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyaes -rsa diff --git a/setup.py b/setup.py deleted file mode 100755 index 0bb0eb6b..00000000 --- a/setup.py +++ /dev/null @@ -1,263 +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 os -import re -import shutil -import sys -import urllib.request -from pathlib import Path -from subprocess import run - -from setuptools import find_packages, setup - -# Needed since we're importing local files -sys.path.insert(0, os.path.dirname(__file__)) - -class TempWorkDir: - """Switches the working directory to be the one on which this file lives, - while within the 'with' block. - """ - def __init__(self, new=None): - self.original = None - self.new = new or str(Path(__file__).parent.resolve()) - - def __enter__(self): - # os.chdir does not work with Path in Python 3.5.x - self.original = str(Path('.').resolve()) - os.makedirs(self.new, exist_ok=True) - os.chdir(self.new) - return self - - def __exit__(self, *args): - os.chdir(self.original) - - -API_REF_URL = 'https://tl.telethon.dev/' - -GENERATOR_DIR = Path('telethon_generator') -LIBRARY_DIR = Path('telethon') - -ERRORS_IN = GENERATOR_DIR / 'data/errors.csv' -ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py' - -METHODS_IN = GENERATOR_DIR / 'data/methods.csv' - -# Which raw API methods are covered by *friendly* methods in the client? -FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv' - -TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')] -TLOBJECT_OUT = LIBRARY_DIR / 'tl' -IMPORT_DEPTH = 2 - -DOCS_IN_RES = GENERATOR_DIR / 'data/html' -DOCS_OUT = Path('docs') - - -def generate(which, action='gen'): - from telethon_generator.parsers import\ - parse_errors, parse_methods, parse_tl, find_layer - - from telethon_generator.generators import\ - generate_errors, generate_tlobjects, generate_docs, clean_tlobjects - - layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) - errors = list(parse_errors(ERRORS_IN)) - methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors})) - - tlobjects = list(itertools.chain(*( - parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS))) - - if not which: - which.extend(('tl', 'errors')) - - clean = action == 'clean' - action = 'Cleaning' if clean else 'Generating' - - if 'all' in which: - which.remove('all') - for x in ('tl', 'errors', 'docs'): - if x not in which: - which.append(x) - - if 'tl' in which: - which.remove('tl') - print(action, 'TLObjects...') - if clean: - clean_tlobjects(TLOBJECT_OUT) - else: - generate_tlobjects(tlobjects, layer, IMPORT_DEPTH, TLOBJECT_OUT) - - if 'errors' in which: - which.remove('errors') - print(action, 'RPCErrors...') - if clean: - if ERRORS_OUT.is_file(): - ERRORS_OUT.unlink() - else: - with ERRORS_OUT.open('w') as file: - generate_errors(errors, file) - - if 'docs' in which: - which.remove('docs') - print(action, 'documentation...') - if clean: - if DOCS_OUT.is_dir(): - shutil.rmtree(str(DOCS_OUT)) - else: - in_path = DOCS_IN_RES.resolve() - with TempWorkDir(DOCS_OUT): - generate_docs(tlobjects, methods, layer, in_path) - - if 'json' in which: - which.remove('json') - print(action, 'JSON schema...') - json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS] - if clean: - for file in json_files: - if file.is_file(): - file.unlink() - else: - def gen_json(fin, fout): - meths = [] - constructors = [] - for tl in parse_tl(fin, layer): - if tl.is_function: - meths.append(tl.to_dict()) - else: - constructors.append(tl.to_dict()) - what = {'constructors': constructors, 'methods': meths} - with open(fout, 'w') as f: - json.dump(what, f, indent=2) - - for fs in zip(TLOBJECT_IN_TLS, json_files): - gen_json(*fs) - - if which: - print( - 'The following items were not understood:', which, - '\n Consider using only "tl", "errors" and/or "docs".' - '\n Using only "clean" will clean them. "all" to act on all.' - '\n For instance "gen tl errors".' - ) - - -def main(argv): - if len(argv) >= 2 and argv[1] in ('gen', 'clean'): - generate(argv[2:], argv[1]) - - elif len(argv) >= 2 and argv[1] == 'pypi': - # Make sure tl.telethon.dev is up-to-date first - with urllib.request.urlopen(API_REF_URL) as resp: - html = resp.read() - m = re.search(br'layer\s+(\d+)', html) - if not m: - print('Failed to check that the API reference is up to date:', API_REF_URL) - return - - from telethon_generator.parsers import find_layer - layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS))) - published_layer = int(m[1]) - if published_layer != layer: - print('Published layer', published_layer, 'does not match current layer', layer, '.') - print('Make sure to update the API reference site first:', API_REF_URL) - return - - # (Re)generate the code to make sure we don't push without it - generate(['tl', 'errors']) - - # Try importing the telethon module to assert it has no errors - try: - import telethon - except: - print('Packaging for PyPi aborted, importing the module failed.') - return - - remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info'] - for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False): - # setuptools is including __pycache__ for some reason (#1605) - if root.endswith('/__pycache__'): - remove_dirs.append(root) - for x in remove_dirs: - shutil.rmtree(x, ignore_errors=True) - - run('python3 setup.py sdist', shell=True) - run('python3 setup.py bdist_wheel', shell=True) - run('twine upload dist/*', shell=True) - for x in ('build', 'dist', 'Telethon.egg-info'): - shutil.rmtree(x, ignore_errors=True) - - else: - # e.g. install from GitHub - if GENERATOR_DIR.is_dir(): - generate(['tl', 'errors']) - - # Get the long description from the README file - with open('README.rst', 'r', encoding='utf-8') as f: - long_description = f.read() - - with open('telethon/version.py', 'r', encoding='utf-8') as f: - version = re.search(r"^__version__\s*=\s*'(.*)'.*$", - f.read(), flags=re.MULTILINE).group(1) - setup( - name='Telethon', - version=version, - description="Full-featured Telegram client library for Python 3", - long_description=long_description, - - url='https://github.com/LonamiWebs/Telethon', - download_url='https://github.com/LonamiWebs/Telethon/releases', - - author='Lonami Exo', - author_email='totufals@hotmail.com', - - license='MIT', - - # See https://stackoverflow.com/a/40300957/4759433 - # -> https://www.python.org/dev/peps/pep-0345/#requires-python - # -> http://setuptools.readthedocs.io/en/latest/setuptools.html - python_requires='>=3.5', - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 5 - Production/Stable', - - 'Intended Audience :: Developers', - 'Topic :: Communications :: Chat', - - 'License :: OSI Approved :: MIT License', - - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - ], - keywords='telegram api chat client library messaging mtproto', - packages=find_packages(exclude=[ - 'telethon_*', 'tests*' - ]), - install_requires=['pyaes', 'rsa'], - extras_require={ - 'cryptg': ['cryptg'] - } - ) - - -if __name__ == '__main__': - with TempWorkDir(): - main(sys.argv) diff --git a/telethon/__init__.py b/telethon/__init__.py deleted file mode 100644 index c3334d7c..00000000 --- a/telethon/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from .client.telegramclient import TelegramClient -from .network import connection -from .tl.custom import Button -from .tl import patched as _ # import for its side-effects -from . import version, events, utils, errors, types, functions, custom - -__version__ = version.__version__ - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/_updates/__init__.py b/telethon/_updates/__init__.py deleted file mode 100644 index f9cc2492..00000000 --- a/telethon/_updates/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .entitycache import EntityCache -from .messagebox import MessageBox, GapError, PrematureEndReason -from .session import SessionState, ChannelState, Entity, EntityType diff --git a/telethon/_updates/entitycache.py b/telethon/_updates/entitycache.py deleted file mode 100644 index fd56c3e1..00000000 --- a/telethon/_updates/entitycache.py +++ /dev/null @@ -1,62 +0,0 @@ -from .session import EntityType, Entity - - -_sentinel = object() - - -class EntityCache: - def __init__( - self, - hash_map: dict = _sentinel, - self_id: int = None, - self_bot: bool = None - ): - self.hash_map = {} if hash_map is _sentinel else hash_map - self.self_id = self_id - self.self_bot = self_bot - - def set_self_user(self, id, bot, hash): - self.self_id = id - self.self_bot = bot - if hash: - self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER) - - def get(self, id): - try: - hash, ty = self.hash_map[id] - return Entity(ty, id, hash) - except KeyError: - return None - - def extend(self, users, chats): - # See https://core.telegram.org/api/min for "issues" with "min constructors". - self.hash_map.update( - (u.id, ( - u.access_hash, - EntityType.BOT if u.bot else EntityType.USER, - )) - for u in users - if getattr(u, 'access_hash', None) and not u.min - ) - self.hash_map.update( - (c.id, ( - c.access_hash, - EntityType.MEGAGROUP if c.megagroup else ( - EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL - ), - )) - for c in chats - if getattr(c, 'access_hash', None) and not getattr(c, 'min', None) - ) - - def get_all_entities(self): - return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()] - - def put(self, entity): - self.hash_map[entity.id] = (entity.hash, entity.ty) - - def retain(self, filter): - self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)} - - def __len__(self): - return len(self.hash_map) diff --git a/telethon/_updates/messagebox.py b/telethon/_updates/messagebox.py deleted file mode 100644 index 2b4e75ff..00000000 --- a/telethon/_updates/messagebox.py +++ /dev/null @@ -1,810 +0,0 @@ -""" -This module deals with correct handling of updates, including gaps, and knowing when the code -should "get difference" (the set of updates that the client should know by now minus the set -of updates that it actually knows). - -Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point"). -At any given time, the message box may be either getting difference for them (entry is in -[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be -found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is -on its happy path. - -Gaps are cleared when they are either resolved on their own (by waiting for a short time) -or because we got the difference for the corresponding entry. - -While there are entries for which their difference must be fetched, -[`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time -to get the difference. -""" -import asyncio -import datetime -import time -import logging -from enum import Enum -from .session import SessionState, ChannelState -from ..tl import types as tl, functions as fn -from ..helpers import get_running_loop - - -# Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too. -NO_SEQ = 0 - -# See https://core.telegram.org/method/updates.getChannelDifference. -BOT_CHANNEL_DIFF_LIMIT = 100000 -USER_CHANNEL_DIFF_LIMIT = 100 - -# > It may be useful to wait up to 0.5 seconds -POSSIBLE_GAP_TIMEOUT = 0.5 - -# After how long without updates the client will "timeout". -# -# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the -# updates that arrive in the meantime. After all updates are fetched when this happens, the -# client will resume normal operation, and the timeout will reset. -# -# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates). -NO_UPDATES_TIMEOUT = 15 * 60 - -# Entry "enum". -# Account-wide `pts` includes private conversations (one-to-one) and small group chats. -ENTRY_ACCOUNT = object() -# Account-wide `qts` includes only "secret" one-to-one chats. -ENTRY_SECRET = object() -# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels. - -# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET. -# We don't define a name for this as libraries shouldn't do that though. -LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2 - -_sentinel = object() - -def next_updates_deadline(): - return get_running_loop().time() + NO_UPDATES_TIMEOUT - - -class GapError(ValueError): - def __repr__(self): - return 'GapError()' - - -class PrematureEndReason(Enum): - TEMPORARY_SERVER_ISSUES = 'tmp' - BANNED = 'ban' - - -# Represents the information needed to correctly handle a specific `tl::enums::Update`. -class PtsInfo: - __slots__ = ('pts', 'pts_count', 'entry') - - def __init__( - self, - pts: int, - pts_count: int, - entry: object - ): - self.pts = pts - self.pts_count = pts_count - self.entry = entry - - @classmethod - def from_update(cls, update): - pts = getattr(update, 'pts', None) - if pts: - pts_count = getattr(update, 'pts_count', None) or 0 - try: - entry = update.message.peer_id.channel_id - except AttributeError: - entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT - return cls(pts=pts, pts_count=pts_count, entry=entry) - - qts = getattr(update, 'qts', None) - if qts: - pts_count = 1 if isinstance(update, tl.UpdateNewEncryptedMessage) else 0 - return cls(pts=qts, pts_count=pts_count, entry=ENTRY_SECRET) - - return None - - def __repr__(self): - if self.entry is ENTRY_ACCOUNT: - entry = 'ENTRY_ACCOUNT' - elif self.entry is ENTRY_SECRET: - entry = 'ENTRY_SECRET' - else: - entry = self.entry - return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={entry})' - - -# The state of a particular entry in the message box. -class State: - __slots__ = ('pts', 'deadline') - - def __init__( - self, - # Current local persistent timestamp. - pts: int, - # Next instant when we would get the update difference if no updates arrived before then. - deadline: float - ): - self.pts = pts - self.deadline = deadline - - def __repr__(self): - return f'State(pts={self.pts}, deadline={self.deadline})' - - -# > ### Recovering gaps -# > […] Manually obtaining updates is also required in the following situations: -# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above). -# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update -# > arrives, that fills the gap. -# -# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because -# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone). -class PossibleGap: - __slots__ = ('deadline', 'updates') - - def __init__( - self, - deadline: float, - # Pending updates (those with a larger PTS, producing the gap which may later be filled). - updates: list # of updates - ): - self.deadline = deadline - self.updates = updates - - def __repr__(self): - return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})' - - -# Represents a "message box" (event `pts` for a specific entry). -# -# See https://core.telegram.org/api/updates#message-related-event-sequences. -class MessageBox: - __slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for', 'reset_deadlines_for') - - def __init__( - self, - log, - # Map each entry to their current state. - map: dict = _sentinel, # entry -> state - - # Additional fields beyond PTS needed by `ENTRY_ACCOUNT`. - date: datetime.datetime = datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc), - seq: int = NO_SEQ, - - # Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline). - next_deadline: object = None, # entry - - # Which entries have a gap and may soon trigger a need to get difference. - # - # If a gap is found, stores the required information to resolve it (when should it timeout and what updates - # should be held in case the gap is resolved on its own). - # - # Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have - # a gap in them). - possible_gaps: dict = _sentinel, # entry -> possiblegap - - # For which entries are we currently getting difference. - getting_diff_for: set = _sentinel, # entry - - # Temporarily stores which entries should have their update deadline reset. - # Stored in the message box in order to reuse the allocation. - reset_deadlines_for: set = _sentinel # entry - ): - self._log = log - self.map = {} if map is _sentinel else map - self.date = date - self.seq = seq - self.next_deadline = next_deadline - self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps - self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for - self.reset_deadlines_for = set() if reset_deadlines_for is _sentinel else reset_deadlines_for - - if __debug__: - # Need this to tell them apart when printing the repr of the state map. - # Could be done once at the global level, but that makes configuring logging - # more annoying because it would need to be done before importing telethon. - self._trace('ENTRY_ACCOUNT = %r; ENTRY_SECRET = %r', ENTRY_ACCOUNT, ENTRY_SECRET) - self._trace('Created new MessageBox with map = %r, date = %r, seq = %r', self.map, self.date, self.seq) - - def _trace(self, msg, *args, **kwargs): - # Calls to trace can't really be removed beforehand without some dark magic. - # So every call to trace is prefixed with `if __debug__`` instead, to remove - # it when using `python -O`. Probably unnecessary, but it's nice to avoid - # paying the cost for something that is not used. - self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs) - - # region Creation, querying, and setting base state. - - def load(self, session_state, channel_states): - """ - Create a [`MessageBox`] from a previously known update state. - """ - if __debug__: - self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states) - - deadline = next_updates_deadline() - - self.map.clear() - if session_state.pts != NO_SEQ: - self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline) - if session_state.qts != NO_SEQ: - self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline) - self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states) - - self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc) - self.seq = session_state.seq - self.next_deadline = ENTRY_ACCOUNT - - def session_state(self): - """ - Return the current state. - - This should be used for persisting the state. - """ - return dict( - pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ, - qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, - date=self.date, - seq=self.seq, - ), {id: state.pts for id, state in self.map.items() if isinstance(id, int)} - - def is_empty(self) -> bool: - """ - Return true if the message box is empty and has no state yet. - """ - return ENTRY_ACCOUNT not in self.map - - def check_deadlines(self): - """ - Return the next deadline when receiving updates should timeout. - - If a deadline expired, the corresponding entries will be marked as needing to get its difference. - While there are entries pending of getting their difference, this method returns the current instant. - """ - now = get_running_loop().time() - - if self.getting_diff_for: - return now - - deadline = next_updates_deadline() - - # Most of the time there will be zero or one gap in flight so finding the minimum is cheap. - if self.possible_gaps: - deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values())) - elif self.next_deadline in self.map: - deadline = min(deadline, self.map[self.next_deadline].deadline) - - # asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that - # we find the same number again on repeated calls. Without the "or equal" part we would log the - # timeout for updates several times (it also makes sense to get difference if now is the deadline). - if now >= deadline: - # Check all expired entries and add them to the list that needs getting difference. - self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now > gap.deadline) - self.getting_diff_for.update(entry for entry, state in self.map.items() if now > state.deadline) - - if __debug__: - self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for) - - # When extending `getting_diff_for`, it's important to have the moral equivalent of - # `begin_get_diff` (that is, clear possible gaps if we're now getting difference). - for entry in self.getting_diff_for: - self.possible_gaps.pop(entry, None) - - return deadline - - # Reset the deadline for the periods without updates for a given entry. - # - # It also updates the next deadline time to reflect the new closest deadline. - def reset_deadline(self, entry, deadline): - if entry not in self.map: - raise RuntimeError('Called reset_deadline on an entry for which we do not have state') - self.map[entry].deadline = deadline - - if self.next_deadline == entry: - # If the updated deadline was the closest one, recalculate the new minimum. - self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0] - elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline: - # If the updated deadline is smaller than the next deadline, change the next deadline to be the new one. - self.next_deadline = entry - # else an unrelated deadline was updated, so the closest one remains unchanged. - - # Convenience to reset a channel's deadline, with optional timeout. - def reset_channel_deadline(self, channel_id, timeout): - self.reset_deadline(channel_id, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT)) - - # Reset all the deadlines in `reset_deadlines_for` and then empty the set. - def apply_deadlines_reset(self): - next_deadline = next_updates_deadline() - - reset_deadlines_for = self.reset_deadlines_for - self.reset_deadlines_for = set() # "move" the set to avoid self.reset_deadline() from touching it during iter - - for entry in reset_deadlines_for: - self.reset_deadline(entry, next_deadline) - - reset_deadlines_for.clear() # reuse allocation, the other empty set was a temporary dummy value - self.reset_deadlines_for = reset_deadlines_for - - # Sets the update state. - # - # Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable - # updates will be fetched. - def set_state(self, state, reset=True): - if __debug__: - self._trace('Setting state %s', state) - - deadline = next_updates_deadline() - - if state.pts != NO_SEQ or not reset: - self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline) - else: - self.map.pop(ENTRY_ACCOUNT, None) - - # Telegram seems to use the `qts` for bot accounts, but while applying difference, - # it might be reset back to 0. See issue #3873 for more details. - # - # During login, a value of zero would mean the `pts` is unknown, - # so the map shouldn't contain that entry. - # But while applying difference, if the value is zero, it (probably) - # truly means that's what should be used (hence the `reset` flag). - if state.qts != NO_SEQ or not reset: - self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline) - else: - self.map.pop(ENTRY_SECRET, None) - - self.date = state.date - self.seq = state.seq - - # Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs. - # - # The update state will only be updated if no entry was known previously. - def try_set_channel_state(self, id, pts): - if __debug__: - self._trace('Trying to set channel state for %r: %r', id, pts) - - if id not in self.map: - self.map[id] = State(pts=pts, deadline=next_updates_deadline()) - - # Try to begin getting difference for the given entry. - # Fails if the entry does not have a previously-known state that can be used to get its difference. - # - # Clears any previous gaps. - def try_begin_get_diff(self, entry): - if entry not in self.map: - # Won't actually be able to get difference for this entry if we don't have a pts to start off from. - if entry in self.possible_gaps: - raise RuntimeError('Should not have a possible_gap for an entry not in the state map') - - # TODO it would be useful to log when this happens - return - - self.getting_diff_for.add(entry) - self.possible_gaps.pop(entry, None) - - # Finish getting difference for the given entry. - # - # It also resets the deadline. - def end_get_diff(self, entry): - try: - self.getting_diff_for.remove(entry) - except KeyError: - raise RuntimeError('Called end_get_diff on an entry which was not getting diff for') - - self.reset_deadline(entry, next_updates_deadline()) - assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference" - - # endregion Creation, querying, and setting base state. - - # region "Normal" updates flow (processing and detection of gaps). - - # Process an update and return what should be done with it. - # - # Updates corresponding to entries for which their difference is currently being fetched - # will be ignored. While according to the [updates' documentation]: - # - # > Implementations [have] to postpone updates received via the socket while - # > filling gaps in the event and `Update` sequences, as well as avoid filling - # > gaps in the same sequence. - # - # In practice, these updates should have also been retrieved through getting difference. - # - # [updates documentation] https://core.telegram.org/api/updates - def process_updates( - self, - updates, - chat_hashes, - result, # out list of updates; returns list of user, chat, or raise if gap - ): - if __debug__: - self._trace('Processing updates %s', updates) - - date = getattr(updates, 'date', None) - if date is None: - # updatesTooLong is the only one with no date (we treat it as a gap) - self.try_begin_get_diff(ENTRY_ACCOUNT) - raise GapError - - # v1 has never sent updates produced by the client itself to the handlers. - # However proper update handling requires those to be processed. - # This is an ugly workaround for that. - self_outgoing = getattr(updates, '_self_outgoing', False) - real_result = result - result = [] - - seq = getattr(updates, 'seq', None) or NO_SEQ - seq_start = getattr(updates, 'seq_start', None) or seq - users = getattr(updates, 'users', None) or [] - chats = getattr(updates, 'chats', None) or [] - - # updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field - updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates] - - for u in updates: - u._self_outgoing = self_outgoing - - # > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors - # > there is no need to check `seq` or change a local state. - if seq_start != NO_SEQ: - if self.seq + 1 > seq_start: - # Skipping updates that were already handled - return (users, chats) - elif self.seq + 1 < seq_start: - # Gap detected - self.try_begin_get_diff(ENTRY_ACCOUNT) - raise GapError - # else apply - - self.date = date - if seq != NO_SEQ: - self.seq = seq - - def _sort_gaps(update): - pts = PtsInfo.from_update(update) - return pts.pts - pts.pts_count if pts else 0 - - # Telegram can send updates out of order (e.g. ReadChannelInbox first - # and then NewChannelMessage, both with the same pts, but the count is - # 0 and 1 respectively). - # - # We can't know beforehand if this would cause issues (i.e. if any of - # the updates is the first one we get to know about a specific channel) - # (other than doing a pre-scan to check if any has info about an entry - # we lack), so instead we sort preemptively. As a bonus there's less - # likelyhood of "possible gaps" by doing this. - # TODO give this more thought, perhaps possible gaps can't happen at all - # (not ones which would be resolved by sorting anyway) - result.extend(filter(None, ( - self.apply_pts_info(u, reset_deadline=True) for u in sorted(updates, key=_sort_gaps)))) - - self.apply_deadlines_reset() - - if self.possible_gaps: - # For each update in possible gaps, see if the gap has been resolved already. - for key in list(self.possible_gaps.keys()): - self.possible_gaps[key].updates.sort(key=_sort_gaps) - - for _ in range(len(self.possible_gaps[key].updates)): - update = self.possible_gaps[key].updates.pop(0) - - # If this fails to apply, it will get re-inserted at the end. - # All should fail, so the order will be preserved (it would've cycled once). - update = self.apply_pts_info(update, reset_deadline=False) - if update: - result.append(update) - - # Clear now-empty gaps. - self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates} - - real_result.extend(u for u in result if not u._self_outgoing) - - return (users, chats) - - # Tries to apply the input update if its `PtsInfo` follows the correct order. - # - # If the update can be applied, it is returned; otherwise, the update is stored in a - # possible gap (unless it was already handled or would be handled through getting - # difference) and `None` is returned. - def apply_pts_info( - self, - update, - *, - reset_deadline, - ): - # This update means we need to call getChannelDifference to get the updates from the channel - if isinstance(update, tl.UpdateChannelTooLong): - self.try_begin_get_diff(update.channel_id) - return None - - pts = PtsInfo.from_update(update) - if not pts: - # No pts means that the update can be applied in any order. - return update - - # As soon as we receive an update of any form related to messages (has `PtsInfo`), - # the "no updates" period for that entry is reset. - # - # Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry. - # - # By the time this method returns, self.map will have an entry for which we can reset its deadline. - if reset_deadline: - self.reset_deadlines_for.add(pts.entry) - - if pts.entry in self.getting_diff_for: - # Note: early returning here also prevents gap from being inserted (which they should - # not be while getting difference). - return None - - if pts.entry in self.map: - local_pts = self.map[pts.entry].pts - if local_pts + pts.pts_count > pts.pts: - # Ignore - return None - elif local_pts + pts.pts_count < pts.pts: - # Possible gap - # TODO store chats too? - if pts.entry not in self.possible_gaps: - self.possible_gaps[pts.entry] = PossibleGap( - deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT, - updates=[] - ) - - self.possible_gaps[pts.entry].updates.append(update) - return None - else: - # Apply - pass - - # In a channel, we may immediately receive: - # * ReadChannelInbox (pts = X, pts_count = 0) - # * NewChannelMessage (pts = X, pts_count = 1) - # - # Notice how both `pts` are the same. If they were to be applied out of order, the first - # one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be - # less than `remote_pts`. So there is no risk by setting the `local_pts` to match the - # `remote_pts` here of missing the new message. - # - # The message would however be lost if we initialized the pts with the first one, since - # the second one would appear "already handled". To prevent this we set the pts to be - # one less when the count is 0 (which might be wrong and trigger a gap later on, but is - # unlikely). This will prevent us from losing updates in the unlikely scenario where these - # two updates arrive in different packets (and therefore couldn't be sorted beforehand). - if pts.entry in self.map: - self.map[pts.entry].pts = pts.pts - else: - # When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox` - # with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`. - # Note how the `pts` for the message is 2 and not 1 unlike the case described before! - # This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY), - # which forces the first update to be 1. But if we got difference with 1 and the second update - # also used 1, we would miss it, so Telegram probably uses 2 to work around that. - self.map[pts.entry] = State( - pts=(pts.pts - (0 if pts.pts_count else 1)) or 1, - deadline=next_updates_deadline() - ) - - return update - - # endregion "Normal" updates flow (processing and detection of gaps). - - # region Getting and applying account difference. - - # Return the request that needs to be made to get the difference, if any. - def get_difference(self): - for entry in (ENTRY_ACCOUNT, ENTRY_SECRET): - if entry in self.getting_diff_for: - if entry not in self.map: - raise RuntimeError('Should not try to get difference for an entry without known state') - - gd = fn.updates.GetDifferenceRequest( - pts=self.map[ENTRY_ACCOUNT].pts, - pts_total_limit=None, - date=self.date, - qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ, - ) - if __debug__: - self._trace('Requesting account difference %s', gd) - return gd - - return None - - # Similar to [`MessageBox::process_updates`], but using the result from getting difference. - def apply_difference( - self, - diff, - chat_hashes, - ): - if __debug__: - self._trace('Applying account difference %s', diff) - - finish = None - result = None - - if isinstance(diff, tl.updates.DifferenceEmpty): - finish = True - self.date = diff.date - self.seq = diff.seq - result = [], [], [] - elif isinstance(diff, tl.updates.Difference): - finish = True - chat_hashes.extend(diff.users, diff.chats) - result = self.apply_difference_type(diff, chat_hashes) - elif isinstance(diff, tl.updates.DifferenceSlice): - finish = False - chat_hashes.extend(diff.users, diff.chats) - result = self.apply_difference_type(diff, chat_hashes) - elif isinstance(diff, tl.updates.DifferenceTooLong): - finish = True - self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends - result = [], [], [] - - if finish: - account = ENTRY_ACCOUNT in self.getting_diff_for - secret = ENTRY_SECRET in self.getting_diff_for - - if not account and not secret: - raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active') - - # Both may be active if both expired at the same time. - if account: - self.end_get_diff(ENTRY_ACCOUNT) - if secret: - self.end_get_diff(ENTRY_SECRET) - - return result - - def apply_difference_type( - self, - diff, - chat_hashes, - ): - state = getattr(diff, 'intermediate_state', None) or diff.state - self.set_state(state, reset=False) - - # diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage. - # We need to process those as if they were socket updates to discard any we have already handled. - updates = [] - self.process_updates(tl.Updates( - updates=diff.other_updates, - users=diff.users, - chats=diff.chats, - date=1, # anything not-None - seq=NO_SEQ, # this way date is not used - ), chat_hashes, updates) - - updates.extend(tl.UpdateNewMessage( - message=m, - pts=NO_SEQ, - pts_count=NO_SEQ, - ) for m in diff.new_messages) - updates.extend(tl.UpdateNewEncryptedMessage( - message=m, - qts=NO_SEQ, - ) for m in diff.new_encrypted_messages) - - return updates, diff.users, diff.chats - - def end_difference(self): - if __debug__: - self._trace('Ending account difference') - - account = ENTRY_ACCOUNT in self.getting_diff_for - secret = ENTRY_SECRET in self.getting_diff_for - - if not account and not secret: - raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active') - - # Both may be active if both expired at the same time. - if account: - self.end_get_diff(ENTRY_ACCOUNT) - if secret: - self.end_get_diff(ENTRY_SECRET) - - # endregion Getting and applying account difference. - - # region Getting and applying channel difference. - - # Return the request that needs to be made to get a channel's difference, if any. - def get_channel_difference( - self, - chat_hashes, - ): - entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None) - if not entry: - return None - - packed = chat_hashes.get(entry) - if not packed: - # Cannot get channel difference as we're missing its hash - # TODO we should probably log this - self.end_get_diff(entry) - # Remove the outdated `pts` entry from the map so that the next update can correct - # it. Otherwise, it will spam that the access hash is missing. - self.map.pop(entry, None) - return None - - state = self.map.get(entry) - if not state: - raise RuntimeError('Should not try to get difference for an entry without known state') - - gd = fn.updates.GetChannelDifferenceRequest( - force=False, - channel=tl.InputChannel(packed.id, packed.hash), - filter=tl.ChannelMessagesFilterEmpty(), - pts=state.pts, - limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT - ) - if __debug__: - self._trace('Requesting channel difference %s', gd) - return gd - - # Similar to [`MessageBox::process_updates`], but using the result from getting difference. - def apply_channel_difference( - self, - request, - diff, - chat_hashes, - ): - entry = request.channel.channel_id - if __debug__: - self._trace('Applying channel difference for %r: %s', entry, diff) - - self.possible_gaps.pop(entry, None) - - if isinstance(diff, tl.updates.ChannelDifferenceEmpty): - assert diff.final - self.end_get_diff(entry) - self.map[entry].pts = diff.pts - return [], [], [] - elif isinstance(diff, tl.updates.ChannelDifferenceTooLong): - assert diff.final - self.map[entry].pts = diff.dialog.pts - chat_hashes.extend(diff.users, diff.chats) - self.reset_channel_deadline(entry, diff.timeout) - # This `diff` has the "latest messages and corresponding chats", but it would - # be strange to give the user only partial changes of these when they would - # expect all updates to be fetched. Instead, nothing is returned. - return [], [], [] - elif isinstance(diff, tl.updates.ChannelDifference): - if diff.final: - self.end_get_diff(entry) - - self.map[entry].pts = diff.pts - chat_hashes.extend(diff.users, diff.chats) - - updates = [] - self.process_updates(tl.Updates( - updates=diff.other_updates, - users=diff.users, - chats=diff.chats, - date=1, # anything not-None - seq=NO_SEQ, # this way date is not used - ), chat_hashes, updates) - - updates.extend(tl.UpdateNewChannelMessage( - message=m, - pts=NO_SEQ, - pts_count=NO_SEQ, - ) for m in diff.new_messages) - self.reset_channel_deadline(entry, None) - - return updates, diff.users, diff.chats - - def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes): - entry = request.channel.channel_id - if __debug__: - self._trace('Ending channel difference for %r because %s', entry, reason) - - if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES: - # Temporary issues. End getting difference without updating the pts so we can retry later. - self.possible_gaps.pop(entry, None) - self.end_get_diff(entry) - elif reason == PrematureEndReason.BANNED: - # Banned in the channel. Forget its state since we can no longer fetch updates from it. - self.possible_gaps.pop(entry, None) - self.end_get_diff(entry) - del self.map[entry] - else: - raise RuntimeError('Unknown reason to end channel difference') - - # endregion Getting and applying channel difference. diff --git a/telethon/_updates/session.py b/telethon/_updates/session.py deleted file mode 100644 index 5025dd38..00000000 --- a/telethon/_updates/session.py +++ /dev/null @@ -1,195 +0,0 @@ -from typing import Optional, Tuple -from enum import IntEnum -from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel - - -class SessionState: - """ - Stores the information needed to fetch updates and about the current user. - - * user_id: 64-bit number representing the user identifier. - * dc_id: 32-bit number relating to the datacenter identifier where the user is. - * bot: is the logged-in user a bot? - * pts: 64-bit number holding the state needed to fetch updates. - * qts: alternative 64-bit number holding the state needed to fetch updates. - * date: 64-bit number holding the date needed to fetch updates. - * seq: 64-bit-number holding the sequence number needed to fetch updates. - * takeout_id: 64-bit-number holding the identifier of the current takeout session. - - Note that some of the numbers will only use 32 out of the 64 available bits. - However, for future-proofing reasons, we recommend you pretend they are 64-bit long. - """ - __slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id') - - def __init__( - self, - user_id: int, - dc_id: int, - bot: bool, - pts: int, - qts: int, - date: int, - seq: int, - takeout_id: Optional[int] - ): - self.user_id = user_id - self.dc_id = dc_id - self.bot = bot - self.pts = pts - self.qts = qts - self.date = date - self.seq = seq - self.takeout_id = takeout_id - - def __repr__(self): - return repr({k: getattr(self, k) for k in self.__slots__}) - - -class ChannelState: - """ - Stores the information needed to fetch updates from a channel. - - * channel_id: 64-bit number representing the channel identifier. - * pts: 64-bit number holding the state needed to fetch updates. - """ - __slots__ = ('channel_id', 'pts') - - def __init__( - self, - channel_id: int, - pts: int, - ): - self.channel_id = channel_id - self.pts = pts - - def __repr__(self): - return repr({k: getattr(self, k) for k in self.__slots__}) - - -class EntityType(IntEnum): - """ - You can rely on the type value to be equal to the ASCII character one of: - - * 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``. - * 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``. - * 'G' (71): this entity belongs to a small group :tl:`Chat`. - * 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`. - * 'M' (77): this entity belongs to a megagroup :tl:`Channel`. - * 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`. - """ - USER = ord('U') - BOT = ord('B') - GROUP = ord('G') - CHANNEL = ord('C') - MEGAGROUP = ord('M') - GIGAGROUP = ord('E') - - def canonical(self): - """ - Return the canonical version of this type. - """ - return _canon_entity_types[self] - - -_canon_entity_types = { - EntityType.USER: EntityType.USER, - EntityType.BOT: EntityType.USER, - EntityType.GROUP: EntityType.GROUP, - EntityType.CHANNEL: EntityType.CHANNEL, - EntityType.MEGAGROUP: EntityType.CHANNEL, - EntityType.GIGAGROUP: EntityType.CHANNEL, -} - - -class Entity: - """ - Stores the information needed to use a certain user, chat or channel with the API. - - * ty: 8-bit number indicating the type of the entity (of type `EntityType`). - * id: 64-bit number uniquely identifying the entity among those of the same type. - * hash: 64-bit signed number needed to use this entity with the API. - - The string representation of this class is considered to be stable, for as long as - Telegram doesn't need to add more fields to the entities. It can also be converted - to bytes with ``bytes(entity)``, for a more compact representation. - """ - __slots__ = ('ty', 'id', 'hash') - - def __init__( - self, - ty: EntityType, - id: int, - hash: int - ): - self.ty = ty - self.id = id - self.hash = hash - - @property - def is_user(self): - """ - ``True`` if the entity is either a user or a bot. - """ - return self.ty in (EntityType.USER, EntityType.BOT) - - @property - def is_group(self): - """ - ``True`` if the entity is a small group chat or `megagroup`_. - - .. _megagroup: https://telegram.org/blog/supergroups5k - """ - return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP) - - @property - def is_broadcast(self): - """ - ``True`` if the entity is a broadcast channel or `broadcast group`_. - - .. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members - """ - return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP) - - @classmethod - def from_str(cls, string: str): - """ - Convert the string into an `Entity`. - """ - try: - ty, id, hash = string.split('.') - ty, id, hash = ord(ty), int(id), int(hash) - except AttributeError: - raise TypeError(f'expected str, got {string!r}') from None - except (TypeError, ValueError): - raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None - - return cls(EntityType(ty), id, hash) - - @classmethod - def from_bytes(cls, blob): - """ - Convert the bytes into an `Entity`. - """ - try: - ty, id, hash = struct.unpack(' 'TelegramClient': - """ - Returns a :ref:`telethon-client` which calls methods behind a takeout session. - - It does so by creating a proxy object over the current client through - which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap - them. In other words, returns the current client modified so that - requests are done as a takeout: - - Some of the calls made through the takeout session will have lower - flood limits. This is useful if you want to export the data from - conversations or mass-download media, since the rate limits will - be lower. Only some requests will be affected, and you will need - to adjust the `wait_time` of methods like `client.iter_messages - `. - - By default, all parameters are `None`, and you need to enable those - you plan to use by setting them to either `True` or `False`. - - You should ``except errors.TakeoutInitDelayError as e``, since this - exception will raise depending on the condition of the session. You - can then access ``e.seconds`` to know how long you should wait for - before calling the method again. - - There's also a `success` property available in the takeout proxy - object, so from the `with` body you can set the boolean result that - will be sent back to Telegram. But if it's left `None` as by - default, then the action is based on the `finalize` parameter. If - it's `True` then the takeout will be finished, and if no exception - occurred during it, then `True` will be considered as a result. - Otherwise, the takeout will not be finished and its ID will be - preserved for future usage as `client.session.takeout_id - `. - - Arguments - finalize (`bool`): - Whether the takeout session should be finalized upon - exit or not. - - contacts (`bool`): - Set to `True` if you plan on downloading contacts. - - users (`bool`): - Set to `True` if you plan on downloading information - from users and their private conversations with you. - - chats (`bool`): - Set to `True` if you plan on downloading information - from small group chats, such as messages and media. - - megagroups (`bool`): - Set to `True` if you plan on downloading information - from megagroups (channels), such as messages and media. - - channels (`bool`): - Set to `True` if you plan on downloading information - from broadcast channels, such as messages and media. - - files (`bool`): - Set to `True` if you plan on downloading media and - you don't only wish to export messages. - - max_file_size (`int`): - The maximum file size, in bytes, that you plan - to download for each message with media. - - Example - .. code-block:: python - - from telethon import errors - - try: - async with client.takeout() as takeout: - await client.get_messages('me') # normal call - await takeout.get_messages('me') # wrapped through takeout (less limits) - - async for message in takeout.iter_messages(chat, wait_time=0): - ... # Do something with the message - - except errors.TakeoutInitDelayError as e: - print('Must wait', e.seconds, 'before takeout') - """ - request_kwargs = dict( - contacts=contacts, - message_users=users, - message_chats=chats, - message_megagroups=megagroups, - message_channels=channels, - files=files, - file_max_size=max_file_size - ) - arg_specified = (arg is not None for arg in request_kwargs.values()) - - if self.session.takeout_id is None or any(arg_specified): - request = functions.account.InitTakeoutSessionRequest( - **request_kwargs) - else: - request = None - - return _TakeoutClient(finalize, self, request) - - async def end_takeout(self: 'TelegramClient', success: bool) -> bool: - """ - Finishes the current takeout session. - - Arguments - success (`bool`): - Whether the takeout completed successfully or not. - - Returns - `True` if the operation was successful, `False` otherwise. - - Example - .. code-block:: python - - await client.end_takeout(success=False) - """ - try: - async with _TakeoutClient(True, self, None) as takeout: - takeout.success = success - except ValueError: - return False - return True diff --git a/telethon/client/auth.py b/telethon/client/auth.py deleted file mode 100644 index 9ca5b458..00000000 --- a/telethon/client/auth.py +++ /dev/null @@ -1,665 +0,0 @@ -import getpass -import inspect -import os -import sys -import typing -import warnings - -from .. import utils, helpers, errors, password as pwd_mod -from ..tl import types, functions, custom -from .._updates import SessionState - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class AuthMethods: - - # region Public methods - - def start( - self: 'TelegramClient', - phone: typing.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 event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. - - Arguments - phone (`str` | `int` | `callable`): - The phone (or callable without arguments to get it) - to which the code will be sent. If a bot-token-like - string is given, it will be used as such instead. - The argument may be a coroutine. - - password (`str`, `callable`, optional): - The password for 2 Factor Authentication (2FA). - This is only required if it is enabled in your account. - The argument may be a coroutine. - - bot_token (`str`): - Bot Token obtained by `@BotFather `_ - to log in as a bot. Cannot be specified with ``phone`` (only - one of either allowed). - - force_sms (`bool`, optional): - Whether to force sending the code request as SMS. - This only makes sense when signing in with a `phone`. - - code_callback (`callable`, optional): - A callable that will be used to retrieve the Telegram - login code. Defaults to `input()`. - The argument may be a coroutine. - - first_name (`str`, optional): - The first name to be used if signing up. This has no - effect if the account already exists and you sign in. - - last_name (`str`, optional): - Similar to the first name, but for the last. Optional. - - max_attempts (`int`, optional): - How many times the code/password callback should be - retried or switching between signing in and signing up. - - Returns - This `TelegramClient`, so initialization - can be chained with ``.start()``. - - Example - .. code-block:: python - - client = TelegramClient('anon', api_id, api_hash) - - # Starting as a bot account - await client.start(bot_token=bot_token) - - # Starting as a user account - await client.start(phone) - # Please enter the code you received: 12345 - # Please enter your password: ******* - # (You are now logged in) - - # Starting using a context manager (this calls start()): - with client: - pass - """ - if code_callback is None: - def code_callback(): - return input('Please enter the code you received: ') - elif not callable(code_callback): - raise ValueError( - 'The code_callback parameter needs to be a callable ' - 'function that returns the code you received by Telegram.' - ) - - if not phone and not bot_token: - raise ValueError('No phone number or bot token provided.') - - if phone and bot_token and not callable(phone): - raise ValueError('Both a phone and a bot token provided, ' - 'must only provide one of either') - - coro = self._start( - phone=phone, - password=password, - bot_token=bot_token, - force_sms=force_sms, - code_callback=code_callback, - first_name=first_name, - last_name=last_name, - max_attempts=max_attempts - ) - return ( - coro if self.loop.is_running() - else self.loop.run_until_complete(coro) - ) - - async def _start( - self: 'TelegramClient', phone, password, bot_token, force_sms, - code_callback, first_name, last_name, max_attempts): - if not self.is_connected(): - await self.connect() - - # Rather than using `is_user_authorized`, use `get_me`. While this is - # more expensive and needs to retrieve more data from the server, it - # enables the library to warn users trying to login to a different - # account. See #1172. - me = await self.get_me() - if me is not None: - # The warnings here are on a best-effort and may fail. - if bot_token: - # bot_token's first part has the bot ID, but it may be invalid - # so don't try to parse as int (instead cast our ID to string). - if bot_token[:bot_token.find(':')] != str(me.id): - warnings.warn( - 'the session already had an authorized user so it did ' - 'not login to the bot account using the provided ' - 'bot_token (it may not be using the user you expect)' - ) - elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone: - warnings.warn( - 'the session already had an authorized user so it did ' - 'not login to the user account using the provided ' - 'phone (it may not be using the user you expect)' - ) - - return self - - if not bot_token: - # Turn the callable into a valid phone number (or bot token) - while callable(phone): - value = phone() - if inspect.isawaitable(value): - value = await value - - if ':' in value: - # Bot tokens have 'user_id:access_hash' format - bot_token = value - break - - phone = utils.parse_phone(value) or phone - - if bot_token: - await self.sign_in(bot_token=bot_token) - return self - - me = None - attempts = 0 - two_step_detected = False - - await self.send_code_request(phone, force_sms=force_sms) - while attempts < max_attempts: - try: - value = code_callback() - if inspect.isawaitable(value): - value = await value - - # Since sign-in with no code works (it sends the code) - # we must double-check that here. Else we'll assume we - # logged in, and it will return None as the User. - if not value: - raise errors.PhoneCodeEmptyError(request=None) - - # Raises SessionPasswordNeededError if 2FA enabled - me = await self.sign_in(phone, code=value) - break - except errors.SessionPasswordNeededError: - two_step_detected = True - break - except (errors.PhoneCodeEmptyError, - errors.PhoneCodeExpiredError, - errors.PhoneCodeHashEmptyError, - errors.PhoneCodeInvalidError): - print('Invalid code. Please try again.', file=sys.stderr) - - attempts += 1 - else: - raise RuntimeError( - '{} consecutive sign-in attempts failed. Aborting' - .format(max_attempts) - ) - - if two_step_detected: - if not password: - raise ValueError( - "Two-step verification is enabled for this account. " - "Please provide the 'password' argument to 'start()'." - ) - - if callable(password): - for _ in range(max_attempts): - try: - value = password() - if inspect.isawaitable(value): - value = await value - - me = await self.sign_in(phone=phone, password=value) - break - except errors.PasswordHashInvalidError: - print('Invalid password. Please try again', - file=sys.stderr) - else: - raise errors.PasswordHashInvalidError(request=None) - else: - me = await self.sign_in(phone=phone, password=password) - - # We won't reach here if any step failed (exit by exception) - signed, name = 'Signed in successfully as ', utils.get_display_name(me) - tos = '; remember to not break the ToS or you will risk an account ban!' - try: - print(signed, name, tos, sep='') - except UnicodeEncodeError: - # Some terminals don't support certain characters - print(signed, name.encode('utf-8', errors='ignore') - .decode('ascii', errors='ignore'), tos, sep='') - - return self - - def _parse_phone_and_hash(self, phone, phone_hash): - """ - Helper method to both parse and validate phone and its hash. - """ - phone = utils.parse_phone(phone) or self._phone - if not phone: - raise ValueError( - 'Please make sure to call send_code_request first.' - ) - - phone_hash = phone_hash or self._phone_code_hash.get(phone, None) - if not phone_hash: - raise ValueError('You also need to provide a phone_code_hash.') - - return phone, phone_hash - - async def sign_in( - self: 'TelegramClient', - phone: str = None, - code: typing.Union[str, int] = None, - *, - password: str = None, - bot_token: str = None, - phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]': - """ - Logs in to Telegram to an existing user or bot account. - - You should only use this if you are not authorized yet. - - This method will send the code if it's not provided. - - .. note:: - - In most cases, you should simply use `start()` and not this method. - - Arguments - phone (`str` | `int`): - The phone to send the code to if no code was provided, - or to override the phone that was previously used with - these requests. - - code (`str` | `int`): - The code that Telegram sent. Note that if you have sent this - code through the application itself it will immediately - expire. If you want to send the code, obfuscate it somehow. - If you're not doing any of this you can ignore this note. - - password (`str`): - 2FA password, should be used if a previous call raised - ``SessionPasswordNeededError``. - - bot_token (`str`): - Used to sign in as a bot. Not all requests will be available. - This should be the hash the `@BotFather `_ - gave you. - - phone_code_hash (`str`, optional): - The hash returned by `send_code_request`. This can be left as - `None` to use the last hash known for the phone to be used. - - Returns - The signed in user, or the information about - :meth:`send_code_request`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - await client.sign_in(phone) # send code - - code = input('enter code: ') - await client.sign_in(phone, code) - """ - me = await self.get_me() - if me: - return me - - if phone and not code and not password: - return await self.send_code_request(phone) - elif code: - phone, phone_code_hash = \ - self._parse_phone_and_hash(phone, phone_code_hash) - - # May raise PhoneCodeEmptyError, PhoneCodeExpiredError, - # PhoneCodeHashEmptyError or PhoneCodeInvalidError. - request = functions.auth.SignInRequest( - phone, phone_code_hash, str(code) - ) - elif password: - pwd = await self(functions.account.GetPasswordRequest()) - request = functions.auth.CheckPasswordRequest( - pwd_mod.compute_check(pwd, password) - ) - elif bot_token: - request = functions.auth.ImportBotAuthorizationRequest( - flags=0, bot_auth_token=bot_token, - api_id=self.api_id, api_hash=self.api_hash - ) - else: - raise ValueError( - 'You must provide a phone and a code the first time, ' - 'and a password only if an RPCError was raised before.' - ) - - try: - result = await self(request) - except errors.PhoneCodeExpiredError: - self._phone_code_hash.pop(phone, None) - raise - - if isinstance(result, types.auth.AuthorizationSignUpRequired): - # Emulate pre-layer 104 behaviour - self._tos = result.terms_of_service - raise errors.PhoneNumberUnoccupiedError(request=request) - - return await self._on_login(result.user) - - async def sign_up( - self: 'TelegramClient', - code: typing.Union[str, int], - first_name: str, - last_name: str = '', - *, - phone: str = None, - phone_code_hash: str = None) -> 'types.User': - """ - This method can no longer be used, and will immediately raise a ``ValueError``. - See `issue #4050 `_ for context. - """ - raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details') - - async def _on_login(self, user): - """ - Callback called whenever the login or sign up process completes. - - Returns the input user parameter. - """ - self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash) - self._authorized = True - - state = await self(functions.updates.GetStateRequest()) - self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), []) - - return user - - async def send_code_request( - self: 'TelegramClient', - phone: str, - *, - force_sms: bool = False, - _retry_count: int = 0) -> 'types.auth.SentCode': - """ - Sends the Telegram code needed to login to the given phone number. - - Arguments - phone (`str` | `int`): - The phone to which the code will be sent. - - force_sms (`bool`, optional): - Whether to force sending as SMS. This has been deprecated. - See `issue #4050 `_ for context. - - Returns - An instance of :tl:`SentCode`. - - Example - .. code-block:: python - - phone = '+34 123 123 123' - sent = await client.send_code_request(phone) - print(sent) - """ - if force_sms: - warnings.warn('force_sms has been deprecated and no longer works') - force_sms = False - - result = None - phone = utils.parse_phone(phone) or self._phone - phone_hash = self._phone_code_hash.get(phone) - - if not phone_hash: - try: - result = await self(functions.auth.SendCodeRequest( - phone, self.api_id, self.api_hash, types.CodeSettings())) - except errors.AuthRestartError: - if _retry_count > 2: - raise - return await self.send_code_request( - phone, force_sms=force_sms, _retry_count=_retry_count+1) - - # TODO figure out when/if/how this can happen - if isinstance(result, types.auth.SentCodeSuccess): - raise RuntimeError('logged in right after sending the code') - - # If we already sent a SMS, do not resend the code (hash may be empty) - if isinstance(result.type, types.auth.SentCodeTypeSms): - force_sms = False - - # phone_code_hash may be empty, if it is, do not save it (#1283) - if result.phone_code_hash: - self._phone_code_hash[phone] = phone_hash = result.phone_code_hash - else: - force_sms = True - - self._phone = phone - - if force_sms: - try: - result = await self( - functions.auth.ResendCodeRequest(phone, phone_hash)) - except errors.PhoneCodeExpiredError: - if _retry_count > 2: - raise - self._phone_code_hash.pop(phone, None) - self._log[__name__].info( - "Phone code expired in ResendCodeRequest, requesting a new code" - ) - return await self.send_code_request( - phone, force_sms=False, _retry_count=_retry_count+1) - - if isinstance(result, types.auth.SentCodeSuccess): - raise RuntimeError('logged in right after resending the code') - - self._phone_code_hash[phone] = result.phone_code_hash - - return result - - async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin: - """ - Initiates the QR login procedure. - - Note that you must be connected before invoking this, as with any - other request. - - It is up to the caller to decide how to present the code to the user, - whether it's the URL, using the token bytes directly, or generating - a QR code and displaying it by other means. - - See the documentation for `QRLogin` to see how to proceed after this. - - Arguments - ignored_ids (List[`int`]): - List of already logged-in user IDs, to prevent logging in - twice with the same user. - - Returns - An instance of `QRLogin`. - - Example - .. code-block:: python - - def display_url_as_qr(url): - pass # do whatever to show url as a qr to the user - - qr_login = await client.qr_login() - display_url_as_qr(qr_login.url) - - # Important! You need to wait for the login to complete! - await qr_login.wait() - - # If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`. - # You should except that error and call `sign_in` with the password if this happens. - """ - qr_login = custom.QRLogin(self, ignored_ids or []) - await qr_login.recreate() - return qr_login - - async def log_out(self: 'TelegramClient') -> bool: - """ - Logs out Telegram and deletes the current ``*.session`` file. - - The client is unusable after logging out and a new instance should be created. - - Returns - `True` if the operation was successful. - - Example - .. code-block:: python - - # Note: you will need to login again! - await client.log_out() - """ - try: - await self(functions.auth.LogOutRequest()) - except errors.RPCError: - return False - - self._mb_entity_cache.set_self_user(None, None, None) - self._authorized = False - - await self.disconnect() - self.session.delete() - self.session = None - return True - - async def edit_2fa( - self: 'TelegramClient', - current_password: str = None, - new_password: str = None, - *, - hint: str = '', - email: str = None, - email_code_callback: typing.Callable[[int], str] = None) -> bool: - """ - Changes the 2FA settings of the logged in user. - - Review carefully the parameter explanations before using this method. - - Note that this method may be *incredibly* slow depending on the - prime numbers that must be used during the process to make sure - that everything is safe. - - Has no effect if both current and new password are omitted. - - Arguments - current_password (`str`, optional): - The current password, to authorize changing to ``new_password``. - Must be set if changing existing 2FA settings. - Must **not** be set if 2FA is currently disabled. - Passing this by itself will remove 2FA (if correct). - - new_password (`str`, optional): - The password to set as 2FA. - If 2FA was already enabled, ``current_password`` **must** be set. - Leaving this blank or `None` will remove the password. - - hint (`str`, optional): - Hint to be displayed by Telegram when it asks for 2FA. - Leaving unspecified is highly discouraged. - Has no effect if ``new_password`` is not set. - - email (`str`, optional): - Recovery and verification email. If present, you must also - set `email_code_callback`, else it raises ``ValueError``. - - email_code_callback (`callable`, optional): - If an email is provided, a callback that returns the code sent - to it must also be set. This callback may be asynchronous. - It should return a string with the code. The length of the - code will be passed to the callback as an input parameter. - - If the callback returns an invalid code, it will raise - ``CodeInvalidError``. - - Returns - `True` if successful, `False` otherwise. - - Example - .. code-block:: python - - # Setting a password for your account which didn't have - await client.edit_2fa(new_password='I_<3_Telethon') - - # Removing the password - await client.edit_2fa(current_password='I_<3_Telethon') - """ - if new_password is None and current_password is None: - return False - - if email and not callable(email_code_callback): - raise ValueError('email present without email_code_callback') - - pwd = await self(functions.account.GetPasswordRequest()) - pwd.new_algo.salt1 += os.urandom(32) - assert isinstance(pwd, types.account.Password) - if not pwd.has_password and current_password: - current_password = None - - if current_password: - password = pwd_mod.compute_check(pwd, current_password) - else: - password = types.InputCheckPasswordEmpty() - - if new_password: - new_password_hash = pwd_mod.compute_digest( - pwd.new_algo, new_password) - else: - new_password_hash = b'' - - try: - await self(functions.account.UpdatePasswordSettingsRequest( - password=password, - new_settings=types.account.PasswordInputSettings( - new_algo=pwd.new_algo, - new_password_hash=new_password_hash, - hint=hint, - email=email, - new_secure_settings=None - ) - )) - except errors.EmailUnconfirmedError as e: - code = email_code_callback(e.code_length) - if inspect.isawaitable(code): - code = await code - - code = str(code) - await self(functions.account.ConfirmPasswordEmailRequest(code)) - - return True - - # endregion - - # region with blocks - - async def __aenter__(self): - return await self.start() - - async def __aexit__(self, *args): - await self.disconnect() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - # endregion diff --git a/telethon/client/bots.py b/telethon/client/bots.py deleted file mode 100644 index 044d8513..00000000 --- a/telethon/client/bots.py +++ /dev/null @@ -1,72 +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, - *, - entity: 'hints.EntityLike' = None, - offset: str = None, - geo_point: 'types.GeoPoint' = None) -> custom.InlineResults: - """ - Makes an inline query to the specified bot (``@vote New Poll``). - - Arguments - bot (`entity`): - The bot entity to which the inline query should be made. - - query (`str`): - The query that should be made to the bot. - - entity (`entity`, optional): - The entity where the inline query is being made from. Certain - bots use this to display different results depending on where - it's used, such as private chats, groups or channels. - - If specified, it will also be the default entity where the - message will be sent after clicked. Otherwise, the "empty - peer" will be used, which some bots may not handle correctly. - - offset (`str`, optional): - The string offset to use for the bot. - - geo_point (:tl:`GeoPoint`, optional) - The geo point location information to send to the bot - for localised results. Available under some bots. - - Returns - A list of `custom.InlineResult - `. - - Example - .. code-block:: python - - # Make an inline query to @like - results = await client.inline_query('like', 'Do you like Telethon?') - - # Send the first result to some chat - message = await results[0].click('TelethonOffTopic') - """ - bot = await self.get_input_entity(bot) - if entity: - peer = await self.get_input_entity(entity) - else: - peer = types.InputPeerEmpty() - - result = await self(functions.messages.GetInlineBotResultsRequest( - bot=bot, - peer=peer, - query=query, - offset=offset or '', - geo_point=geo_point - )) - - return custom.InlineResults(self, result, entity=peer if entity else None) diff --git a/telethon/client/buttons.py b/telethon/client/buttons.py deleted file mode 100644 index 7e848ab1..00000000 --- a/telethon/client/buttons.py +++ /dev/null @@ -1,96 +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')) - # later - await client.send_message(chat, 'click me', buttons=markup) - """ - if buttons is None: - return None - - try: - if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: - return buttons # crc32(b'ReplyMarkup'): - except AttributeError: - pass - - if not utils.is_list_like(buttons): - buttons = [[buttons]] - elif not buttons or not utils.is_list_like(buttons[0]): - buttons = [buttons] - - is_inline = False - is_normal = False - resize = None - single_use = None - selective = None - - rows = [] - for row in buttons: - current = [] - for button in row: - if isinstance(button, custom.Button): - if button.resize is not None: - resize = button.resize - if button.single_use is not None: - single_use = button.single_use - if button.selective is not None: - selective = button.selective - - button = button.button - elif isinstance(button, custom.MessageButton): - button = button.button - - inline = custom.Button._is_inline(button) - is_inline |= inline - is_normal |= not inline - - if button.SUBCLASS_OF_ID == 0xbad74a3: - # 0xbad74a3 == crc32(b'KeyboardButton') - current.append(button) - - if current: - rows.append(types.KeyboardButtonRow(current)) - - if inline_only and is_normal: - raise ValueError('You cannot use non-inline buttons here') - elif is_inline == is_normal and is_normal: - raise ValueError('You cannot mix inline with normal buttons') - elif is_inline: - return types.ReplyInlineMarkup(rows) - # elif is_normal: - return types.ReplyKeyboardMarkup( - rows, resize=resize, single_use=single_use, selective=selective) diff --git a/telethon/client/chats.py b/telethon/client/chats.py deleted file mode 100644 index 73dc0312..00000000 --- a/telethon/client/chats.py +++ /dev/null @@ -1,1347 +0,0 @@ -import asyncio -import inspect -import itertools -import string -import typing - -from .. import helpers, utils, hints, errors -from ..requestiter import RequestIter -from ..tl import types, functions, custom - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - -_MAX_PARTICIPANTS_CHUNK_SIZE = 200 -_MAX_ADMIN_LOG_CHUNK_SIZE = 100 -_MAX_PROFILE_PHOTO_CHUNK_SIZE = 100 - - -class _ChatAction: - _str_mapping = { - 'typing': types.SendMessageTypingAction(), - 'contact': types.SendMessageChooseContactAction(), - 'game': types.SendMessageGamePlayAction(), - 'location': types.SendMessageGeoLocationAction(), - 'sticker': types.SendMessageChooseStickerAction(), - - 'record-audio': types.SendMessageRecordAudioAction(), - 'record-voice': types.SendMessageRecordAudioAction(), # alias - 'record-round': types.SendMessageRecordRoundAction(), - 'record-video': types.SendMessageRecordVideoAction(), - - 'audio': types.SendMessageUploadAudioAction(1), - 'voice': types.SendMessageUploadAudioAction(1), # alias - 'song': types.SendMessageUploadAudioAction(1), # alias - 'round': types.SendMessageUploadRoundAction(1), - 'video': types.SendMessageUploadVideoAction(1), - - 'photo': types.SendMessageUploadPhotoAction(1), - 'document': types.SendMessageUploadDocumentAction(1), - 'file': types.SendMessageUploadDocumentAction(1), # alias - - 'cancel': types.SendMessageCancelAction() - } - - def __init__(self, client, chat, action, *, delay, auto_cancel): - self._client = client - self._chat = chat - self._action = action - self._delay = delay - self._auto_cancel = auto_cancel - self._request = None - self._task = None - self._running = False - - async def __aenter__(self): - self._chat = await self._client.get_input_entity(self._chat) - - # Since `self._action` is passed by reference we can avoid - # recreating the request all the time and still modify - # `self._action.progress` directly in `progress`. - self._request = functions.messages.SetTypingRequest( - self._chat, self._action) - - self._running = True - self._task = self._client.loop.create_task(self._update()) - return self - - async def __aexit__(self, *args): - self._running = False - if self._task: - self._task.cancel() - try: - await self._task - except asyncio.CancelledError: - pass - - self._task = None - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - async def _update(self): - try: - while self._running: - await self._client(self._request) - await asyncio.sleep(self._delay) - except ConnectionError: - pass - except asyncio.CancelledError: - if self._auto_cancel: - await self._client(functions.messages.SetTypingRequest( - self._chat, types.SendMessageCancelAction())) - - def progress(self, current, total): - if hasattr(self._action, 'progress'): - self._action.progress = 100 * round(current / total) - - -class _ParticipantsIter(RequestIter): - async def _init(self, entity, filter, search): - if isinstance(filter, type): - if filter in (types.ChannelParticipantsBanned, - types.ChannelParticipantsKicked, - types.ChannelParticipantsSearch, - types.ChannelParticipantsContacts): - # These require a `q` parameter (support types for convenience) - filter = filter('') - else: - filter = filter() - - entity = await self.client.get_input_entity(entity) - ty = helpers._entity_type(entity) - if search and (filter or ty != helpers._EntityType.CHANNEL): - # We need to 'search' ourselves unless we have a PeerChannel - search = search.casefold() - - self.filter_entity = lambda ent: ( - search in utils.get_display_name(ent).casefold() or - search in (getattr(ent, 'username', None) or '').casefold() - ) - else: - self.filter_entity = lambda ent: True - - # Only used for channels, but we should always set the attribute - # Called `requests` even though it's just one for legacy purposes. - self.requests = None - - if ty == helpers._EntityType.CHANNEL: - if self.limit <= 0: - # May not have access to the channel, but getFull can get the .total. - self.total = (await self.client( - functions.channels.GetFullChannelRequest(entity) - )).full_chat.participants_count - raise StopAsyncIteration - - self.seen = set() - self.requests = functions.channels.GetParticipantsRequest( - channel=entity, - filter=filter or types.ChannelParticipantsSearch(search), - offset=0, - limit=_MAX_PARTICIPANTS_CHUNK_SIZE, - hash=0 - ) - - elif ty == helpers._EntityType.CHAT: - full = await self.client( - functions.messages.GetFullChatRequest(entity.chat_id)) - if not isinstance( - full.full_chat.participants, types.ChatParticipants): - # ChatParticipantsForbidden won't have ``.participants`` - self.total = 0 - raise StopAsyncIteration - - self.total = len(full.full_chat.participants.participants) - - users = {user.id: user for user in full.users} - for participant in full.full_chat.participants.participants: - if isinstance(participant, types.ChannelParticipantLeft): - # See issue #3231 to learn why this is ignored. - continue - elif isinstance(participant, types.ChannelParticipantBanned): - user_id = participant.peer.user_id - else: - user_id = participant.user_id - user = users[user_id] - if not self.filter_entity(user): - continue - - user = users[user_id] - user.participant = participant - self.buffer.append(user) - - return True - else: - self.total = 1 - if self.limit != 0: - user = await self.client.get_entity(entity) - if self.filter_entity(user): - user.participant = None - self.buffer.append(user) - - return True - - async def _load_next_chunk(self): - if not self.requests: - return True - - self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE) - - if self.requests.offset > self.limit: - return True - - if self.total is None: - f = self.requests.filter - if ( - not isinstance(f, types.ChannelParticipantsRecent) - and (not isinstance(f, types.ChannelParticipantsSearch) or f.q) - ): - # Only do an additional getParticipants here to get the total - # if there's a filter which would reduce the real total number. - # getParticipants is cheaper than getFull. - self.total = (await self.client(functions.channels.GetParticipantsRequest( - channel=self.requests.channel, - filter=types.ChannelParticipantsRecent(), - offset=0, - limit=1, - hash=0 - ))).count - - participants = await self.client(self.requests) - if self.total is None: - # Will only get here if there was one request with a filter that matched all users. - self.total = participants.count - if not participants.users: - self.requests = None - return - - self.requests.offset += len(participants.participants) - users = {user.id: user for user in participants.users} - for participant in participants.participants: - - if isinstance(participant, types.ChannelParticipantBanned): - if not isinstance(participant.peer, types.PeerUser): - # May have the entire channel banned. See #3105. - continue - user_id = participant.peer.user_id - else: - user_id = participant.user_id - - user = users[user_id] - if not self.filter_entity(user) or user.id in self.seen: - continue - self.seen.add(user_id) - user = users[user_id] - user.participant = participant - self.buffer.append(user) - - -class _AdminLogIter(RequestIter): - async def _init( - self, entity, admins, search, min_id, max_id, - join, leave, invite, restrict, unrestrict, ban, unban, - promote, demote, info, settings, pinned, edit, delete, - group_call - ): - if any((join, leave, invite, restrict, unrestrict, ban, unban, - promote, demote, info, settings, pinned, edit, delete, - group_call)): - events_filter = types.ChannelAdminLogEventsFilter( - join=join, leave=leave, invite=invite, ban=restrict, - unban=unrestrict, kick=ban, unkick=unban, promote=promote, - demote=demote, info=info, settings=settings, pinned=pinned, - edit=edit, delete=delete, group_call=group_call - ) - else: - events_filter = None - - self.entity = await self.client.get_input_entity(entity) - - admin_list = [] - if admins: - if not utils.is_list_like(admins): - admins = (admins,) - - for admin in admins: - admin_list.append(await self.client.get_input_entity(admin)) - - self.request = functions.channels.GetAdminLogRequest( - self.entity, q=search or '', min_id=min_id, max_id=max_id, - limit=0, events_filter=events_filter, admins=admin_list or None - ) - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_ADMIN_LOG_CHUNK_SIZE) - r = await self.client(self.request) - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - self.request.max_id = min((e.id for e in r.events), default=0) - for ev in r.events: - if isinstance(ev.action, - types.ChannelAdminLogEventActionEditMessage): - ev.action.prev_message._finish_init( - self.client, entities, self.entity) - - ev.action.new_message._finish_init( - self.client, entities, self.entity) - - elif isinstance(ev.action, - types.ChannelAdminLogEventActionDeleteMessage): - ev.action.message._finish_init( - self.client, entities, self.entity) - - self.buffer.append(custom.AdminLogEvent(ev, entities)) - - if len(r.events) < self.request.limit: - return True - - -class _ProfilePhotoIter(RequestIter): - async def _init( - self, entity, offset, max_id - ): - entity = await self.client.get_input_entity(entity) - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.USER: - self.request = functions.photos.GetUserPhotosRequest( - entity, - offset=offset, - max_id=max_id, - limit=1 - ) - else: - self.request = functions.messages.SearchRequest( - peer=entity, - q='', - filter=types.InputMessagesFilterChatPhotos(), - min_date=None, - max_date=None, - offset_id=0, - add_offset=offset, - limit=1, - max_id=max_id, - min_id=0, - hash=0 - ) - - if self.limit == 0: - self.request.limit = 1 - result = await self.client(self.request) - if isinstance(result, types.photos.Photos): - self.total = len(result.photos) - elif isinstance(result, types.messages.Messages): - self.total = len(result.messages) - else: - # Luckily both photosSlice and messages have a count for total - self.total = getattr(result, 'count', None) - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_PROFILE_PHOTO_CHUNK_SIZE) - result = await self.client(self.request) - - if isinstance(result, types.photos.Photos): - self.buffer = result.photos - self.left = len(self.buffer) - self.total = len(self.buffer) - elif isinstance(result, types.messages.Messages): - self.buffer = [x.action.photo for x in result.messages - if isinstance(x.action, types.MessageActionChatEditPhoto)] - - self.left = len(self.buffer) - self.total = len(self.buffer) - elif isinstance(result, types.photos.PhotosSlice): - self.buffer = result.photos - self.total = result.count - if len(self.buffer) < self.request.limit: - self.left = len(self.buffer) - else: - self.request.offset += len(result.photos) - else: - # Some broadcast channels have a photo that this request doesn't - # retrieve for whatever random reason the Telegram server feels. - # - # This means the `total` count may be wrong but there's not much - # that can be done around it (perhaps there are too many photos - # and this is only a partial result so it's not possible to just - # use the len of the result). - self.total = getattr(result, 'count', None) - - # Unconditionally fetch the full channel to obtain this photo and - # yield it with the rest (unless it's a duplicate). - seen_id = None - if isinstance(result, types.messages.ChannelMessages): - channel = await self.client(functions.channels.GetFullChannelRequest(self.request.peer)) - photo = channel.full_chat.chat_photo - if isinstance(photo, types.Photo): - self.buffer.append(photo) - seen_id = photo.id - - self.buffer.extend( - x.action.photo for x in result.messages - if isinstance(x.action, types.MessageActionChatEditPhoto) - and x.action.photo.id != seen_id - ) - - if len(result.messages) < self.request.limit: - self.left = len(self.buffer) - elif result.messages: - self.request.add_offset = 0 - self.request.offset_id = result.messages[-1].id - - -class ChatMethods: - - # region Public methods - - def iter_participants( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - search: str = '', - filter: 'types.TypeChannelParticipantsFilter' = None, - aggressive: bool = False) -> _ParticipantsIter: - """ - Iterator over the participants belonging to the specified chat. - - The order is unspecified. - - Arguments - entity (`entity`): - The entity from which to retrieve the participants list. - - limit (`int`): - Limits amount of participants fetched. - - search (`str`, optional): - Look for participants with this string in name/username. - - filter (:tl:`ChannelParticipantsFilter`, optional): - The filter to be used, if you want e.g. only admins - Note that you might not have permissions for some filter. - This has no effect for normal chats or users. - - .. note:: - - The filter :tl:`ChannelParticipantsBanned` will return - *restricted* users. If you want *banned* users you should - use :tl:`ChannelParticipantsKicked` instead. - - aggressive (`bool`, optional): - Does nothing. This is kept for backwards-compatibility. - - There have been several changes to Telegram's API that limits - the amount of members that can be retrieved, and this was a - hack that no longer works. - - Yields - The :tl:`User` objects returned by :tl:`GetParticipantsRequest` - with an additional ``.participant`` attribute which is the - matched :tl:`ChannelParticipant` type for channels/megagroups - or :tl:`ChatParticipants` for normal chats. - - Example - .. code-block:: python - - # Show all user IDs in a chat - async for user in client.iter_participants(chat): - print(user.id) - - # Search by name - async for user in client.iter_participants(chat, search='name'): - print(user.username) - - # Filter by admins - from telethon.tl.types import ChannelParticipantsAdmins - async for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins): - print(user.first_name) - """ - return _ParticipantsIter( - self, - limit, - entity=entity, - filter=filter, - search=search - ) - - async def get_participants( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_participants()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - users = await client.get_participants(chat) - print(users[0].first_name) - - for user in users: - if user.username is not None: - print(user.username) - """ - return await self.iter_participants(*args, **kwargs).collect() - - get_participants.__signature__ = inspect.signature(iter_participants) - - - def iter_admin_log( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - max_id: int = 0, - min_id: int = 0, - search: str = None, - admins: 'hints.EntitiesLike' = None, - join: bool = None, - leave: bool = None, - invite: bool = None, - restrict: bool = None, - unrestrict: bool = None, - ban: bool = None, - unban: bool = None, - promote: bool = None, - demote: bool = None, - info: bool = None, - settings: bool = None, - pinned: bool = None, - edit: bool = None, - delete: bool = None, - group_call: bool = None) -> _AdminLogIter: - """ - Iterator over the admin log for the specified channel. - - The default order is from the most recent event to to the oldest. - - Note that you must be an administrator of it to use this method. - - If none of the filters are present (i.e. they all are `None`), - *all* event types will be returned. If at least one of them is - `True`, only those that are true will be returned. - - Arguments - entity (`entity`): - The channel entity from which to get its admin log. - - limit (`int` | `None`, optional): - Number of events to be retrieved. - - The limit may also be `None`, which would eventually return - the whole history. - - max_id (`int`): - All the events with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the events with a lower (older) ID or equal to this will - be excluded. - - search (`str`): - The string to be used as a search query. - - admins (`entity` | `list`): - If present, the events will be filtered by these admins - (or single admin) and only those caused by them will be - returned. - - join (`bool`): - If `True`, events for when a user joined will be returned. - - leave (`bool`): - If `True`, events for when a user leaves will be returned. - - invite (`bool`): - If `True`, events for when a user joins through an invite - link will be returned. - - restrict (`bool`): - If `True`, events with partial restrictions will be - returned. This is what the API calls "ban". - - unrestrict (`bool`): - If `True`, events removing restrictions will be returned. - This is what the API calls "unban". - - ban (`bool`): - If `True`, events applying or removing all restrictions will - be returned. This is what the API calls "kick" (restricting - all permissions removed is a ban, which kicks the user). - - unban (`bool`): - If `True`, events removing all restrictions will be - returned. This is what the API calls "unkick". - - promote (`bool`): - If `True`, events with admin promotions will be returned. - - demote (`bool`): - If `True`, events with admin demotions will be returned. - - info (`bool`): - If `True`, events changing the group info will be returned. - - settings (`bool`): - If `True`, events changing the group settings will be - returned. - - pinned (`bool`): - If `True`, events of new pinned messages will be returned. - - edit (`bool`): - If `True`, events of message edits will be returned. - - delete (`bool`): - If `True`, events of message deletions will be returned. - - group_call (`bool`): - If `True`, events related to group calls will be returned. - - Yields - Instances of `AdminLogEvent `. - - Example - .. code-block:: python - - async for event in client.iter_admin_log(channel): - if event.changed_title: - print('The title changed from', event.old, 'to', event.new) - """ - return _AdminLogIter( - self, - limit, - entity=entity, - admins=admins, - search=search, - min_id=min_id, - max_id=max_id, - join=join, - leave=leave, - invite=invite, - restrict=restrict, - unrestrict=unrestrict, - ban=ban, - unban=unban, - promote=promote, - demote=demote, - info=info, - settings=settings, - pinned=pinned, - edit=edit, - delete=delete, - group_call=group_call - ) - - async def get_admin_log( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_admin_log()`, but returns a ``list`` instead. - - Example - .. code-block:: python - - # Get a list of deleted message events which said "heck" - events = await client.get_admin_log(channel, search='heck', delete=True) - - # Print the old message before it was deleted - print(events[0].old) - """ - return await self.iter_admin_log(*args, **kwargs).collect() - - get_admin_log.__signature__ = inspect.signature(iter_admin_log) - - def iter_profile_photos( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: int = None, - *, - offset: int = 0, - max_id: int = 0) -> _ProfilePhotoIter: - """ - Iterator over a user's profile photos or a chat's photos. - - The order is from the most recent photo to the oldest. - - Arguments - entity (`entity`): - The entity from which to get the profile or chat photos. - - limit (`int` | `None`, optional): - Number of photos to be retrieved. - - The limit may also be `None`, which would eventually all - the photos that are still available. - - offset (`int`): - How many photos should be skipped before returning the first one. - - max_id (`int`): - The maximum ID allowed when fetching photos. - - Yields - Instances of :tl:`Photo`. - - Example - .. code-block:: python - - # Download all the profile photos of some user - async for photo in client.iter_profile_photos(user): - await client.download_media(photo) - """ - return _ProfilePhotoIter( - self, - limit, - entity=entity, - offset=offset, - max_id=max_id - ) - - async def get_profile_photos( - self: 'TelegramClient', - *args, - **kwargs) -> 'hints.TotalList': - """ - Same as `iter_profile_photos()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get the photos of a channel - photos = await client.get_profile_photos(channel) - - # Download the oldest photo - await client.download_media(photos[-1]) - """ - return await self.iter_profile_photos(*args, **kwargs).collect() - - get_profile_photos.__signature__ = inspect.signature(iter_profile_photos) - - def action( - self: 'TelegramClient', - entity: 'hints.EntityLike', - action: 'typing.Union[str, types.TypeSendMessageAction]', - *, - delay: float = 4, - auto_cancel: bool = True) -> 'typing.Union[_ChatAction, typing.Coroutine]': - """ - Returns a context-manager object to represent a "chat action". - - Chat actions indicate things like "user is typing", "user is - uploading a photo", etc. - - If the action is ``'cancel'``, you should just ``await`` the result, - since it makes no sense to use a context-manager for it. - - See the example below for intended usage. - - Arguments - entity (`entity`): - The entity where the action should be showed in. - - action (`str` | :tl:`SendMessageAction`): - The action to show. You can either pass a instance of - :tl:`SendMessageAction` or better, a string used while: - - * ``'typing'``: typing a text message. - * ``'contact'``: choosing a contact. - * ``'game'``: playing a game. - * ``'location'``: choosing a geo location. - * ``'sticker'``: choosing a sticker. - * ``'record-audio'``: recording a voice note. - You may use ``'record-voice'`` as alias. - * ``'record-round'``: recording a round video. - * ``'record-video'``: recording a normal video. - * ``'audio'``: sending an audio file (voice note or song). - You may use ``'voice'`` and ``'song'`` as aliases. - * ``'round'``: uploading a round video. - * ``'video'``: uploading a video file. - * ``'photo'``: uploading a photo. - * ``'document'``: uploading a document file. - You may use ``'file'`` as alias. - * ``'cancel'``: cancel any pending action in this chat. - - Invalid strings will raise a ``ValueError``. - - delay (`int` | `float`): - The delay, in seconds, to wait between sending actions. - For example, if the delay is 5 and it takes 7 seconds to - do something, three requests will be made at 0s, 5s, and - 7s to cancel the action. - - auto_cancel (`bool`): - Whether the action should be cancelled once the context - manager exists or not. The default is `True`, since - you don't want progress to be shown when it has already - completed. - - Returns - Either a context-manager object or a coroutine. - - Example - .. code-block:: python - - # Type for 2 seconds, then send a message - async with client.action(chat, 'typing'): - await asyncio.sleep(2) - await client.send_message(chat, 'Hello world! I type slow ^^') - - # Cancel any previous action - await client.action(chat, 'cancel') - - # Upload a document, showing its progress (most clients ignore this) - async with client.action(chat, 'document') as action: - await client.send_file(chat, zip_file, progress_callback=action.progress) - """ - if isinstance(action, str): - try: - action = _ChatAction._str_mapping[action.lower()] - except KeyError: - raise ValueError( - 'No such action "{}"'.format(action)) from None - elif not isinstance(action, types.TLObject) or action.SUBCLASS_OF_ID != 0x20b2cc21: - # 0x20b2cc21 = crc32(b'SendMessageAction') - if isinstance(action, type): - raise ValueError('You must pass an instance, not the class') - else: - raise ValueError('Cannot use {} as action'.format(action)) - - if isinstance(action, types.SendMessageCancelAction): - # ``SetTypingRequest.resolve`` will get input peer of ``entity``. - return self(functions.messages.SetTypingRequest( - entity, types.SendMessageCancelAction())) - - return _ChatAction( - self, entity, action, delay=delay, auto_cancel=auto_cancel) - - async def edit_admin( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'hints.EntityLike', - *, - change_info: bool = None, - post_messages: bool = None, - edit_messages: bool = None, - delete_messages: bool = None, - ban_users: bool = None, - invite_users: bool = None, - pin_messages: bool = None, - add_admins: bool = None, - manage_call: bool = None, - anonymous: bool = None, - is_admin: bool = None, - title: str = None) -> types.Updates: - """ - Edits admin permissions for someone in a chat. - - Raises an error if a wrong combination of rights are given - (e.g. you don't have enough permissions to grant one). - - Unless otherwise stated, permissions will work in channels and megagroups. - - Arguments - entity (`entity`): - The channel, megagroup or chat where the promotion should happen. - - user (`entity`): - The user to be promoted. - - change_info (`bool`, optional): - Whether the user will be able to change info. - - post_messages (`bool`, optional): - Whether the user will be able to post in the channel. - This will only work in broadcast channels. - - edit_messages (`bool`, optional): - Whether the user will be able to edit messages in the channel. - This will only work in broadcast channels. - - delete_messages (`bool`, optional): - Whether the user will be able to delete messages. - - ban_users (`bool`, optional): - Whether the user will be able to ban users. - - invite_users (`bool`, optional): - Whether the user will be able to invite users. Needs some testing. - - pin_messages (`bool`, optional): - Whether the user will be able to pin messages. - - add_admins (`bool`, optional): - Whether the user will be able to add admins. - - manage_call (`bool`, optional): - Whether the user will be able to manage group calls. - - anonymous (`bool`, optional): - Whether the user will remain anonymous when sending messages. - The sender of the anonymous messages becomes the group itself. - - .. note:: - - Users may be able to identify the anonymous admin by its - custom title, so additional care is needed when using both - ``anonymous`` and custom titles. For example, if multiple - anonymous admins share the same title, users won't be able - to distinguish them. - - is_admin (`bool`, optional): - Whether the user will be an admin in the chat. - This will only work in small group chats. - Whether the user will be an admin in the chat. This is the - only permission available in small group chats, and when - used in megagroups, all non-explicitly set permissions will - have this value. - - Essentially, only passing ``is_admin=True`` will grant all - permissions, but you can still disable those you need. - - title (`str`, optional): - The custom title (also known as "rank") to show for this admin. - This text will be shown instead of the "admin" badge. - This will only work in channels and megagroups. - - When left unspecified or empty, the default localized "admin" - badge will be shown. - - Returns - The resulting :tl:`Updates` object. - - Example - .. code-block:: python - - # Allowing `user` to pin messages in `chat` - await client.edit_admin(chat, user, pin_messages=True) - - # Granting all permissions except for `add_admins` - await client.edit_admin(chat, user, is_admin=True, add_admins=False) - """ - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - perm_names = ( - 'change_info', 'post_messages', 'edit_messages', 'delete_messages', - 'ban_users', 'invite_users', 'pin_messages', 'add_admins', - 'anonymous', 'manage_call', - ) - - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.CHANNEL: - # If we try to set these permissions in a megagroup, we - # would get a RIGHT_FORBIDDEN. However, it makes sense - # that an admin can post messages, so we want to avoid the error - if post_messages or edit_messages: - # TODO get rid of this once sessions cache this information - if entity.channel_id not in self._megagroup_cache: - full_entity = await self.get_entity(entity) - self._megagroup_cache[entity.channel_id] = full_entity.megagroup - - if self._megagroup_cache[entity.channel_id]: - post_messages = None - edit_messages = None - - perms = locals() - return await self(functions.channels.EditAdminRequest(entity, user, types.ChatAdminRights(**{ - # A permission is its explicit (not-None) value or `is_admin`. - # This essentially makes `is_admin` be the default value. - name: perms[name] if perms[name] is not None else is_admin - for name in perm_names - }), rank=title or '')) - - elif ty == helpers._EntityType.CHAT: - # If the user passed any permission in a small - # group chat, they must be a full admin to have it. - if is_admin is None: - is_admin = any(locals()[x] for x in perm_names) - - return await self(functions.messages.EditChatAdminRequest( - entity.chat_id, user, is_admin=is_admin)) - - else: - raise ValueError( - 'You can only edit permissions in groups and channels') - - async def edit_permissions( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' = None, - until_date: 'hints.DateLike' = None, - *, - view_messages: bool = True, - send_messages: bool = True, - send_media: bool = True, - send_stickers: bool = True, - send_gifs: bool = True, - send_games: bool = True, - send_inline: bool = True, - embed_link_previews: bool = True, - send_polls: bool = True, - change_info: bool = True, - invite_users: bool = True, - pin_messages: bool = True) -> types.Updates: - """ - Edits user restrictions in a chat. - - Set an argument to `False` to apply a restriction (i.e. remove - the permission), or omit them to use the default `True` (i.e. - don't apply a restriction). - - Raises an error if a wrong combination of rights are given - (e.g. you don't have enough permissions to revoke one). - - By default, each boolean argument is `True`, meaning that it - is true that the user has access to the default permission - and may be able to make use of it. - - If you set an argument to `False`, then a restriction is applied - regardless of the default permissions. - - It is important to note that `True` does *not* mean grant, only - "don't restrict", and this is where the default permissions come - in. A user may have not been revoked the ``pin_messages`` permission - (it is `True`) but they won't be able to use it if the default - permissions don't allow it either. - - Arguments - entity (`entity`): - The channel or megagroup where the restriction should happen. - - user (`entity`, optional): - If specified, the permission will be changed for the specific user. - If left as `None`, the default chat permissions will be updated. - - until_date (`DateLike`, optional): - When the user will be unbanned. - - If the due date or duration is longer than 366 days or shorter than - 30 seconds, the ban will be forever. Defaults to ``0`` (ban forever). - - view_messages (`bool`, optional): - Whether the user is able to view messages or not. - Forbidding someone from viewing messages equals to banning them. - This will only work if ``user`` is set. - - send_messages (`bool`, optional): - Whether the user is able to send messages or not. - - send_media (`bool`, optional): - Whether the user is able to send media or not. - - send_stickers (`bool`, optional): - Whether the user is able to send stickers or not. - - send_gifs (`bool`, optional): - Whether the user is able to send animated gifs or not. - - send_games (`bool`, optional): - Whether the user is able to send games or not. - - send_inline (`bool`, optional): - Whether the user is able to use inline bots or not. - - embed_link_previews (`bool`, optional): - Whether the user is able to enable the link preview in the - messages they send. Note that the user will still be able to - send messages with links if this permission is removed, but - these links won't display a link preview. - - send_polls (`bool`, optional): - Whether the user is able to send polls or not. - - change_info (`bool`, optional): - Whether the user is able to change info or not. - - invite_users (`bool`, optional): - Whether the user is able to invite other users or not. - - pin_messages (`bool`, optional): - Whether the user is able to pin messages or not. - - Returns - The resulting :tl:`Updates` object. - - Example - .. code-block:: python - - from datetime import timedelta - - # Banning `user` from `chat` for 1 minute - await client.edit_permissions(chat, user, timedelta(minutes=1), - view_messages=False) - - # Banning `user` from `chat` forever - await client.edit_permissions(chat, user, view_messages=False) - - # Kicking someone (ban + un-ban) - await client.edit_permissions(chat, user, view_messages=False) - await client.edit_permissions(chat, user) - """ - entity = await self.get_input_entity(entity) - ty = helpers._entity_type(entity) - if ty != helpers._EntityType.CHANNEL: - raise ValueError('You must pass either a channel or a supergroup') - - rights = types.ChatBannedRights( - until_date=until_date, - view_messages=not view_messages, - send_messages=not send_messages, - send_media=not send_media, - send_stickers=not send_stickers, - send_gifs=not send_gifs, - send_games=not send_games, - send_inline=not send_inline, - embed_links=not embed_link_previews, - send_polls=not send_polls, - change_info=not change_info, - invite_users=not invite_users, - pin_messages=not pin_messages - ) - - if user is None: - return await self(functions.messages.EditChatDefaultBannedRightsRequest( - peer=entity, - banned_rights=rights - )) - - user = await self.get_input_entity(user) - ty = helpers._entity_type(user) - if ty != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - if isinstance(user, types.InputPeerSelf): - raise ValueError('You cannot restrict yourself') - - return await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, - banned_rights=rights - )) - - async def kick_participant( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'typing.Optional[hints.EntityLike]' - ): - """ - Kicks a user from a chat. - - Kicking yourself (``'me'``) will result in leaving the chat. - - .. note:: - - Attempting to kick someone who was banned will remove their - restrictions (and thus unbanning them), since kicking is just - ban + unban. - - Arguments - entity (`entity`): - The channel or chat where the user should be kicked from. - - user (`entity`, optional): - The user to kick. - - Returns - Returns the service `Message ` - produced about a user being kicked, if any. - - Example - .. code-block:: python - - # Kick some user from some chat, and deleting the service message - msg = await client.kick_participant(chat, user) - await msg.delete() - - # Leaving chat - await client.kick_participant(chat, 'me') - """ - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.CHAT: - resp = await self(functions.messages.DeleteChatUserRequest(entity.chat_id, user)) - elif ty == helpers._EntityType.CHANNEL: - if isinstance(user, types.InputPeerSelf): - # Despite no longer being in the channel, the account still - # seems to get the service message. - resp = await self(functions.channels.LeaveChannelRequest(entity)) - else: - resp = await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, - banned_rights=types.ChatBannedRights( - until_date=None, view_messages=True) - )) - await asyncio.sleep(0.5) - await self(functions.channels.EditBannedRequest( - channel=entity, - participant=user, - banned_rights=types.ChatBannedRights(until_date=None) - )) - else: - raise ValueError('You must pass either a channel or a chat') - - return self._get_response_message(None, resp, entity) - - async def get_permissions( - self: 'TelegramClient', - entity: 'hints.EntityLike', - user: 'hints.EntityLike' = None - ) -> 'typing.Optional[custom.ParticipantPermissions]': - """ - Fetches the permissions of a user in a specific chat or channel or - get Default Restricted Rights of Chat or Channel. - - .. note:: - - This request has to fetch the entire chat for small group chats, - which can get somewhat expensive, so use of a cache is advised. - - Arguments - entity (`entity`): - The channel or chat the user is participant of. - - user (`entity`, optional): - Target user. - - Returns - A `ParticipantPermissions ` - instance. Refer to its documentation to see what properties are - available. - - Example - .. code-block:: python - - permissions = await client.get_permissions(chat, user) - if permissions.is_admin: - # do something - - # Get Banned Permissions of Chat - await client.get_permissions(chat) - """ - entity = await self.get_entity(entity) - - if not user: - if isinstance(entity, types.Channel): - FullChat = await self(functions.channels.GetFullChannelRequest(entity)) - elif isinstance(entity, types.Chat): - FullChat = await self(functions.messages.GetFullChatRequest(entity.id)) - else: - return - return FullChat.chats[0].default_banned_rights - - entity = await self.get_input_entity(entity) - user = await self.get_input_entity(user) - if helpers._entity_type(user) != helpers._EntityType.USER: - raise ValueError('You must pass a user entity') - if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: - participant = await self(functions.channels.GetParticipantRequest( - entity, - user - )) - return custom.ParticipantPermissions(participant.participant, False) - elif helpers._entity_type(entity) == helpers._EntityType.CHAT: - chat = await self(functions.messages.GetFullChatRequest( - entity.chat_id - )) - if isinstance(user, types.InputPeerSelf): - user = await self.get_me(input_peer=True) - for participant in chat.full_chat.participants.participants: - if participant.user_id == user.user_id: - return custom.ParticipantPermissions(participant, True) - raise errors.UserNotParticipantError(None) - - raise ValueError('You must pass either a channel or a chat') - - async def get_stats( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[int, types.Message]' = None, - ): - """ - Retrieves statistics from the given megagroup or broadcast channel. - - Note that some restrictions apply before being able to fetch statistics, - in particular the channel must have enough members (for megagroups, this - requires `at least 500 members`_). - - Arguments - entity (`entity`): - The channel from which to get statistics. - - message (`int` | ``Message``, optional): - The message ID from which to get statistics, if your goal is - to obtain the statistics of a single message. - - Raises - If the given entity is not a channel (broadcast or megagroup), - a `TypeError` is raised. - - If there are not enough members (poorly named) errors such as - ``telethon.errors.ChatAdminRequiredError`` will appear. - - Returns - If both ``entity`` and ``message`` were provided, returns - :tl:`MessageStats`. Otherwise, either :tl:`BroadcastStats` or - :tl:`MegagroupStats`, depending on whether the input belonged to a - broadcast channel or megagroup. - - Example - .. code-block:: python - - # Some megagroup or channel username or ID to fetch - channel = -100123 - stats = await client.get_stats(channel) - print('Stats from', stats.period.min_date, 'to', stats.period.max_date, ':') - print(stats.stringify()) - - .. _`at least 500 members`: https://telegram.org/blog/profile-videos-people-nearby-and-more - """ - entity = await self.get_input_entity(entity) - if helpers._entity_type(entity) != helpers._EntityType.CHANNEL: - raise TypeError('You must pass a channel entity') - - message = utils.get_message_id(message) - if message is not None: - try: - req = functions.stats.GetMessageStatsRequest(entity, message) - return await self(req) - except errors.StatsMigrateError as e: - dc = e.dc - else: - # Don't bother fetching the Channel entity (costs a request), instead - # try to guess and if it fails we know it's the other one (best case - # no extra request, worst just one). - try: - req = functions.stats.GetBroadcastStatsRequest(entity) - return await self(req) - except errors.StatsMigrateError as e: - dc = e.dc - except errors.BroadcastRequiredError: - req = functions.stats.GetMegagroupStatsRequest(entity) - try: - return await self(req) - except errors.StatsMigrateError as e: - dc = e.dc - - sender = await self._borrow_exported_sender(dc) - try: - # req will be resolved to use the right types inside by now - return await sender.send(req) - finally: - await self._return_exported_sender(sender) - - # endregion diff --git a/telethon/client/dialogs.py b/telethon/client/dialogs.py deleted file mode 100644 index b0db0a61..00000000 --- a/telethon/client/dialogs.py +++ /dev/null @@ -1,610 +0,0 @@ -import asyncio -import inspect -import itertools -import typing - -from .. import helpers, utils, hints, errors -from ..requestiter import RequestIter -from ..tl import types, functions, custom - -_MAX_CHUNK_SIZE = 100 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -def _dialog_message_key(peer, message_id): - """ - Get the key to get messages from a dialog. - - We cannot just use the message ID because channels share message IDs, - and the peer ID is required to distinguish between them. But it is not - necessary in small group chats and private chats. - """ - return (peer.channel_id if isinstance(peer, types.PeerChannel) else None), message_id - - -class _DialogsIter(RequestIter): - async def _init( - self, offset_date, offset_id, offset_peer, ignore_pinned, ignore_migrated, folder - ): - self.request = functions.messages.GetDialogsRequest( - offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - limit=1, - hash=0, - exclude_pinned=ignore_pinned, - folder_id=folder - ) - - if self.limit <= 0: - # Special case, get a single dialog and determine count - dialogs = await self.client(self.request) - self.total = getattr(dialogs, 'count', len(dialogs.dialogs)) - raise StopAsyncIteration - - self.seen = set() - self.offset_date = offset_date - self.ignore_migrated = ignore_migrated - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) - r = await self.client(self.request) - - self.total = getattr(r, 'count', len(r.dialogs)) - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats) - if not isinstance(x, (types.UserEmpty, types.ChatEmpty))} - - self.client._mb_entity_cache.extend(r.users, r.chats) - - messages = {} - for m in r.messages: - m._finish_init(self.client, entities, None) - messages[_dialog_message_key(m.peer_id, m.id)] = m - - for d in r.dialogs: - # We check the offset date here because Telegram may ignore it - message = messages.get(_dialog_message_key(d.peer, d.top_message)) - if self.offset_date: - date = getattr(message, 'date', None) - if not date or date.timestamp() > self.offset_date.timestamp(): - continue - - peer_id = utils.get_peer_id(d.peer) - if peer_id not in self.seen: - self.seen.add(peer_id) - if peer_id not in entities: - # > In which case can a UserEmpty appear in the list of banned members? - # > In a very rare cases. This is possible but isn't an expected behavior. - # Real world example: https://t.me/TelethonChat/271471 - continue - - cd = custom.Dialog(self.client, d, entities, message) - if cd.dialog.pts: - self.client._message_box.try_set_channel_state( - utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts) - - if not self.ignore_migrated or getattr( - cd.entity, 'migrated_to', None) is None: - self.buffer.append(cd) - - if not self.buffer or len(r.dialogs) < self.request.limit\ - or not isinstance(r, types.messages.DialogsSlice): - # Buffer being empty means all returned dialogs were skipped (due to offsets). - # Less than we requested means we reached the end, or - # we didn't get a DialogsSlice which means we got all. - return True - - # We can't use `messages[-1]` as the offset ID / date. - # Why? Because pinned dialogs will mess with the order - # in this list. Instead, we find the last dialog which - # has a message, and use it as an offset. - last_message = next(filter(None, ( - messages.get(_dialog_message_key(d.peer, d.top_message)) - for d in reversed(r.dialogs) - )), None) - - self.request.exclude_pinned = True - self.request.offset_id = last_message.id if last_message else 0 - self.request.offset_date = last_message.date if last_message else None - self.request.offset_peer = self.buffer[-1].input_entity - - -class _DraftsIter(RequestIter): - async def _init(self, entities, **kwargs): - if not entities: - r = await self.client(functions.messages.GetAllDraftsRequest()) - items = r.updates - else: - peers = [] - for entity in entities: - peers.append(types.InputDialogPeer( - await self.client.get_input_entity(entity))) - - r = await self.client(functions.messages.GetPeerDialogsRequest(peers)) - items = r.dialogs - - # TODO Maybe there should be a helper method for this? - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - self.buffer.extend( - custom.Draft(self.client, entities[utils.get_peer_id(d.peer)], d.draft) - for d in items - ) - - async def _load_next_chunk(self): - return [] - - -class DialogMethods: - - # region Public methods - - def iter_dialogs( - self: 'TelegramClient', - limit: float = None, - *, - offset_date: 'hints.DateLike' = None, - offset_id: int = 0, - offset_peer: 'hints.EntityLike' = types.InputPeerEmpty(), - ignore_pinned: bool = False, - ignore_migrated: bool = False, - folder: int = None, - archived: bool = None - ) -> _DialogsIter: - """ - Iterator over the dialogs (open conversations/subscribed channels). - - The order is the same as the one seen in official applications - (first pinned, them from those with the most recent message to - those with the oldest message). - - Arguments - limit (`int` | `None`): - How many dialogs to be retrieved as maximum. Can be set to - `None` to retrieve all dialogs. Note that this may take - whole minutes if you have hundreds of dialogs, as Telegram - will tell the library to slow down through a - ``FloodWaitError``. - - offset_date (`datetime`, optional): - The offset date to be used. - - offset_id (`int`, optional): - The message ID to be used as an offset. - - offset_peer (:tl:`InputPeer`, optional): - The peer to be used as an offset. - - ignore_pinned (`bool`, optional): - Whether pinned dialogs should be ignored or not. - When set to `True`, these won't be yielded at all. - - ignore_migrated (`bool`, optional): - Whether :tl:`Chat` that have ``migrated_to`` a :tl:`Channel` - should be included or not. By default all the chats in your - dialogs are returned, but setting this to `True` will ignore - (i.e. skip) them in the same way official applications do. - - folder (`int`, optional): - The folder from which the dialogs should be retrieved. - - If left unspecified, all dialogs (including those from - folders) will be returned. - - If set to ``0``, all dialogs that don't belong to any - folder will be returned. - - If set to a folder number like ``1``, only those from - said folder will be returned. - - By default Telegram assigns the folder ID ``1`` to - archived chats, so you should use that if you need - to fetch the archived dialogs. - - archived (`bool`, optional): - Alias for `folder`. If unspecified, all will be returned, - `False` implies ``folder=0`` and `True` implies ``folder=1``. - Yields - Instances of `Dialog `. - - Example - .. code-block:: python - - # Print all dialog IDs and the title, nicely formatted - async for dialog in client.iter_dialogs(): - print('{:>14}: {}'.format(dialog.id, dialog.title)) - """ - if archived is not None: - folder = 1 if archived else 0 - - return _DialogsIter( - self, - limit, - offset_date=offset_date, - offset_id=offset_id, - offset_peer=offset_peer, - ignore_pinned=ignore_pinned, - ignore_migrated=ignore_migrated, - folder=folder - ) - - async def get_dialogs(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList': - """ - Same as `iter_dialogs()`, but returns a - `TotalList ` instead. - - Example - .. code-block:: python - - # Get all open conversation, print the title of the first - dialogs = await client.get_dialogs() - first = dialogs[0] - print(first.title) - - # Use the dialog somewhere else - await client.send_message(first, 'hi') - - # Getting only non-archived dialogs (both equivalent) - non_archived = await client.get_dialogs(folder=0) - non_archived = await client.get_dialogs(archived=False) - - # Getting only archived dialogs (both equivalent) - archived = await client.get_dialogs(folder=1) - archived = await client.get_dialogs(archived=True) - """ - return await self.iter_dialogs(*args, **kwargs).collect() - - get_dialogs.__signature__ = inspect.signature(iter_dialogs) - - def iter_drafts( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None - ) -> _DraftsIter: - """ - Iterator over draft messages. - - The order is unspecified. - - Arguments - entity (`hints.EntitiesLike`, optional): - The entity or entities for which to fetch the draft messages. - If left unspecified, all draft messages will be returned. - - Yields - Instances of `Draft `. - - Example - .. code-block:: python - - # Clear all drafts - async for draft in client.get_drafts(): - await draft.delete() - - # Getting the drafts with 'bot1' and 'bot2' - async for draft in client.iter_drafts(['bot1', 'bot2']): - print(draft.text) - """ - if entity and not utils.is_list_like(entity): - entity = (entity,) - - # TODO Passing a limit here makes no sense - return _DraftsIter(self, None, entities=entity) - - async def get_drafts( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None - ) -> 'hints.TotalList': - """ - Same as `iter_drafts()`, but returns a list instead. - - Example - .. code-block:: python - - # Get drafts, print the text of the first - drafts = await client.get_drafts() - print(drafts[0].text) - - # Get the draft in your chat - draft = await client.get_drafts('me') - print(drafts.text) - """ - items = await self.iter_drafts(entity).collect() - if not entity or utils.is_list_like(entity): - return items - else: - return items[0] - - async def edit_folder( - self: 'TelegramClient', - entity: 'hints.EntitiesLike' = None, - folder: typing.Union[int, typing.Sequence[int]] = None, - *, - unpack=None - ) -> types.Updates: - """ - Edits the folder used by one or more dialogs to archive them. - - Arguments - entity (entities): - The entity or list of entities to move to the desired - archive folder. - - folder (`int`): - The folder to which the dialog should be archived to. - - If you want to "archive" a dialog, use ``folder=1``. - - If you want to "un-archive" it, use ``folder=0``. - - You may also pass a list with the same length as - `entities` if you want to control where each entity - will go. - - unpack (`int`, optional): - If you want to unpack an archived folder, set this - parameter to the folder number that you want to - delete. - - When you unpack a folder, all the dialogs inside are - moved to the folder number 0. - - You can only use this parameter if the other two - are not set. - - Returns - The :tl:`Updates` object that the request produces. - - Example - .. code-block:: python - - # Archiving the first 5 dialogs - dialogs = await client.get_dialogs(5) - await client.edit_folder(dialogs, 1) - - # Un-archiving the third dialog (archiving to folder 0) - await client.edit_folder(dialog[2], 0) - - # Moving the first dialog to folder 0 and the second to 1 - dialogs = await client.get_dialogs(2) - await client.edit_folder(dialogs, [0, 1]) - - # Un-archiving all dialogs - await client.edit_folder(unpack=1) - """ - if (entity is None) == (unpack is None): - raise ValueError('You can only set either entities or unpack, not both') - - if unpack is not None: - return await self(functions.folders.DeleteFolderRequest( - folder_id=unpack - )) - - if not utils.is_list_like(entity): - entities = [await self.get_input_entity(entity)] - else: - entities = await asyncio.gather( - *(self.get_input_entity(x) for x in entity)) - - if folder is None: - raise ValueError('You must specify a folder') - elif not utils.is_list_like(folder): - folder = [folder] * len(entities) - elif len(entities) != len(folder): - raise ValueError('Number of folders does not match number of entities') - - return await self(functions.folders.EditPeerFoldersRequest([ - types.InputFolderPeer(x, folder_id=y) - for x, y in zip(entities, folder) - ])) - - async def delete_dialog( - self: 'TelegramClient', - entity: 'hints.EntityLike', - *, - revoke: bool = False - ): - """ - Deletes a dialog (leaves a chat or channel). - - This method can be used as a user and as a bot. However, - bots will only be able to use it to leave groups and channels - (trying to delete a private conversation will do nothing). - - See also `Dialog.delete() `. - - Arguments - entity (entities): - The entity of the dialog to delete. If it's a chat or - channel, you will leave it. Note that the chat itself - is not deleted, only the dialog, because you left it. - - revoke (`bool`, optional): - On private chats, you may revoke the messages from - the other peer too. By default, it's `False`. Set - it to `True` to delete the history for both. - - This makes no difference for bot accounts, who can - only leave groups and channels. - - Returns - The :tl:`Updates` object that the request produces, - or nothing for private conversations. - - Example - .. code-block:: python - - # Deleting the first dialog - dialogs = await client.get_dialogs(5) - await client.delete_dialog(dialogs[0]) - - # Leaving a channel by username - await client.delete_dialog('username') - """ - # If we have enough information (`Dialog.delete` gives it to us), - # then we know we don't have to kick ourselves in deactivated chats. - if isinstance(entity, types.Chat): - deactivated = entity.deactivated - else: - deactivated = False - - entity = await self.get_input_entity(entity) - ty = helpers._entity_type(entity) - if ty == helpers._EntityType.CHANNEL: - return await self(functions.channels.LeaveChannelRequest(entity)) - - if ty == helpers._EntityType.CHAT and not deactivated: - try: - result = await self(functions.messages.DeleteChatUserRequest( - entity.chat_id, types.InputUserSelf(), revoke_history=revoke - )) - except errors.PeerIdInvalidError: - # Happens if we didn't have the deactivated information - result = None - else: - result = None - - if not await self.is_bot(): - await self(functions.messages.DeleteHistoryRequest(entity, 0, revoke=revoke)) - - return result - - def conversation( - self: 'TelegramClient', - entity: 'hints.EntityLike', - *, - timeout: float = 60, - total_timeout: float = None, - max_messages: int = 100, - exclusive: bool = True, - replies_are_responses: bool = True) -> custom.Conversation: - """ - Creates a `Conversation ` - with the given entity. - - .. note:: - - This Conversation API has certain shortcomings, such as lacking - persistence, poor interaction with other event handlers, and - overcomplicated usage for anything beyond the simplest case. - - If you plan to interact with a bot without handlers, this works - fine, but when running a bot yourself, you may instead prefer - to follow the advice from https://stackoverflow.com/a/62246569/. - - This is not the same as just sending a message to create a "dialog" - with them, but rather a way to easily send messages and await for - responses or other reactions. Refer to its documentation for more. - - Arguments - entity (`entity`): - The entity with which a new conversation should be opened. - - timeout (`int` | `float`, optional): - The default timeout (in seconds) *per action* to be used. You - may also override this timeout on a per-method basis. By - default each action can take up to 60 seconds (the value of - this timeout). - - total_timeout (`int` | `float`, optional): - The total timeout (in seconds) to use for the whole - conversation. This takes priority over per-action - timeouts. After these many seconds pass, subsequent - actions will result in ``asyncio.TimeoutError``. - - max_messages (`int`, optional): - The maximum amount of messages this conversation will - remember. After these many messages arrive in the - specified chat, subsequent actions will result in - ``ValueError``. - - exclusive (`bool`, optional): - By default, conversations are exclusive within a single - chat. That means that while a conversation is open in a - chat, you can't open another one in the same chat, unless - you disable this flag. - - If you try opening an exclusive conversation for - a chat where it's already open, it will raise - ``AlreadyInConversationError``. - - replies_are_responses (`bool`, optional): - Whether replies should be treated as responses or not. - - If the setting is enabled, calls to `conv.get_response - ` - and a subsequent call to `conv.get_reply - ` - will return different messages, otherwise they may return - the same message. - - Consider the following scenario with one outgoing message, - 1, and two incoming messages, the second one replying:: - - Hello! <1 - 2> (reply to 1) Hi! - 3> (reply to 1) How are you? - - And the following code: - - .. code-block:: python - - async with client.conversation(chat) as conv: - msg1 = await conv.send_message('Hello!') - msg2 = await conv.get_response() - msg3 = await conv.get_reply() - - With the setting enabled, ``msg2`` will be ``'Hi!'`` and - ``msg3`` be ``'How are you?'`` since replies are also - responses, and a response was already returned. - - With the setting disabled, both ``msg2`` and ``msg3`` will - be ``'Hi!'`` since one is a response and also a reply. - - Returns - A `Conversation `. - - Example - .. code-block:: python - - # denotes outgoing messages you sent - # denotes incoming response messages - with bot.conversation(chat) as conv: - # Hi! - conv.send_message('Hi!') - - # Hello! - hello = conv.get_response() - - # Please tell me your name - conv.send_message('Please tell me your name') - - # ? - name = conv.get_response().raw_text - - while not any(x.isalpha() for x in name): - # Your name didn't have any letters! Try again - conv.send_message("Your name didn't have any letters! Try again") - - # Human - name = conv.get_response().raw_text - - # Thanks Human! - conv.send_message('Thanks {}!'.format(name)) - """ - return custom.Conversation( - self, - entity, - timeout=timeout, - total_timeout=total_timeout, - max_messages=max_messages, - exclusive=exclusive, - replies_are_responses=replies_are_responses - - ) - - # endregion diff --git a/telethon/client/downloads.py b/telethon/client/downloads.py deleted file mode 100644 index d7ae5a9c..00000000 --- a/telethon/client/downloads.py +++ /dev/null @@ -1,1046 +0,0 @@ -import datetime -import io -import os -import pathlib -import typing -import inspect -import asyncio - -from ..crypto import AES - -from .. import utils, helpers, errors, hints -from ..requestiter import RequestIter -from ..tl import TLObject, types, functions - -try: - import aiohttp -except ImportError: - aiohttp = None - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - -# Chunk sizes for upload.getFile must be multiples of the smallest size -MIN_CHUNK_SIZE = 4096 -MAX_CHUNK_SIZE = 512 * 1024 - -# 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files. -TIMED_OUT_SLEEP = 1 - -class _DirectDownloadIter(RequestIter): - async def _init( - self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data - ): - 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._msg_data = msg_data - self._timed_out = False - - self._exported = dc_id and self.client.session.dc_id != dc_id - if not self._exported: - # The used sender will also change if ``FileMigrateError`` occurs - self._sender = self.client._sender - else: - try: - self._sender = await self.client._borrow_exported_sender(dc_id) - except errors.DcIdInvalidError: - # Can't export a sender for the ID we are currently in - config = await self.client(functions.help.GetConfigRequest()) - for option in config.dc_options: - if option.ip_address == self.client.session.server_address: - self.client.session.set_dc( - option.id, option.ip_address, option.port) - self.client.session.save() - break - - # TODO Figure out why the session may have the wrong DC ID - self._sender = self.client._sender - self._exported = False - - async def _load_next_chunk(self): - cur = await self._request() - self.buffer.append(cur) - if len(cur) < self.request.limit: - self.left = len(self.buffer) - await self.close() - else: - self.request.offset += self._stride - - async def _request(self): - try: - result = await self.client._call(self._sender, self.request) - self._timed_out = False - if isinstance(result, types.upload.FileCdnRedirect): - raise NotImplementedError # TODO Implement - else: - return result.bytes - - except errors.TimeoutError as e: - if self._timed_out: - self.client._log[__name__].warning('Got two timeouts in a row while downloading file') - raise - - self._timed_out = True - self.client._log[__name__].info('Got timeout while downloading file, retrying once') - await asyncio.sleep(TIMED_OUT_SLEEP) - return await self._request() - - except errors.FileMigrateError as e: - self.client._log[__name__].info('File lives in another DC') - self._sender = await self.client._borrow_exported_sender(e.new_dc) - self._exported = True - return await self._request() - - except errors.FilerefUpgradeNeededError as e: - # Only implemented for documents which are the ones that may take that long to download - if not self._msg_data \ - or not isinstance(self.request.location, types.InputDocumentFileLocation) \ - or self.request.location.thumb_size != '': - raise - - self.client._log[__name__].info('File ref expired during download; refetching message') - chat, msg_id = self._msg_data - msg = await self.client.get_messages(chat, ids=msg_id) - - if not isinstance(msg.media, types.MessageMediaDocument): - raise - - document = msg.media.document - - # Message media may have been edited for something else - if document.id != self.request.location.id: - raise - - self.request.location.file_reference = document.file_reference - return await self._request() - - async def close(self): - if not self._sender: - return - - try: - if self._exported: - await self.client._return_exported_sender(self._sender) - elif self._sender != self.client._sender: - await self._sender.disconnect() - finally: - self._sender = None - - async def __aenter__(self): - return self - - async def __aexit__(self, *args): - await self.close() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - -class _GenericDownloadIter(_DirectDownloadIter): - async def _load_next_chunk(self): - # 1. Fetch enough for one chunk - data = b'' - - # 1.1. ``bad`` is how much into the data we have we need to offset - bad = self.request.offset % self.request.limit - before = self.request.offset - - # 1.2. We have to fetch from a valid offset, so remove that bad part - self.request.offset -= bad - - done = False - while not done and len(data) - bad < self._chunk_size: - cur = await self._request() - self.request.offset += self.request.limit - - data += cur - done = len(cur) < self.request.limit - - # 1.3 Restore our last desired offset - self.request.offset = before - - # 2. Fill the buffer with the data we have - # 2.1. Slicing `bytes` is expensive, yield `memoryview` instead - mem = memoryview(data) - - # 2.2. The current chunk starts at ``bad`` offset into the data, - # and each new chunk is ``stride`` bytes apart of the other - for i in range(bad, len(data), self._stride): - self.buffer.append(mem[i:i + self._chunk_size]) - - # 2.3. We will yield this offset, so move to the next one - self.request.offset += self._stride - - # 2.4. If we are in the last chunk, we will return the last partial data - if done: - self.left = len(self.buffer) - await self.close() - return - - # 2.5. If we are not done, we can't return incomplete chunks. - if len(self.buffer[-1]) != self._chunk_size: - self._last_part = self.buffer.pop().tobytes() - - # 3. Be careful with the offsets. Re-fetching a bit of data - # is fine, since it greatly simplifies things. - # TODO Try to not re-fetch data - self.request.offset -= self._stride - - -class DownloadMethods: - - # region Public methods - - async def download_profile_photo( - self: 'TelegramClient', - entity: 'hints.EntityLike', - file: 'hints.FileLike' = None, - *, - download_big: bool = True) -> typing.Optional[str]: - """ - Downloads the profile photo from the given user, chat or channel. - - Arguments - entity (`entity`): - From who the photo will be downloaded. - - .. note:: - - This method expects the full entity (which has the data - to download the photo), not an input variant. - - It's possible that sometimes you can't fetch the entity - from its input (since you can get errors like - ``ChannelPrivateError``) but you already have it through - another call, like getting a forwarded message from it. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - and returned as a bytestring (i.e. ``file=bytes``, without - parentheses or quotes). - - download_big (`bool`, optional): - Whether to use the big version of the available photos. - - Returns - `None` if no photo was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. - - Example - .. code-block:: python - - # Download your own profile photo - path = await client.download_profile_photo('me') - print(path) - """ - # hex(crc32(x.encode('ascii'))) for x in - # ('User', 'Chat', 'UserFull', 'ChatFull') - ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) - # ('InputPeer', 'InputUser', 'InputChannel') - INPUTS = (0xc91c90b6, 0xe669bf46, 0x40f202fd) - if not isinstance(entity, TLObject) or entity.SUBCLASS_OF_ID in INPUTS: - entity = await self.get_entity(entity) - - thumb = -1 if download_big else 0 - - possible_names = [] - if entity.SUBCLASS_OF_ID not in ENTITIES: - photo = entity - else: - if not hasattr(entity, 'photo'): - # Special case: may be a ChatFull with photo:Photo - # This is different from a normal UserProfilePhoto and Chat - if not hasattr(entity, 'chat_photo'): - return None - - return await self._download_photo( - entity.chat_photo, file, date=None, - thumb=thumb, progress_callback=None - ) - - for attr in ('username', 'first_name', 'title'): - possible_names.append(getattr(entity, attr, None)) - - photo = entity.photo - - if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)): - dc_id = photo.dc_id - loc = types.InputPeerPhotoFileLocation( - # min users can be used to download profile photos - # self.get_input_entity would otherwise not accept those - peer=utils.get_input_peer(entity, check_hash=False), - photo_id=photo.photo_id, - big=download_big - ) - else: - # It doesn't make any sense to check if `photo` can be used - # as input location, because then this method would be able - # to "download the profile photo of a message", i.e. its - # media which should be done with `download_media` instead. - return None - - file = self._get_proper_filename( - file, 'profile_photo', '.jpg', - possible_names=possible_names - ) - - try: - result = await self.download_file(loc, file, dc_id=dc_id) - return result if file is bytes else file - except errors.LocationInvalidError: - # See issue #500, Android app fails as of v4.6.0 (1155). - # The fix seems to be using the full channel chat photo. - ie = await self.get_input_entity(entity) - ty = helpers._entity_type(ie) - if ty == helpers._EntityType.CHANNEL: - full = await self(functions.channels.GetFullChannelRequest(ie)) - return await self._download_photo( - full.full_chat.chat_photo, file, - date=None, progress_callback=None, - thumb=thumb - ) - else: - # Until there's a report for chats, no need to. - return None - - async def download_media( - self: 'TelegramClient', - message: 'hints.MessageLike', - file: 'hints.FileLike' = None, - *, - thumb: 'typing.Union[int, types.TypePhotoSize]' = None, - progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: - """ - Downloads the given media from a message object. - - Note that if the download is too slow, you should consider installing - ``cryptg`` (through ``pip install cryptg``) so that decrypting the - received data is done in C instead of Python (much faster). - - See also `Message.download_media() `. - - Arguments - message (`Message ` | :tl:`Media`): - The media or message containing the media that will be downloaded. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - If file is the type `bytes`, it will be downloaded in-memory - and returned as a bytestring (i.e. ``file=bytes``, without - parentheses or quotes). - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(received bytes, total)``. - - thumb (`int` | :tl:`PhotoSize`, optional): - Which thumbnail size from the document or photo to download, - instead of downloading the document or photo itself. - - If it's specified but the file does not have a thumbnail, - this method will return `None`. - - The parameter should be an integer index between ``0`` and - ``len(sizes)``. ``0`` will download the smallest thumbnail, - and ``len(sizes) - 1`` will download the largest thumbnail. - You can also use negative indices, which work the same as - they do in Python's `list`. - - You can also pass the :tl:`PhotoSize` instance to use. - Alternatively, the thumb size type `str` may be used. - - In short, use ``thumb=0`` if you want the smallest thumbnail - and ``thumb=-1`` if you want the largest thumbnail. - - .. note:: - The largest thumbnail may be a video instead of a photo, - as they are available since layer 116 and are bigger than - any of the photos. - - Returns - `None` if no media was provided, or if it was Empty. On success - the file path is returned since it may differ from the one given. - - Example - .. code-block:: python - - path = await client.download_media(message) - await client.download_media(message, filename) - # or - path = await message.download_media() - await message.download_media(filename) - - # Downloading to memory - blob = await client.download_media(message, bytes) - - # Printing download progress - def callback(current, total): - print('Downloaded', current, 'out of', total, - 'bytes: {:.2%}'.format(current / total)) - - await client.download_media(message, progress_callback=callback) - """ - # Downloading large documents may be slow enough to require a new file reference - # to be obtained mid-download. Store (input chat, message id) so that the message - # can be re-fetched. - msg_data = None - - # TODO This won't work for messageService - if isinstance(message, types.Message): - date = message.date - media = message.media - msg_data = (message.input_chat, message.id) if message.input_chat else None - else: - date = datetime.datetime.now() - media = message - - if isinstance(media, str): - media = utils.resolve_bot_file_id(media) - - if isinstance(media, types.MessageService): - if isinstance(message.action, - types.MessageActionChatEditPhoto): - media = media.photo - - if isinstance(media, types.MessageMediaWebPage): - if isinstance(media.webpage, types.WebPage): - media = media.webpage.document or media.webpage.photo - - if isinstance(media, (types.MessageMediaPhoto, types.Photo)): - return await self._download_photo( - media, file, date, thumb, progress_callback - ) - elif isinstance(media, (types.MessageMediaDocument, types.Document)): - return await self._download_document( - media, file, date, thumb, progress_callback, msg_data - ) - elif isinstance(media, types.MessageMediaContact) and thumb is None: - return self._download_contact( - media, file - ) - elif isinstance(media, (types.WebDocument, types.WebDocumentNoProxy)) and thumb is None: - return await self._download_web_document( - media, file, progress_callback - ) - - async def download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None) -> typing.Optional[bytes]: - """ - Low-level method to download files from their input location. - - .. note:: - - Generally, you should instead use `download_media`. - This method is intended to be a bit more low-level. - - Arguments - input_location (:tl:`InputFileLocation`): - The file location from which the file will be downloaded. - See `telethon.utils.get_input_location` source for a complete - list of supported types. - - file (`str` | `file`, optional): - The output file path, directory, or stream-like object. - If the path exists and is a file, it will be overwritten. - - If the file path is `None` or `bytes`, then the result - will be saved in memory and returned as `bytes`. - - part_size_kb (`int`, optional): - Chunk size when downloading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_size (`int`, optional): - The file size that is about to be downloaded, if known. - Only used if ``progress_callback`` is specified. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(downloaded bytes, total)``. Note that the - ``total`` is the provided ``file_size``. - - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. - - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied - - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied - - - Example - .. code-block:: python - - # Download a file and print its header - data = await client.download_file(input_file, bytes) - print(data[:16]) - """ - return await self._download_file( - input_location, - file, - part_size_kb=part_size_kb, - file_size=file_size, - progress_callback=progress_callback, - dc_id=dc_id, - key=key, - iv=iv, - ) - - async def _download_file( - self: 'TelegramClient', - input_location: 'hints.FileLike', - file: 'hints.OutFileLike' = None, - *, - part_size_kb: float = None, - file_size: int = None, - progress_callback: 'hints.ProgressCallback' = None, - dc_id: int = None, - key: bytes = None, - iv: bytes = None, - msg_data: tuple = None) -> typing.Optional[bytes]: - if not part_size_kb: - if not file_size: - part_size_kb = 64 # Reasonable default - else: - part_size_kb = utils.get_appropriated_part_size(file_size) - - part_size = int(part_size_kb * 1024) - if part_size % MIN_CHUNK_SIZE != 0: - raise ValueError( - 'The part size must be evenly divisible by 4096.') - - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - in_memory = file is None or file is bytes - if in_memory: - f = io.BytesIO() - elif isinstance(file, str): - # Ensure that we'll be able to download the media - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - try: - async for chunk in self._iter_download( - input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data): - if iv and key: - chunk = AES.decrypt_ige(chunk, key, iv) - r = f.write(chunk) - if inspect.isawaitable(r): - await r - - if progress_callback: - r = progress_callback(f.tell(), file_size) - if inspect.isawaitable(r): - await r - - # Not all IO objects have flush (see #1227) - if callable(getattr(f, 'flush', None)): - f.flush() - - if in_memory: - return f.getvalue() - finally: - if isinstance(file, str) or in_memory: - f.close() - - def iter_download( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - offset: int = 0, - stride: int = None, - limit: int = None, - chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, - file_size: int = None, - dc_id: int = None - ): - """ - Iterates over a file download, yielding chunks of the file. - - This method can be used to stream files in a more convenient - way, since it offers more control (pausing, resuming, etc.) - - .. note:: - - Using a value for `offset` or `stride` which is not a multiple - of the minimum allowed `request_size`, or if `chunk_size` is - different from `request_size`, the library will need to do a - bit more work to fetch the data in the way you intend it to. - - You normally shouldn't worry about this. - - Arguments - file (`hints.FileLike`): - The file of which contents you want to iterate over. - - offset (`int`, optional): - The offset in bytes into the file from where the - download should start. For example, if a file is - 1024KB long and you just want the last 512KB, you - would use ``offset=512 * 1024``. - - stride (`int`, optional): - The stride of each chunk (how much the offset should - advance between reading each chunk). This parameter - should only be used for more advanced use cases. - - It must be bigger than or equal to the `chunk_size`. - - limit (`int`, optional): - The limit for how many *chunks* will be yielded at most. - - chunk_size (`int`, optional): - The maximum size of the chunks that will be yielded. - Note that the last chunk may be less than this value. - By default, it equals to `request_size`. - - request_size (`int`, optional): - How many bytes will be requested to Telegram when more - data is required. By default, as many bytes as possible - are requested. If you would like to request data in - smaller sizes, adjust this parameter. - - Note that values outside the valid range will be clamped, - and the final value will also be a multiple of the minimum - allowed size. - - file_size (`int`, optional): - If the file size is known beforehand, you should set - this parameter to said value. Depending on the type of - the input file passed, this may be set automatically. - - dc_id (`int`, optional): - The data center the library should connect to in order - to download the file. You shouldn't worry about this. - - Yields - - `bytes` objects representing the chunks of the file if the - right conditions are met, or `memoryview` objects instead. - - Example - .. code-block:: python - - # Streaming `media` to an output file - # After the iteration ends, the sender is cleaned up - with open('photo.jpg', 'wb') as fd: - async for chunk in client.iter_download(media): - fd.write(chunk) - - # Fetching only the header of a file (32 bytes) - # You should manually close the iterator in this case. - # - # "stream" is a common name for asynchronous generators, - # and iter_download will yield `bytes` (chunks of the file). - stream = client.iter_download(media, request_size=32) - header = await stream.__anext__() # "manual" version of `async for` - await stream.close() - assert len(header) == 32 - """ - return self._iter_download( - file, - offset=offset, - stride=stride, - limit=limit, - chunk_size=chunk_size, - request_size=request_size, - file_size=file_size, - dc_id=dc_id, - ) - - def _iter_download( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - offset: int = 0, - stride: int = None, - limit: int = None, - chunk_size: int = None, - request_size: int = MAX_CHUNK_SIZE, - file_size: int = None, - dc_id: int = None, - msg_data: tuple = None - ): - info = utils._get_file_info(file) - if info.dc_id is not None: - dc_id = info.dc_id - - if file_size is None: - file_size = info.size - - file = info.location - - if chunk_size is None: - chunk_size = request_size - - if limit is None and file_size is not None: - limit = (file_size + chunk_size - 1) // chunk_size - - if stride is None: - stride = chunk_size - elif stride < chunk_size: - raise ValueError('stride must be >= chunk_size') - - request_size -= request_size % MIN_CHUNK_SIZE - if request_size < MIN_CHUNK_SIZE: - request_size = MIN_CHUNK_SIZE - elif request_size > MAX_CHUNK_SIZE: - request_size = MAX_CHUNK_SIZE - - if chunk_size == request_size \ - and offset % MIN_CHUNK_SIZE == 0 \ - and stride % MIN_CHUNK_SIZE == 0 \ - and (limit is None or offset % limit == 0): - cls = _DirectDownloadIter - self._log[__name__].info('Starting direct file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - else: - cls = _GenericDownloadIter - self._log[__name__].info('Starting indirect file download in chunks of ' - '%d at %d, stride %d', request_size, offset, stride) - - return cls( - self, - limit, - file=file, - dc_id=dc_id, - offset=offset, - stride=stride, - chunk_size=chunk_size, - request_size=request_size, - file_size=file_size, - msg_data=msg_data, - ) - - # endregion - - # region Private methods - - @staticmethod - def _get_thumb(thumbs, thumb): - # Seems Telegram has changed the order and put `PhotoStrippedSize` - # last while this is the smallest (layer 116). Ensure we have the - # sizes sorted correctly with a custom function. - def sort_thumbs(thumb): - if isinstance(thumb, types.PhotoStrippedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, types.PhotoCachedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, types.PhotoSize): - return 1, thumb.size - if isinstance(thumb, types.PhotoSizeProgressive): - return 1, max(thumb.sizes) - if isinstance(thumb, types.VideoSize): - return 2, thumb.size - - # Empty size or invalid should go last - return 0, 0 - - thumbs = list(sorted(thumbs, key=sort_thumbs)) - - for i in reversed(range(len(thumbs))): - # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually - # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this - # thumb size doesn't actually exist (#1655). - if isinstance(thumbs[i], types.PhotoPathSize): - thumbs.pop(i) - - if thumb is None: - return thumbs[-1] - elif isinstance(thumb, int): - return thumbs[thumb] - elif isinstance(thumb, str): - return next((t for t in thumbs if t.type == thumb), None) - elif isinstance(thumb, (types.PhotoSize, types.PhotoCachedSize, - types.PhotoStrippedSize, types.VideoSize)): - return thumb - else: - return None - - def _download_cached_photo_size(self: 'TelegramClient', size, file): - # No need to download anything, simply write the bytes - if isinstance(size, types.PhotoStrippedSize): - data = utils.stripped_photo_to_jpg(size.bytes) - else: - data = size.bytes - - if file is bytes: - return data - elif isinstance(file, str): - helpers.ensure_parent_dir_exists(file) - f = open(file, 'wb') - else: - f = file - - try: - f.write(data) - finally: - if isinstance(file, str): - f.close() - return file - - async def _download_photo(self: 'TelegramClient', photo, file, date, thumb, progress_callback): - """Specialized version of .download_media() for photos""" - # Determine the photo and its largest size - if isinstance(photo, types.MessageMediaPhoto): - photo = photo.photo - if not isinstance(photo, types.Photo): - return - - # Include video sizes here (but they may be None so provide an empty list) - size = self._get_thumb(photo.sizes + (photo.video_sizes or []), thumb) - if not size or isinstance(size, types.PhotoSizeEmpty): - return - - if isinstance(size, types.VideoSize): - file = self._get_proper_filename(file, 'video', '.mp4', date=date) - else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - - if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) - - if isinstance(size, types.PhotoSizeProgressive): - file_size = max(size.sizes) - else: - file_size = size.size - - result = await self.download_file( - types.InputPhotoFileLocation( - id=photo.id, - access_hash=photo.access_hash, - file_reference=photo.file_reference, - thumb_size=size.type - ), - file, - file_size=file_size, - progress_callback=progress_callback - ) - return result if file is bytes else file - - @staticmethod - def _get_kind_and_names(attributes): - """Gets kind and possible names for :tl:`DocumentAttribute`.""" - kind = 'document' - possible_names = [] - for attr in attributes: - if isinstance(attr, types.DocumentAttributeFilename): - possible_names.insert(0, attr.file_name) - - elif isinstance(attr, types.DocumentAttributeAudio): - kind = 'audio' - if attr.performer and attr.title: - possible_names.append('{} - {}'.format( - attr.performer, attr.title - )) - elif attr.performer: - possible_names.append(attr.performer) - elif attr.title: - possible_names.append(attr.title) - elif attr.voice: - kind = 'voice' - - return kind, possible_names - - async def _download_document( - self, document, file, date, thumb, progress_callback, msg_data): - """Specialized version of .download_media() for documents.""" - if isinstance(document, types.MessageMediaDocument): - document = document.document - if not isinstance(document, types.Document): - return - - if thumb is None: - kind, possible_names = self._get_kind_and_names(document.attributes) - file = self._get_proper_filename( - file, kind, utils.get_extension(document), - date=date, possible_names=possible_names - ) - size = None - else: - file = self._get_proper_filename(file, 'photo', '.jpg', date=date) - size = self._get_thumb(document.thumbs, thumb) - if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)): - return self._download_cached_photo_size(size, file) - - result = await self._download_file( - types.InputDocumentFileLocation( - id=document.id, - access_hash=document.access_hash, - file_reference=document.file_reference, - thumb_size=size.type if size else '' - ), - file, - file_size=size.size if size else document.size, - progress_callback=progress_callback, - msg_data=msg_data, - ) - - return result if file is bytes else file - - @classmethod - def _download_contact(cls, mm_contact, file): - """ - Specialized version of .download_media() for contacts. - Will make use of the vCard 4.0 format. - """ - first_name = mm_contact.first_name - last_name = mm_contact.last_name - phone_number = mm_contact.phone_number - - # Remove these pesky characters - first_name = first_name.replace(';', '') - last_name = (last_name or '').replace(';', '') - result = ( - 'BEGIN:VCARD\n' - 'VERSION:4.0\n' - 'N:{f};{l};;;\n' - 'FN:{f} {l}\n' - 'TEL;TYPE=cell;VALUE=uri:tel:+{p}\n' - 'END:VCARD\n' - ).format(f=first_name, l=last_name, p=phone_number).encode('utf-8') - - file = cls._get_proper_filename( - file, 'contact', '.vcard', - possible_names=[first_name, phone_number, last_name] - ) - if file is bytes: - return result - f = file if hasattr(file, 'write') else open(file, 'wb') - - try: - f.write(result) - finally: - # Only close the stream if we opened it - if f != file: - f.close() - - return file - - @classmethod - async def _download_web_document(cls, web, file, progress_callback): - """ - Specialized version of .download_media() for web documents. - """ - if not aiohttp: - raise ValueError( - 'Cannot download web documents without the aiohttp ' - 'dependency install it (pip install aiohttp)' - ) - - # TODO Better way to get opened handles of files and auto-close - kind, possible_names = self._get_kind_and_names(web.attributes) - file = self._get_proper_filename( - file, kind, utils.get_extension(web), - possible_names=possible_names - ) - if file is bytes: - f = io.BytesIO() - elif hasattr(file, 'write'): - f = file - else: - f = open(file, 'wb') - - try: - async with aiohttp.ClientSession() as session: - # TODO Use progress_callback; get content length from response - # https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319 - async with session.get(web.url) as response: - while True: - chunk = await response.content.read(128 * 1024) - if not chunk: - break - f.write(chunk) - finally: - if f != file: - f.close() - - return f.getvalue() if file is bytes else file - - @staticmethod - def _get_proper_filename(file, kind, extension, - date=None, possible_names=None): - """Gets a proper filename for 'file', if this is a path. - - 'kind' should be the kind of the output file (photo, document...) - 'extension' should be the extension to be added to the file if - the filename doesn't have any yet - 'date' should be when this file was originally sent, if known - 'possible_names' should be an ordered list of possible names - - If no modification is made to the path, any existing file - will be overwritten. - If any modification is made to the path, this method will - ensure that no existing file will be overwritten. - """ - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - if file is not None and not isinstance(file, str): - # Probably a stream-like object, we cannot set a filename here - return file - - if file is None: - file = '' - elif os.path.isfile(file): - # Make no modifications to valid existing paths - return file - - if os.path.isdir(file) or not file: - try: - name = None if possible_names is None else next( - x for x in possible_names if x - ) - except StopIteration: - name = None - - if not name: - if not date: - date = datetime.datetime.now() - name = '{}_{}-{:02}-{:02}_{:02}-{:02}-{:02}'.format( - kind, - date.year, date.month, date.day, - date.hour, date.minute, date.second, - ) - file = os.path.join(file, name) - - directory, name = os.path.split(file) - name, ext = os.path.splitext(name) - if not ext: - ext = extension - - result = os.path.join(directory, name + ext) - if not os.path.isfile(result): - return result - - i = 1 - while True: - result = os.path.join(directory, '{} ({}){}'.format(name, i, ext)) - if not os.path.isfile(result): - return result - i += 1 - - # endregion diff --git a/telethon/client/messageparse.py b/telethon/client/messageparse.py deleted file mode 100644 index 4ddfa523..00000000 --- a/telethon/client/messageparse.py +++ /dev/null @@ -1,233 +0,0 @@ -import itertools -import re -import typing - -from .. import helpers, utils -from ..tl import types - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class MessageParseMethods: - - # region Public properties - - @property - def parse_mode(self: 'TelegramClient'): - """ - This property is the default parse mode used when sending messages. - Defaults to `telethon.extensions.markdown`. It will always - be either `None` or an object with ``parse`` and ``unparse`` - methods. - - When setting a different value it should be one of: - - * Object with ``parse`` and ``unparse`` methods. - * A ``callable`` to act as the parse method. - * A `str` indicating the ``parse_mode``. For Markdown ``'md'`` - or ``'markdown'`` may be used. For HTML, ``'htm'`` or ``'html'`` - may be used. - - The ``parse`` method should be a function accepting a single - parameter, the text to parse, and returning a tuple consisting - of ``(parsed message str, [MessageEntity instances])``. - - The ``unparse`` method should be the inverse of ``parse`` such - that ``assert text == unparse(*parse(text))``. - - See :tl:`MessageEntity` for allowed message entities. - - Example - .. code-block:: python - - # Disabling default formatting - client.parse_mode = None - - # Enabling HTML as the default format - client.parse_mode = 'html' - """ - return self._parse_mode - - @parse_mode.setter - def parse_mode(self: 'TelegramClient', mode: str): - self._parse_mode = utils.sanitize_parse_mode(mode) - - # endregion - - # region Private methods - - async def _replace_with_mention(self: 'TelegramClient', entities, i, user): - """ - Helper method to replace ``entities[i]`` to mention ``user``, - or do nothing if it can't be found. - """ - try: - entities[i] = types.InputMessageEntityMentionName( - entities[i].offset, entities[i].length, - await self.get_input_entity(user) - ) - return True - except (ValueError, TypeError): - return False - - async def _parse_message_text(self: 'TelegramClient', message, parse_mode): - """ - Returns a (parsed message, entities) tuple depending on ``parse_mode``. - """ - if parse_mode == (): - parse_mode = self._parse_mode - else: - parse_mode = utils.sanitize_parse_mode(parse_mode) - - if not parse_mode: - return message, [] - - original_message = message - message, msg_entities = parse_mode.parse(message) - if original_message and not message and not msg_entities: - raise ValueError("Failed to parse message") - - for i in reversed(range(len(msg_entities))): - e = msg_entities[i] - if not e.length: - # 0-length MessageEntity is no longer valid #3884. - # Because the user can provide their own parser (with reasonable 0-length - # entities), strip them here rather than fixing the built-in parsers. - del msg_entities[i] - elif isinstance(e, types.MessageEntityTextUrl): - m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url) - if m: - user = int(m.group(1)) if m.group(1) else e.url - is_mention = await self._replace_with_mention(msg_entities, i, user) - if not is_mention: - del msg_entities[i] - elif isinstance(e, (types.MessageEntityMentionName, - types.InputMessageEntityMentionName)): - is_mention = await self._replace_with_mention(msg_entities, i, e.user_id) - if not is_mention: - del msg_entities[i] - - return message, msg_entities - - def _get_response_message(self: 'TelegramClient', request, result, input_chat): - """ - Extracts the response message known a request and Update result. - The request may also be the ID of the message to match. - - If ``request is None`` this method returns ``{id: message}``. - - If ``request.random_id`` is a list, this method returns a list too. - """ - if isinstance(result, types.UpdateShort): - updates = [result.update] - entities = {} - elif isinstance(result, (types.Updates, types.UpdatesCombined)): - updates = result.updates - entities = {utils.get_peer_id(x): x - for x in - itertools.chain(result.users, result.chats)} - else: - return None - - random_to_id = {} - id_to_message = {} - for update in updates: - if isinstance(update, types.UpdateMessageID): - random_to_id[update.random_id] = update.id - - elif isinstance(update, ( - types.UpdateNewChannelMessage, types.UpdateNewMessage)): - update.message._finish_init(self, entities, input_chat) - - # Pinning a message with `updatePinnedMessage` seems to - # always produce a service message we can't map so return - # it directly. The same happens for kicking users. - # - # It could also be a list (e.g. when sending albums). - # - # TODO this method is getting messier and messier as time goes on - if hasattr(request, 'random_id') or utils.is_list_like(request): - id_to_message[update.message.id] = update.message - else: - return update.message - - elif (isinstance(update, types.UpdateEditMessage) - and helpers._entity_type(request.peer) != helpers._EntityType.CHANNEL): - update.message._finish_init(self, entities, input_chat) - - # Live locations use `sendMedia` but Telegram responds with - # `updateEditMessage`, which means we won't have `id` field. - if hasattr(request, 'random_id'): - id_to_message[update.message.id] = update.message - elif request.id == update.message.id: - return update.message - - elif (isinstance(update, types.UpdateEditChannelMessage) - and utils.get_peer_id(request.peer) == - utils.get_peer_id(update.message.peer_id)): - if request.id == update.message.id: - update.message._finish_init(self, entities, input_chat) - return update.message - - elif isinstance(update, types.UpdateNewScheduledMessage): - update.message._finish_init(self, entities, input_chat) - # Scheduled IDs may collide with normal IDs. However, for a - # single request there *shouldn't* be a mix between "some - # scheduled and some not". - id_to_message[update.message.id] = update.message - - elif isinstance(update, types.UpdateMessagePoll): - if request.media.poll.id == update.poll_id: - m = types.Message( - id=request.id, - peer_id=utils.get_peer(request.peer), - media=types.MessageMediaPoll( - poll=update.poll, - results=update.results - ) - ) - m._finish_init(self, entities, input_chat) - return m - - if request is None: - return id_to_message - - random_id = request if isinstance(request, (int, list)) else getattr(request, 'random_id', None) - if random_id is None: - # Can happen when pinning a message does not actually produce a service message. - self._log[__name__].warning( - 'No random_id in %s to map to, returning None message for %s', request, result) - return None - - if not utils.is_list_like(random_id): - msg = id_to_message.get(random_to_id.get(random_id)) - - if not msg: - self._log[__name__].warning( - 'Request %s had missing message mapping %s', request, result) - - return msg - - try: - return [id_to_message[random_to_id[rnd]] for rnd in random_id] - except KeyError: - # Sometimes forwards fail (`MESSAGE_ID_INVALID` if a message gets - # deleted or `WORKER_BUSY_TOO_LONG_RETRY` if there are issues at - # Telegram), in which case we get some "missing" message mappings. - # Log them with the hope that we can better work around them. - # - # This also happens when trying to forward messages that can't - # be forwarded because they don't exist (0, service, deleted) - # among others which could be (like deleted or existing). - self._log[__name__].warning( - 'Request %s had missing message mappings %s', request, result) - - return [ - id_to_message.get(random_to_id[rnd]) - if rnd in random_to_id - else None - for rnd in random_id - ] - - # endregion diff --git a/telethon/client/messages.py b/telethon/client/messages.py deleted file mode 100644 index f474dc90..00000000 --- a/telethon/client/messages.py +++ /dev/null @@ -1,1490 +0,0 @@ -import inspect -import itertools -import typing -import warnings - -from .. import helpers, utils, errors, hints -from ..requestiter import RequestIter -from ..tl import types, functions - -_MAX_CHUNK_SIZE = 100 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class _MessagesIter(RequestIter): - """ - Common factor for all requests that need to iterate over messages. - """ - async def _init( - self, entity, offset_id, min_id, max_id, - from_user, offset_date, add_offset, filter, search, reply_to, - scheduled - ): - # Note that entity being `None` will perform a global search. - if entity: - self.entity = await self.client.get_input_entity(entity) - else: - self.entity = None - if self.reverse: - raise ValueError('Cannot reverse global search') - - # Telegram doesn't like min_id/max_id. If these IDs are low enough - # (starting from last_id - 100), the request will return nothing. - # - # We can emulate their behaviour locally by setting offset = max_id - # and simply stopping once we hit a message with ID <= min_id. - if self.reverse: - offset_id = max(offset_id, min_id) - if offset_id and max_id: - if max_id - offset_id <= 1: - raise StopAsyncIteration - - if not max_id: - max_id = float('inf') - else: - offset_id = max(offset_id, max_id) - if offset_id and min_id: - if offset_id - min_id <= 1: - raise StopAsyncIteration - - if self.reverse: - if offset_id: - offset_id += 1 - elif not offset_date: - # offset_id has priority over offset_date, so don't - # set offset_id to 1 if we want to offset by date. - offset_id = 1 - - if from_user: - from_user = await self.client.get_input_entity(from_user) - self.from_id = await self.client.get_peer_id(from_user) - else: - self.from_id = None - - # `messages.searchGlobal` only works with text `search` or `filter` queries. - # If we want to perform global a search with `from_user` we have to perform - # a normal `messages.search`, *but* we can make the entity be `inputPeerEmpty`. - if not self.entity and from_user: - self.entity = types.InputPeerEmpty() - - if filter is None: - filter = types.InputMessagesFilterEmpty() - else: - filter = filter() if isinstance(filter, type) else filter - - if not self.entity: - self.request = functions.messages.SearchGlobalRequest( - q=search or '', - filter=filter, - min_date=None, - max_date=offset_date, - offset_rate=0, - offset_peer=types.InputPeerEmpty(), - offset_id=offset_id, - limit=1 - ) - elif scheduled: - self.request = functions.messages.GetScheduledHistoryRequest( - peer=entity, - hash=0 - ) - elif reply_to is not None: - self.request = functions.messages.GetRepliesRequest( - peer=self.entity, - msg_id=reply_to, - offset_id=offset_id, - offset_date=offset_date, - add_offset=add_offset, - limit=1, - max_id=0, - min_id=0, - hash=0 - ) - elif search is not None or not isinstance(filter, types.InputMessagesFilterEmpty) or from_user: - # Telegram completely ignores `from_id` in private chats - ty = helpers._entity_type(self.entity) - if ty == helpers._EntityType.USER: - # Don't bother sending `from_user` (it's ignored anyway), - # but keep `from_id` defined above to check it locally. - from_user = None - else: - # Do send `from_user` to do the filtering server-side, - # and set `from_id` to None to avoid checking it locally. - self.from_id = None - - self.request = functions.messages.SearchRequest( - peer=self.entity, - q=search or '', - filter=filter, - min_date=None, - max_date=offset_date, - offset_id=offset_id, - add_offset=add_offset, - limit=0, # Search actually returns 0 items if we ask it to - max_id=0, - min_id=0, - hash=0, - from_id=from_user - ) - - # Workaround issue #1124 until a better solution is found. - # Telegram seemingly ignores `max_date` if `filter` (and - # nothing else) is specified, so we have to rely on doing - # a first request to offset from the ID instead. - # - # Even better, using `filter` and `from_id` seems to always - # trigger `RPC_CALL_FAIL` which is "internal issues"... - if not isinstance(filter, types.InputMessagesFilterEmpty) \ - and offset_date and not search and not offset_id: - async for m in self.client.iter_messages( - self.entity, 1, offset_date=offset_date): - self.request.offset_id = m.id + 1 - else: - self.request = functions.messages.GetHistoryRequest( - peer=self.entity, - limit=1, - offset_date=offset_date, - offset_id=offset_id, - min_id=0, - max_id=0, - add_offset=add_offset, - hash=0 - ) - - if self.limit <= 0: - # No messages, but we still need to know the total message count - result = await self.client(self.request) - if isinstance(result, types.messages.MessagesNotModified): - self.total = result.count - else: - self.total = getattr(result, 'count', len(result.messages)) - raise StopAsyncIteration - - if self.wait_time is None: - self.wait_time = 1 if self.limit > 3000 else 0 - - # When going in reverse we need an offset of `-limit`, but we - # also want to respect what the user passed, so add them together. - if self.reverse: - self.request.add_offset -= _MAX_CHUNK_SIZE - - self.add_offset = add_offset - self.max_id = max_id - self.min_id = min_id - self.last_id = 0 if self.reverse else float('inf') - - async def _load_next_chunk(self): - self.request.limit = min(self.left, _MAX_CHUNK_SIZE) - if self.reverse and self.request.limit != _MAX_CHUNK_SIZE: - # Remember that we need -limit when going in reverse - self.request.add_offset = self.add_offset - self.request.limit - - r = await self.client(self.request) - self.total = getattr(r, 'count', len(r.messages)) - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - messages = reversed(r.messages) if self.reverse else r.messages - for message in messages: - if (isinstance(message, types.MessageEmpty) - or self.from_id and message.sender_id != self.from_id): - continue - - if not self._message_in_range(message): - return True - - # There has been reports that on bad connections this method - # was returning duplicated IDs sometimes. Using ``last_id`` - # is an attempt to avoid these duplicates, since the message - # IDs are returned in descending order (or asc if reverse). - self.last_id = message.id - message._finish_init(self.client, entities, self.entity) - self.buffer.append(message) - - # Not a slice (using offset would return the same, with e.g. SearchGlobal). - if isinstance(r, types.messages.Messages): - return True - - # Some channels are "buggy" and may return less messages than - # requested (apparently, the messages excluded are, for example, - # "not displayable due to local laws"). - # - # This means it's not safe to rely on `len(r.messages) < req.limit` as - # the stop condition. Unfortunately more requests must be made. - # - # However we can still check if the highest ID is equal to or lower - # than the limit, in which case there won't be any more messages - # because the lowest message ID is 1. - # - # We also assume the API will always return, at least, one message if - # there is more to fetch. - if not r.messages or r.messages[0].id <= self.request.limit: - return True - - # Get the last message that's not empty (in some rare cases - # it can happen that the last message is :tl:`MessageEmpty`) - if self.buffer: - self._update_offset(self.buffer[-1], r) - else: - # There are some cases where all the messages we get start - # being empty. This can happen on migrated mega-groups if - # the history was cleared, and we're using search. Telegram - # acts incredibly weird sometimes. Messages are returned but - # only "empty", not their contents. If this is the case we - # should just give up since there won't be any new Message. - return True - - def _message_in_range(self, message): - """ - Determine whether the given message is in the range or - it should be ignored (and avoid loading more chunks). - """ - # No entity means message IDs between chats may vary - if self.entity: - if self.reverse: - if message.id <= self.last_id or message.id >= self.max_id: - return False - else: - if message.id >= self.last_id or message.id <= self.min_id: - return False - - return True - - def _update_offset(self, last_message, response): - """ - After making the request, update its offset with the last message. - """ - self.request.offset_id = last_message.id - if self.reverse: - # We want to skip the one we already have - self.request.offset_id += 1 - - if isinstance(self.request, functions.messages.SearchRequest): - # Unlike getHistory and searchGlobal that use *offset* date, - # this is *max* date. This means that doing a search in reverse - # will break it. Since it's not really needed once we're going - # (only for the first request), it's safe to just clear it off. - self.request.max_date = None - else: - # getHistory, searchGlobal and getReplies call it offset_date - self.request.offset_date = last_message.date - - if isinstance(self.request, functions.messages.SearchGlobalRequest): - if last_message.input_chat: - self.request.offset_peer = last_message.input_chat - else: - self.request.offset_peer = types.InputPeerEmpty() - - self.request.offset_rate = getattr(response, 'next_rate', 0) - - -class _IDsIter(RequestIter): - async def _init(self, entity, ids): - self.total = len(ids) - self._ids = list(reversed(ids)) if self.reverse else ids - self._offset = 0 - self._entity = (await self.client.get_input_entity(entity)) if entity else None - self._ty = helpers._entity_type(self._entity) if self._entity else None - - # 30s flood wait every 300 messages (3 requests of 100 each, 30 of 10, etc.) - if self.wait_time is None: - self.wait_time = 10 if self.limit > 300 else 0 - - async def _load_next_chunk(self): - ids = self._ids[self._offset:self._offset + _MAX_CHUNK_SIZE] - if not ids: - raise StopAsyncIteration - - self._offset += _MAX_CHUNK_SIZE - - from_id = None # By default, no need to validate from_id - if self._ty == helpers._EntityType.CHANNEL: - try: - r = await self.client( - functions.channels.GetMessagesRequest(self._entity, ids)) - except errors.MessageIdsEmptyError: - # All IDs were invalid, use a dummy result - r = types.messages.MessagesNotModified(len(ids)) - else: - r = await self.client(functions.messages.GetMessagesRequest(ids)) - if self._entity: - from_id = await self.client._get_peer(self._entity) - - if isinstance(r, types.messages.MessagesNotModified): - self.buffer.extend(None for _ in ids) - return - - entities = {utils.get_peer_id(x): x - for x in itertools.chain(r.users, r.chats)} - - # Telegram seems to return the messages in the order in which - # we asked them for, so we don't need to check it ourselves, - # unless some messages were invalid in which case Telegram - # may decide to not send them at all. - # - # The passed message IDs may not belong to the desired entity - # since the user can enter arbitrary numbers which can belong to - # arbitrary chats. Validate these unless ``from_id is None``. - for message in r.messages: - if isinstance(message, types.MessageEmpty) or ( - from_id and message.peer_id != from_id): - self.buffer.append(None) - else: - message._finish_init(self.client, entities, self._entity) - self.buffer.append(message) - - -class MessageMethods: - - # region Public methods - - # region Message retrieval - - def iter_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - limit: float = None, - *, - offset_date: 'hints.DateLike' = None, - offset_id: int = 0, - max_id: int = 0, - min_id: int = 0, - add_offset: int = 0, - search: str = None, - filter: 'typing.Union[types.TypeMessagesFilter, typing.Type[types.TypeMessagesFilter]]' = None, - from_user: 'hints.EntityLike' = None, - wait_time: float = None, - ids: 'typing.Union[int, typing.Sequence[int]]' = None, - reverse: bool = False, - reply_to: int = None, - scheduled: bool = False - ) -> 'typing.Union[_MessagesIter, _IDsIter]': - """ - Iterator over the messages for the given chat. - - The default order is from newest to oldest, but this - behaviour can be changed with the `reverse` parameter. - - If either `search`, `filter` or `from_user` are provided, - :tl:`messages.Search` will be used instead of :tl:`messages.getHistory`. - - .. note:: - - Telegram's flood wait limit for :tl:`GetHistoryRequest` seems to - be around 30 seconds per 10 requests, therefore a sleep of 1 - second is the default for this limit (or above). - - Arguments - entity (`entity`): - The entity from whom to retrieve the message history. - - It may be `None` to perform a global search, or - to get messages by their ID from no particular chat. - Note that some of the offsets will not work if this - is the case. - - Note that if you want to perform a global search, - you **must** set a non-empty `search` string, a `filter`. - or `from_user`. - - limit (`int` | `None`, optional): - Number of messages to be retrieved. Due to limitations with - the API retrieving more than 3000 messages will take longer - than half a minute (or even more based on previous calls). - - The limit may also be `None`, which would eventually return - the whole history. - - offset_date (`datetime`): - Offset date (messages *previous* to this date will be - retrieved). Exclusive. - - offset_id (`int`): - Offset message ID (only messages *previous* to the given - ID will be retrieved). Exclusive. - - max_id (`int`): - All the messages with a higher (newer) ID or equal to this will - be excluded. - - min_id (`int`): - All the messages with a lower (older) ID or equal to this will - be excluded. - - add_offset (`int`): - Additional message offset (all of the specified offsets + - this offset = older messages). - - search (`str`): - The string to be used as a search query. - - filter (:tl:`MessagesFilter` | `type`): - The filter to use when returning messages. For instance, - :tl:`InputMessagesFilterPhotos` would yield only messages - containing photos. - - from_user (`entity`): - Only messages from this entity will be returned. - - wait_time (`int`): - Wait time (in seconds) between different - :tl:`GetHistoryRequest`. Use this parameter to avoid hitting - the ``FloodWaitError`` as needed. If left to `None`, it will - default to 1 second only if the limit is higher than 3000. - - If the ``ids`` parameter is used, this time will default - to 10 seconds only if the amount of IDs is higher than 300. - - ids (`int`, `list`): - A single integer ID (or several IDs) for the message that - should be returned. This parameter takes precedence over - the rest (which will be ignored if this is set). This can - for instance be used to get the message with ID 123 from - a channel. Note that if the message doesn't exist, `None` - will appear in its place, so that zipping the list of IDs - with the messages can match one-to-one. - - .. note:: - - At the time of writing, Telegram will **not** return - :tl:`MessageEmpty` for :tl:`InputMessageReplyTo` IDs that - failed (i.e. the message is not replying to any, or is - replying to a deleted message). This means that it is - **not** possible to match messages one-by-one, so be - careful if you use non-integers in this parameter. - - reverse (`bool`, optional): - If set to `True`, the messages will be returned in reverse - order (from oldest to newest, instead of the default newest - to oldest). This also means that the meaning of `offset_id` - and `offset_date` parameters is reversed, although they will - still be exclusive. `min_id` becomes equivalent to `offset_id` - instead of being `max_id` as well since messages are returned - in ascending order. - - You cannot use this if both `entity` and `ids` are `None`. - - reply_to (`int`, optional): - If set to a message ID, the messages that reply to this ID - will be returned. This feature is also known as comments in - posts of broadcast channels, or viewing threads in groups. - - This feature can only be used in broadcast channels and their - linked megagroups. Using it in a chat or private conversation - will result in ``telethon.errors.PeerIdInvalidError`` to occur. - - When using this parameter, the ``filter`` and ``search`` - parameters have no effect, since Telegram's API doesn't - support searching messages in replies. - - .. note:: - - This feature is used to get replies to a message in the - *discussion* group. If the same broadcast channel sends - a message and replies to it itself, that reply will not - be included in the results. - - scheduled (`bool`, optional): - If set to `True`, messages which are scheduled will be returned. - All other parameter will be ignored for this, except `entity`. - - Yields - Instances of `Message `. - - Example - .. code-block:: python - - # From most-recent to oldest - async for message in client.iter_messages(chat): - print(message.id, message.text) - - # From oldest to most-recent - async for message in client.iter_messages(chat, reverse=True): - print(message.id, message.text) - - # Filter by sender - async for message in client.iter_messages(chat, from_user='me'): - print(message.text) - - # Server-side search with fuzzy text - async for message in client.iter_messages(chat, search='hello'): - print(message.id) - - # Filter by message type: - from telethon.tl.types import InputMessagesFilterPhotos - async for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos): - print(message.photo) - - # Getting comments from a post in a channel: - async for message in client.iter_messages(channel, reply_to=123): - print(message.chat.title, message.text) - """ - if ids is not None: - if not utils.is_list_like(ids): - ids = [ids] - - return _IDsIter( - client=self, - reverse=reverse, - wait_time=wait_time, - limit=len(ids), - entity=entity, - ids=ids - ) - - return _MessagesIter( - client=self, - reverse=reverse, - wait_time=wait_time, - limit=limit, - entity=entity, - offset_id=offset_id, - min_id=min_id, - max_id=max_id, - from_user=from_user, - offset_date=offset_date, - add_offset=add_offset, - filter=filter, - search=search, - reply_to=reply_to, - scheduled=scheduled - ) - - async def get_messages(self: 'TelegramClient', *args, **kwargs) -> '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 = await client.get_messages(chat, 0, filter=InputMessagesFilterPhotos) - print(photos.total) - - # Get all the photos - photos = await client.get_messages(chat, None, filter=InputMessagesFilterPhotos) - - # Get messages by ID: - message_1337 = await client.get_messages(chat, ids=1337) - """ - if len(args) == 1 and 'limit' not in kwargs: - if 'min_id' in kwargs and 'max_id' in kwargs: - kwargs['limit'] = None - else: - kwargs['limit'] = 1 - - it = self.iter_messages(*args, **kwargs) - - ids = kwargs.get('ids') - if ids and not utils.is_list_like(ids): - async for message in it: - return message - else: - # Iterator exhausted = empty, to handle InputMessageReplyTo - return None - - return await it.collect() - - get_messages.__signature__ = inspect.signature(iter_messages) - - # endregion - - # region Message sending/editing/deleting - - async def _get_comment_data( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[int, types.Message]' - ): - r = await self(functions.messages.GetDiscussionMessageRequest( - peer=entity, - msg_id=utils.get_message_id(message) - )) - m = min(r.messages, key=lambda msg: msg.id) - chat = next(c for c in r.chats if c.id == m.peer_id.channel_id) - return utils.get_input_peer(chat), m.id - - async def send_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'hints.MessageLike' = '', - *, - reply_to: 'typing.Union[int, types.Message]' = None, - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - parse_mode: typing.Optional[str] = (), - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'typing.Union[hints.FileLike, typing.Sequence[hints.FileLike]]' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - clear_draft: bool = False, - buttons: typing.Optional['hints.MarkupLike'] = None, - silent: bool = None, - background: bool = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, types.Message]' = None, - nosound_video: bool = None, - ) -> 'types.Message': - """ - Sends a message to the specified user, chat or channel. - - The default parse mode is the same as the official applications - (a custom flavour of markdown). ``**bold**, `code` or __italic__`` - are available. In addition you can send ``[links](https://example.com)`` - and ``[mentions](@username)`` (or using IDs like in the Bot API: - ``[mention](tg://user?id=123456789)``) and ``pre`` blocks with three - backticks. - - Sending a ``/start`` command with a parameter (like ``?start=data``) - is also done through this method. Simply send ``'/start data'`` to - the bot. - - See also `Message.respond() ` - and `Message.reply() `. - - Arguments - entity (`entity`): - To who will it be sent. - - message (`str` | `Message `): - The message to be sent, or another message object to resend. - - The maximum length for a message is 35,000 bytes or 4,096 - characters. Longer messages will not be sliced automatically, - and you should slice them manually if the text to send is - longer than said length. - - reply_to (`int` | `Message `, optional): - Whether to reply to a message or not. If an integer is provided, - it should be the ID of the message that it should reply to. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`file`, optional): - Sends a message with a file attached (e.g. a photo, - video, audio or document). The ``message`` may be empty. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - force_document (`bool`, optional): - Whether to send the given file as a document or not. - - clear_draft (`bool`, optional): - Whether the existing draft should be cleared or not. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - All the following limits apply together: - - * There can be 100 buttons at most (any more are ignored). - * There can be 8 buttons per row at most (more are ignored). - * The maximum callback data per button is 64 bytes. - * The maximum data that can be embedded in total is just - over 4KB, shared between inline callback data and text. - - silent (`bool`, optional): - Whether the message should notify people in a broadcast - channel or not. Defaults to `False`, which means it will - notify them. Set it to `True` to alter this behaviour. - - background (`bool`, optional): - Whether the message should be send in background. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the message won't send immediately, and instead - it will be scheduled to be automatically sent at a later - time. - - comment_to (`int` | `Message `, optional): - Similar to ``reply_to``, but replies in the linked group of a - broadcast channel instead (effectively leaving a "comment to" - the specified message). - - This parameter takes precedence over ``reply_to``. If there is - no linked chat, `telethon.errors.sgIdInvalidError` is raised. - - nosound_video (`bool`, optional): - Only applicable when sending a video file without an audio - track. If set to ``True``, the video will be displayed in - Telegram as a video. If set to ``False``, Telegram will attempt - to display the video as an animated gif. (It may still display - as a video due to other factors.) The value is ignored if set - on non-video files. This is set to ``True`` for albums, as gifs - cannot be sent in albums. - - Returns - The sent `custom.Message `. - - Example - .. code-block:: python - - # Markdown is the default - await client.send_message('me', 'Hello **world**!') - - # Default to another parse mode - client.parse_mode = 'html' - - await client.send_message('me', 'Some bold and italic text') - await client.send_message('me', 'An URL') - # code and pre tags also work, but those break the documentation :) - await client.send_message('me', 'Mentions') - - # Explicit parse mode - # No parse mode by default - client.parse_mode = None - - # ...but here I want markdown - await client.send_message('me', 'Hello, **world**!', parse_mode='md') - - # ...and here I need HTML - await client.send_message('me', 'Hello, world!', parse_mode='html') - - # If you logged in as a bot account, you can send buttons - from telethon import events, Button - - @client.on(events.CallbackQuery) - async def callback(event): - await event.edit('Thank you for clicking {}!'.format(event.data)) - - # Single inline button - await client.send_message(chat, 'A single button, with "clk1" as data', - buttons=Button.inline('Click me', b'clk1')) - - # Matrix of inline buttons - await client.send_message(chat, 'Pick one from this grid', buttons=[ - [Button.inline('Left'), Button.inline('Right')], - [Button.url('Check this site!', 'https://example.com')] - ]) - - # Reply keyboard - await client.send_message(chat, 'Welcome', buttons=[ - Button.text('Thanks!', resize=True, single_use=True), - Button.request_phone('Send phone'), - Button.request_location('Send location') - ]) - - # Forcing replies or clearing buttons. - await client.send_message(chat, 'Reply to me', buttons=Button.force_reply()) - await client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear()) - - # Scheduling a message to be sent after 5 minutes - from datetime import timedelta - await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5)) - """ - if file is not None: - return await self.send_file( - entity, file, caption=message, reply_to=reply_to, - attributes=attributes, parse_mode=parse_mode, - force_document=force_document, thumb=thumb, - buttons=buttons, clear_draft=clear_draft, silent=silent, - schedule=schedule, supports_streaming=supports_streaming, - formatting_entities=formatting_entities, - comment_to=comment_to, background=background, - nosound_video=nosound_video, - ) - - entity = await self.get_input_entity(entity) - if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) - - if isinstance(message, types.Message): - if buttons is None: - markup = message.reply_markup - else: - markup = self.build_reply_markup(buttons) - - if silent is None: - silent = message.silent - - if (message.media and not isinstance( - message.media, types.MessageMediaWebPage)): - return await self.send_file( - entity, - message.media, - caption=message.message, - silent=silent, - background=background, - reply_to=reply_to, - buttons=markup, - formatting_entities=message.entities, - parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities - schedule=schedule - ) - - request = functions.messages.SendMessageRequest( - peer=entity, - message=message.message or '', - silent=silent, - background=background, - reply_to_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), - schedule_date=schedule - ) - message = message.message - else: - if formatting_entities is None: - message, formatting_entities = await self._parse_message_text(message, parse_mode) - if not message: - raise ValueError( - 'The message cannot be empty unless a file is provided' - ) - - request = functions.messages.SendMessageRequest( - peer=entity, - message=message, - entities=formatting_entities, - no_webpage=not link_preview, - reply_to_msg_id=utils.get_message_id(reply_to), - clear_draft=clear_draft, - silent=silent, - background=background, - reply_markup=self.build_reply_markup(buttons), - schedule_date=schedule - ) - - result = await self(request) - if isinstance(result, types.UpdateShortSentMessage): - message = types.Message( - id=result.id, - peer_id=await self._get_peer(entity), - message=message, - date=result.date, - out=result.out, - media=result.media, - entities=result.entities, - reply_markup=request.reply_markup, - ttl_period=result.ttl_period, - reply_to=types.MessageReplyHeader(request.reply_to_msg_id) - ) - message._finish_init(self, {}, entity) - return message - - return self._get_response_message(request, result, entity) - - async def forward_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - messages: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - from_peer: 'hints.EntityLike' = None, - *, - background: bool = None, - with_my_score: bool = None, - silent: bool = None, - as_album: bool = None, - schedule: 'hints.DateLike' = None - ) -> 'typing.Sequence[types.Message]': - """ - Forwards the given messages to the specified entity. - - If you want to "forward" a message without the forward header - (the "forwarded from" text), you should use `send_message` with - the original message instead. This will send a copy of it. - - See also `Message.forward_to() `. - - Arguments - entity (`entity`): - To which entity the message(s) will be forwarded. - - messages (`list` | `int` | `Message `): - The message(s) to forward, or their integer IDs. - - from_peer (`entity`): - If the given messages are integer IDs and not instances - of the ``Message`` class, this *must* be specified in - order for the forward to work. This parameter indicates - the entity from which the messages should be forwarded. - - silent (`bool`, optional): - Whether the message should notify people with sound or not. - Defaults to `False` (send with a notification sound unless - the person has the chat muted). Set it to `True` to alter - this behaviour. - - background (`bool`, optional): - Whether the message should be forwarded in background. - - with_my_score (`bool`, optional): - Whether forwarded should contain your game score. - - as_album (`bool`, optional): - This flag no longer has any effect. - - schedule (`hints.DateLike`, optional): - If set, the message(s) won't forward immediately, and - instead they will be scheduled to be automatically sent - at a later time. - - Returns - The list of forwarded `Message `, - or a single one if a list wasn't provided as input. - - Note that if all messages are invalid (i.e. deleted) the call - will fail with ``MessageIdInvalidError``. If only some are - invalid, the list will have `None` instead of those messages. - - Example - .. code-block:: python - - # a single one - await client.forward_messages(chat, message) - # or - await client.forward_messages(chat, message_id, from_chat) - # or - await message.forward_to(chat) - - # multiple - await client.forward_messages(chat, messages) - # or - await client.forward_messages(chat, message_ids, from_chat) - - # Forwarding as a copy - await client.send_message(chat, message) - """ - if as_album is not None: - warnings.warn('the as_album argument is deprecated and no longer has any effect') - - single = not utils.is_list_like(messages) - if single: - messages = (messages,) - - entity = await self.get_input_entity(entity) - - if from_peer: - from_peer = await self.get_input_entity(from_peer) - from_peer_id = await self.get_peer_id(from_peer) - else: - from_peer_id = None - - def get_key(m): - if isinstance(m, int): - if from_peer_id is not None: - return from_peer_id - - raise ValueError('from_peer must be given if integer IDs are used') - elif isinstance(m, types.Message): - return m.chat_id - else: - raise TypeError('Cannot forward messages of type {}'.format(type(m))) - - sent = [] - for _chat_id, chunk in itertools.groupby(messages, key=get_key): - chunk = list(chunk) - if isinstance(chunk[0], int): - chat = from_peer - else: - chat = from_peer or await self.get_input_entity(chunk[0].peer_id) - chunk = [m.id for m in chunk] - - req = functions.messages.ForwardMessagesRequest( - from_peer=chat, - id=chunk, - to_peer=entity, - silent=silent, - background=background, - with_my_score=with_my_score, - schedule_date=schedule - ) - 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 = (), - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - link_preview: bool = True, - file: 'hints.FileLike' = None, - thumb: 'hints.FileLike' = None, - force_document: bool = False, - buttons: typing.Optional['hints.MarkupLike'] = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None - ) -> 'types.Message': - """ - Edits the given message to change its text or media. - - See also `Message.edit() `. - - Arguments - entity (`entity` | `Message `): - From which chat to edit the message. This can also be - the message to be edited, and the entity will be inferred - from it, so the next parameter will be assumed to be the - message text. - - You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`, - which is the only way to edit messages that were sent - after the user selects an inline query result. - - message (`int` | `Message ` | `str`): - The ID of the message (or `Message - ` itself) to be edited. - If the `entity` was a `Message - `, then this message - will be treated as the new text. - - text (`str`, optional): - The new text of the message. Does nothing if the `entity` - was a `Message `. - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - link_preview (`bool`, optional): - Should the link preview be shown? - - file (`str` | `bytes` | `file` | `media`, optional): - The file object that should replace the existing media - in the message. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - force_document (`bool`, optional): - Whether to send the given file as a document or not. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the message won't be edited immediately, and instead - it will be scheduled to be automatically edited at a later - time. - - Note that this parameter will have no effect if you are - trying to edit a message that was sent via inline bots. - - Returns - The edited `Message `, - unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which - case this method returns a boolean. - - Raises - ``MessageAuthorRequiredError`` if you're not the author of the - message but tried editing it anyway. - - ``MessageNotModifiedError`` if the contents of the message were - not modified at all. - - ``MessageIdInvalidError`` if the ID of the message is invalid - (the ID itself may be correct, but the message with that ID - cannot be edited). For example, when trying to edit messages - with a reply markup (or clear markup) this error will be raised. - - Example - .. code-block:: python - - message = await client.send_message(chat, 'hello') - - await client.edit_message(chat, message, 'hello!') - # or - await client.edit_message(chat, message.id, 'hello!!') - # or - await client.edit_message(message, 'hello!!!') - """ - if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): - text = text or message - message = entity - elif isinstance(entity, types.Message): - text = message # Shift the parameters to the right - message = entity - entity = entity.peer_id - - if formatting_entities is None: - text, formatting_entities = await self._parse_message_text(text, parse_mode) - file_handle, media, image = await self._file_to_media(file, - supports_streaming=supports_streaming, - thumb=thumb, - attributes=attributes, - force_document=force_document) - - if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): - request = functions.messages.EditInlineBotMessageRequest( - id=entity, - message=text, - no_webpage=not link_preview, - entities=formatting_entities, - media=media, - reply_markup=self.build_reply_markup(buttons) - ) - # Invoke `messages.editInlineBotMessage` from the right datacenter. - # Otherwise, Telegram will error with `MESSAGE_ID_INVALID` and do nothing. - exported = self.session.dc_id != entity.dc_id - if exported: - try: - sender = await self._borrow_exported_sender(entity.dc_id) - return await self._call(sender, request) - finally: - await self._return_exported_sender(sender) - else: - return await self(request) - - entity = await self.get_input_entity(entity) - request = functions.messages.EditMessageRequest( - peer=entity, - id=utils.get_message_id(message), - message=text, - no_webpage=not link_preview, - entities=formatting_entities, - media=media, - reply_markup=self.build_reply_markup(buttons), - schedule_date=schedule - ) - msg = self._get_response_message(request, await self(request), entity) - return msg - - async def delete_messages( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message_ids: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]', - *, - revoke: bool = True) -> 'typing.Sequence[types.messages.AffectedMessages]': - """ - Deletes the given messages, optionally "for everyone". - - See also `Message.delete() `. - - .. warning:: - - This method does **not** validate that the message IDs belong - to the chat that you passed! It's possible for the method to - delete messages from different private chats and small group - chats at once, so make sure to pass the right IDs. - - Arguments - entity (`entity`): - From who the message will be deleted. This can actually - be `None` for normal chats, but **must** be present - for channels and megagroups. - - message_ids (`list` | `int` | `Message `): - The IDs (or ID) or messages to be deleted. - - revoke (`bool`, optional): - Whether the message should be deleted for everyone or not. - By default it has the opposite behaviour of official clients, - and it will delete the message for everyone. - - `Since 24 March 2019 - `_, you can - also revoke messages of any age (i.e. messages sent long in - the past) the *other* person sent in private conversations - (and of course your messages too). - - Disabling this has no effect on channels or megagroups, - since it will unconditionally delete the message for everyone. - - Returns - A list of :tl:`AffectedMessages`, each item being the result - for the delete calls of the messages in chunks of 100 each. - - Example - .. code-block:: python - - await client.delete_messages(chat, messages) - """ - if not utils.is_list_like(message_ids): - message_ids = (message_ids,) - - message_ids = ( - m.id if isinstance(m, ( - types.Message, types.MessageService, types.MessageEmpty)) - else int(m) for m in message_ids - ) - - if entity: - entity = await self.get_input_entity(entity) - ty = helpers._entity_type(entity) - else: - # no entity (None), set a value that's not a channel for private delete - ty = helpers._EntityType.USER - - if ty == helpers._EntityType.CHANNEL: - return await self([functions.channels.DeleteMessagesRequest( - entity, list(c)) for c in utils.chunks(message_ids)]) - else: - return await self([functions.messages.DeleteMessagesRequest( - list(c), revoke) for c in utils.chunks(message_ids)]) - - # endregion - - # region Miscellaneous - - async def send_read_acknowledge( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None, - *, - max_id: int = None, - clear_mentions: bool = False, - clear_reactions: bool = False) -> bool: - """ - Marks messages as read and optionally clears mentions. - - This effectively marks a message as read (or more than one) in the - given conversation. - - If neither message nor maximum ID are provided, all messages will be - marked as read by assuming that ``max_id = 0``. - - If a message or maximum ID is provided, all the messages up to and - including such ID will be marked as read (for all messages whose ID - ≤ max_id). - - See also `Message.mark_read() `. - - Arguments - entity (`entity`): - The chat where these messages are located. - - message (`list` | `Message `): - Either a list of messages or a single message. - - max_id (`int`): - Until which message should the read acknowledge be sent for. - This has priority over the ``message`` parameter. - - clear_mentions (`bool`): - Whether the mention badge should be cleared (so that - there are no more mentions) or not for the given entity. - - If no message is provided, this will be the only action - taken. - - clear_reactions (`bool`): - Whether the reactions badge should be cleared (so that - there are no more reaction notifications) or not for the given entity. - - If no message is provided, this will be the only action - taken. - - Example - .. code-block:: python - - # using a Message object - await client.send_read_acknowledge(chat, message) - # ...or using the int ID of a Message - await client.send_read_acknowledge(chat, message_id) - # ...or passing a list of messages to mark as read - await client.send_read_acknowledge(chat, messages) - """ - if max_id is None: - if not message: - max_id = 0 - else: - if utils.is_list_like(message): - max_id = max(msg.id for msg in message) - else: - max_id = message.id - - entity = await self.get_input_entity(entity) - if clear_mentions: - await self(functions.messages.ReadMentionsRequest(entity)) - if max_id is None and not clear_reactions: - return True - if clear_reactions: - await self(functions.messages.ReadReactionsRequest(entity)) - if max_id is None: - return True - - if max_id is not None: - if helpers._entity_type(entity) == helpers._EntityType.CHANNEL: - return await self(functions.channels.ReadHistoryRequest( - utils.get_input_channel(entity), max_id=max_id)) - else: - return await self(functions.messages.ReadHistoryRequest( - entity, max_id=max_id)) - - return False - - async def pin_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Optional[hints.MessageIDLike]', - *, - notify: bool = False, - pm_oneside: bool = False - ): - """ - Pins a message in a chat. - - The default behaviour is to *not* notify members, unlike the - official applications. - - See also `Message.pin() `. - - Arguments - entity (`entity`): - The chat where the message should be pinned. - - message (`int` | `Message `): - The message or the message ID to pin. If it's - `None`, all messages will be unpinned instead. - - notify (`bool`, optional): - Whether the pin should notify people or not. - - pm_oneside (`bool`, optional): - Whether the message should be pinned for everyone or not. - By default it has the opposite behaviour of official clients, - and it will pin the message for both sides, in private chats. - - Example - .. code-block:: python - - # Send and pin a message to annoy everyone - message = await client.send_message(chat, 'Pinotifying is fun!') - await client.pin_message(chat, message, notify=True) - """ - return await self._pin(entity, message, unpin=False, notify=notify, pm_oneside=pm_oneside) - - async def unpin_message( - self: 'TelegramClient', - entity: 'hints.EntityLike', - message: 'typing.Optional[hints.MessageIDLike]' = None, - *, - notify: bool = False - ): - """ - Unpins a message in a chat. - - If no message ID is specified, all pinned messages will be unpinned. - - See also `Message.unpin() `. - - Arguments - entity (`entity`): - The chat where the message should be pinned. - - message (`int` | `Message `): - The message or the message ID to unpin. If it's - `None`, all messages will be unpinned instead. - - Example - .. code-block:: python - - # Unpin all messages from a chat - await client.unpin_message(chat) - """ - return await self._pin(entity, message, unpin=True, notify=notify) - - async def _pin(self, entity, message, *, unpin, notify=False, pm_oneside=False): - message = utils.get_message_id(message) or 0 - entity = await self.get_input_entity(entity) - if message <= 0: # old behaviour accepted negative IDs to unpin - await self(functions.messages.UnpinAllMessagesRequest(entity)) - return - - request = functions.messages.UpdatePinnedMessageRequest( - peer=entity, - id=message, - silent=not notify, - unpin=unpin, - pm_oneside=pm_oneside - ) - result = await self(request) - - # Unpinning does not produce a service message. - # Pinning a message that was already pinned also produces no service message. - # Pinning a message in your own chat does not produce a service message, - # but pinning on a private conversation with someone else does. - if unpin or not result.updates: - return - - # Pinning a message that doesn't exist would RPC-error earlier - return self._get_response_message(request, result, entity) - - # endregion - - # endregion diff --git a/telethon/client/telegrambaseclient.py b/telethon/client/telegrambaseclient.py deleted file mode 100644 index dd127654..00000000 --- a/telethon/client/telegrambaseclient.py +++ /dev/null @@ -1,951 +0,0 @@ -import abc -import re -import asyncio -import collections -import logging -import platform -import time -import typing -import datetime - -from .. import version, helpers, __name__ as __base_name__ -from ..crypto import rsa -from ..extensions import markdown -from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy -from ..sessions import Session, SQLiteSession, MemorySession -from ..tl import functions, types -from ..tl.alltlobjects import LAYER -from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType - -DEFAULT_DC_ID = 2 -DEFAULT_IPV4_IP = '149.154.167.51' -DEFAULT_IPV6_IP = '2001:67c:4e8:f002::a' -DEFAULT_PORT = 443 - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - -_base_log = logging.getLogger(__base_name__) - - -# In seconds, how long to wait before disconnecting a exported sender. -_DISCONNECT_EXPORTED_AFTER = 60 - - -class _ExportState: - def __init__(self): - # ``n`` is the amount of borrows a given sender has; - # once ``n`` reaches ``0``, disconnect the sender after a while. - self._n = 0 - self._zero_ts = 0 - self._connected = False - - def add_borrow(self): - self._n += 1 - self._connected = True - - def add_return(self): - self._n -= 1 - assert self._n >= 0, 'returned sender more than it was borrowed' - if self._n == 0: - self._zero_ts = time.time() - - def should_disconnect(self): - return (self._n == 0 - and self._connected - and (time.time() - self._zero_ts) > _DISCONNECT_EXPORTED_AFTER) - - def need_connect(self): - return not self._connected - - def mark_disconnected(self): - assert self.should_disconnect(), 'marked as disconnected when it was borrowed' - self._connected = False - - -# TODO How hard would it be to support both `trio` and `asyncio`? -class TelegramBaseClient(abc.ABC): - """ - This is the abstract base class for the client. It defines some - basic stuff like connecting, switching data center, etc, and - leaves the `__call__` unimplemented. - - Arguments - session (`str` | `telethon.sessions.abstract.Session`, `None`): - The file name of the session file to be used if a string is - given (it may be a full path), or the Session instance to be - used otherwise. If it's `None`, the session will not be saved, - and you should call :meth:`.log_out()` when you're done. - - Note that if you pass a string it will be a file in the current - working directory, although you can also pass absolute paths. - - The session file contains enough information for you to login - without re-sending the code, so if you have to enter the code - more than once, maybe you're changing the working directory, - renaming or removing the file, or using random names. - - api_id (`int` | `str`): - The API ID you obtained from https://my.telegram.org. - - api_hash (`str`): - The API hash you obtained from https://my.telegram.org. - - connection (`telethon.network.connection.common.Connection`, optional): - The connection instance to be used when creating a new connection - to the servers. It **must** be a type. - - Defaults to `telethon.network.connection.tcpfull.ConnectionTcpFull`. - - use_ipv6 (`bool`, optional): - Whether to connect to the servers through IPv6 or not. - By default this is `False` as IPv6 support is not - too widespread yet. - - proxy (`tuple` | `list` | `dict`, optional): - An iterable consisting of the proxy info. If `connection` is - one of `MTProxy`, then it should contain MTProxy credentials: - ``('hostname', port, 'secret')``. Otherwise, it's meant to store - function parameters for PySocks, like ``(type, 'hostname', port)``. - See https://github.com/Anorov/PySocks#usage-1 for more. - - local_addr (`str` | `tuple`, optional): - Local host address (and port, optionally) used to bind the socket to locally. - You only need to use this if you have multiple network cards and - want to use a specific one. - - timeout (`int` | `float`, optional): - The timeout in seconds to be used when connecting. - This is **not** the timeout to be used when ``await``'ing for - invoked requests, and you should use ``asyncio.wait`` or - ``asyncio.wait_for`` for that. - - request_retries (`int` | `None`, optional): - How many times a request should be retried. Request are retried - when Telegram is having internal issues (due to either - ``errors.ServerError`` or ``errors.RpcCallFailError``), - when there is a ``errors.FloodWaitError`` less than - `flood_sleep_threshold`, or when there's a migrate error. - - May take a negative or `None` value for infinite retries, but - this is not recommended, since some requests can always trigger - a call fail (such as searching for messages). - - connection_retries (`int` | `None`, optional): - How many times the reconnection should retry, either on the - initial connection or when Telegram disconnects us. May be - set to a negative or `None` value for infinite retries, but - this is not recommended, since the program can get stuck in an - infinite loop. - - retry_delay (`int` | `float`, optional): - The delay in seconds to sleep between automatic reconnections. - - auto_reconnect (`bool`, optional): - Whether reconnection should be retried `connection_retries` - times automatically if Telegram disconnects us or not. - - sequential_updates (`bool`, optional): - By default every incoming update will create a new task, so - you can handle several updates in parallel. Some scripts need - the order in which updates are processed to be sequential, and - this setting allows them to do so. - - If set to `True`, incoming updates will be put in a queue - and processed sequentially. This means your event handlers - should *not* perform long-running operations since new - updates are put inside of an unbounded queue. - - flood_sleep_threshold (`int` | `float`, optional): - The threshold below which the library should automatically - sleep on flood wait and slow mode wait errors (inclusive). For instance, if a - ``FloodWaitError`` for 17s occurs and `flood_sleep_threshold` - is 20s, the library will ``sleep`` automatically. If the error - was for 21s, it would ``raise FloodWaitError`` instead. Values - larger than a day (like ``float('inf')``) will be changed to a day. - - raise_last_call_error (`bool`, optional): - When API calls fail in a way that causes Telethon to retry - automatically, should the RPC error of the last attempt be raised - instead of a generic ValueError. This is mostly useful for - detecting when Telegram has internal issues. - - device_model (`str`, optional): - "Device model" to be sent when creating the initial connection. - Defaults to 'PC (n)bit' derived from ``platform.uname().machine``, or its direct value if unknown. - - system_version (`str`, optional): - "System version" to be sent when creating the initial connection. - Defaults to ``platform.uname().release`` stripped of everything ahead of -. - - app_version (`str`, optional): - "App version" to be sent when creating the initial connection. - Defaults to `telethon.version.__version__`. - - lang_code (`str`, optional): - "Language code" to be sent when creating the initial connection. - Defaults to ``'en'``. - - system_lang_code (`str`, optional): - "System lang code" to be sent when creating the initial connection. - Defaults to `lang_code`. - - loop (`asyncio.AbstractEventLoop`, optional): - Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`. - This argument is ignored. - - base_logger (`str` | `logging.Logger`, optional): - Base logger name or instance to use. - If a `str` is given, it'll be passed to `logging.getLogger()`. If a - `logging.Logger` is given, it'll be used directly. If something - else or nothing is given, the default logger will be used. - - receive_updates (`bool`, optional): - Whether the client will receive updates or not. By default, updates - will be received from Telegram as they occur. - - Turning this off means that Telegram will not send updates at all - so event handlers, conversations, and QR login will not work. - However, certain scripts don't need updates, so this will reduce - the amount of bandwidth used. - - entity_cache_limit (`int`, optional): - How many users, chats and channels to keep in the in-memory cache - at most. This limit is checked against when processing updates. - - When this limit is reached or exceeded, all entities that are not - required for update handling will be flushed to the session file. - - Note that this implies that there is a lower bound to the amount - of entities that must be kept in memory. - - Setting this limit too low will cause the library to attempt to - flush entities to the session file even if no entities can be - removed from the in-memory cache, which will degrade performance. - """ - - # Current TelegramClient version - __version__ = version.__version__ - - # Cached server configuration (with .dc_options), can be "global" - _config = None - _cdn_config = None - - # region Initialization - - def __init__( - self: 'TelegramClient', - session: 'typing.Union[str, Session]', - api_id: int, - api_hash: str, - *, - connection: 'typing.Type[Connection]' = ConnectionTcpFull, - use_ipv6: bool = False, - proxy: typing.Union[tuple, dict] = None, - local_addr: typing.Union[str, tuple] = None, - timeout: int = 10, - request_retries: int = 5, - connection_retries: int = 5, - retry_delay: int = 1, - auto_reconnect: bool = True, - sequential_updates: bool = False, - flood_sleep_threshold: int = 60, - raise_last_call_error: bool = False, - device_model: str = None, - system_version: str = None, - app_version: str = None, - lang_code: str = 'en', - system_lang_code: str = 'en', - loop: asyncio.AbstractEventLoop = None, - base_logger: typing.Union[str, logging.Logger] = None, - receive_updates: bool = True, - catch_up: bool = False, - entity_cache_limit: int = 5000 - ): - if not api_id or not api_hash: - raise ValueError( - "Your API ID or Hash cannot be empty or None. " - "Refer to telethon.rtfd.io for more information.") - - self._use_ipv6 = use_ipv6 - - if isinstance(base_logger, str): - base_logger = logging.getLogger(base_logger) - elif not isinstance(base_logger, logging.Logger): - base_logger = _base_log - - class _Loggers(dict): - def __missing__(self, key): - if key.startswith("telethon."): - key = key.split('.', maxsplit=1)[1] - - return base_logger.getChild(key) - - self._log = _Loggers() - - # Determine what session object we have - if isinstance(session, str) 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 _mb_entity_cache - # to avoid network access and the need for await in session files. - # - # The session files only wants the entities to persist - # them to disk, and to save additional useful information. - # TODO Session should probably return all cached - # info of entities, not just the input versions - self.session = session - self.api_id = int(api_id) - self.api_hash = api_hash - - # Current proxy implementation requires `sock_connect`, and some - # event loops lack this method. If the current loop is missing it, - # bail out early and suggest an alternative. - # - # TODO A better fix is obviously avoiding the use of `sock_connect` - # - # See https://github.com/LonamiWebs/Telethon/issues/1337 for details. - if not callable(getattr(self.loop, 'sock_connect', None)): - raise TypeError( - 'Event loop of type {} lacks `sock_connect`, which is needed to use proxies.\n\n' - 'Change the event loop in use to use proxies:\n' - '# https://github.com/LonamiWebs/Telethon/issues/1337\n' - 'import asyncio\n' - 'asyncio.set_event_loop(asyncio.SelectorEventLoop())'.format( - self.loop.__class__.__name__ - ) - ) - - if local_addr is not None: - if use_ipv6 is False and ':' in local_addr: - raise TypeError( - 'A local IPv6 address must only be used with `use_ipv6=True`.' - ) - elif use_ipv6 is True and ':' not in local_addr: - raise TypeError( - '`use_ipv6=True` must only be used with a local IPv6 address.' - ) - - self._raise_last_call_error = raise_last_call_error - - self._request_retries = request_retries - self._connection_retries = connection_retries - self._retry_delay = retry_delay or 0 - self._proxy = proxy - self._local_addr = local_addr - self._timeout = timeout - self._auto_reconnect = auto_reconnect - - assert isinstance(connection, type) - self._connection = connection - init_proxy = None if not issubclass(connection, TcpMTProxy) else \ - types.InputClientProxy(*connection.address_info(proxy)) - - # Used on connection. Capture the variables in a lambda since - # exporting clients need to create this InvokeWithLayerRequest. - system = platform.uname() - - if system.machine in ('x86_64', 'AMD64'): - default_device_model = 'PC 64bit' - elif system.machine in ('i386','i686','x86'): - default_device_model = 'PC 32bit' - else: - default_device_model = system.machine - default_system_version = re.sub(r'-.+','',system.release) - - self._init_request = functions.InitConnectionRequest( - api_id=self.api_id, - device_model=device_model or default_device_model or 'Unknown', - system_version=system_version or default_system_version or '1.0', - app_version=app_version or self.__version__, - lang_code=lang_code, - system_lang_code=system_lang_code, - lang_pack='', # "langPacks are for official apps only" - query=None, - proxy=init_proxy - ) - - # Remember flood-waited requests to avoid making them again - self._flood_waited_requests = {} - - # Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders - self._borrowed_senders = {} - self._borrow_sender_lock = asyncio.Lock() - - self._loop = None # only used as a sanity check - self._updates_error = None - self._updates_handle = None - self._keepalive_handle = None - self._last_request = time.time() - self._no_updates = not receive_updates - - # Used for non-sequential updates, in order to terminate all pending tasks on disconnect. - self._sequential_updates = sequential_updates - self._event_handler_tasks = set() - - self._authorized = None # None = unknown, False = no, True = yes - - # Some further state for subclasses - self._event_builders = [] - - # {chat_id: {Conversation}} - self._conversations = collections.defaultdict(set) - - # Hack to workaround the fact Telegram may send album updates as - # different Updates when being sent from a different data center. - # {grouped_id: AlbumHack} - # - # FIXME: We don't bother cleaning this up because it's not really - # worth it, albums are pretty rare and this only holds them - # for a second at most. - self._albums = {} - - # Default parse mode - self._parse_mode = markdown - - # Some fields to easy signing in. Let {phone: hash} be - # a dictionary because the user may change their mind. - self._phone_code_hash = {} - self._phone = None - self._tos = None - - # A place to store if channels are a megagroup or not (see `edit_admin`) - self._megagroup_cache = {} - - # This is backported from v2 in a very ad-hoc way just to get proper update handling - self._catch_up = catch_up - self._updates_queue = asyncio.Queue() - self._message_box = MessageBox(self._log['messagebox']) - self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference) - self._entity_cache_limit = entity_cache_limit - - self._sender = MTProtoSender( - self.session.auth_key, - loggers=self._log, - retries=self._connection_retries, - delay=self._retry_delay, - auto_reconnect=self._auto_reconnect, - connect_timeout=self._timeout, - auth_key_callback=self._auth_key_callback, - updates_queue=self._updates_queue, - auto_reconnect_callback=self._handle_auto_reconnect - ) - - - # endregion - - # region Properties - - @property - def loop(self: 'TelegramClient') -> asyncio.AbstractEventLoop: - """ - Property with the ``asyncio`` event loop used by this client. - - Example - .. code-block:: python - - # Download media in the background - task = client.loop.create_task(message.download_media()) - - # Do some work - ... - - # Join the task (wait for it to complete) - await task - """ - return helpers.get_running_loop() - - @property - def disconnected(self: 'TelegramClient') -> asyncio.Future: - """ - Property with a ``Future`` that resolves upon disconnection. - - Example - .. code-block:: python - - # Wait for a disconnection to occur - try: - await client.disconnected - except OSError: - print('Error on disconnect') - """ - return self._sender.disconnected - - @property - def flood_sleep_threshold(self): - return self._flood_sleep_threshold - - @flood_sleep_threshold.setter - def flood_sleep_threshold(self, value): - # None -> 0, negative values don't really matter - self._flood_sleep_threshold = min(value or 0, 24 * 60 * 60) - - # endregion - - # region Connecting - - async def connect(self: 'TelegramClient') -> None: - """ - Connects to Telegram. - - .. note:: - - Connect means connect and nothing else, and only one low-level - request is made to notify Telegram about which layer we will be - using. - - Before Telegram sends you updates, you need to make a high-level - request, like `client.get_me() `, - as described in https://core.telegram.org/api/updates. - - Example - .. code-block:: python - - try: - await client.connect() - except OSError: - print('Failed to connect') - """ - if self.session is None: - raise ValueError('TelegramClient instance cannot be reused after logging out') - - if self._loop is None: - self._loop = helpers.get_running_loop() - elif self._loop != helpers.get_running_loop(): - raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') - - if not await self._sender.connect(self._connection( - self.session.server_address, - self.session.port, - self.session.dc_id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )): - # We don't want to init or modify anything if we were already connected - return - - self.session.auth_key = self._sender.auth_key - self.session.save() - - try: - # See comment when saving entities to understand this hack - self_id = self.session.get_input_entity(0).access_hash - self_user = self.session.get_input_entity(self_id) - self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash) - except ValueError: - pass - - if self._catch_up: - ss = SessionState(0, 0, False, 0, 0, 0, 0, None) - cs = [] - - for entity_id, state in self.session.get_update_states(): - if entity_id == 0: - # TODO current session doesn't store self-user info but adding that is breaking on downstream session impls - ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None) - else: - cs.append(ChannelState(entity_id, state.pts)) - - self._message_box.load(ss, cs) - for state in cs: - try: - entity = self.session.get_input_entity(state.channel_id) - except ValueError: - self._log[__name__].warning( - 'No access_hash in cache for channel %s, will not catch up', state.channel_id) - else: - self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash)) - - self._init_request.query = functions.help.GetConfigRequest() - - req = self._init_request - if self._no_updates: - req = functions.InvokeWithoutUpdatesRequest(req) - - await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req)) - - if self._message_box.is_empty(): - me = await self.get_me() - if me: - await self._on_login(me) # also calls GetState to initialize the MessageBox - - self._updates_handle = self.loop.create_task(self._update_loop()) - self._keepalive_handle = self.loop.create_task(self._keepalive_loop()) - - def is_connected(self: 'TelegramClient') -> bool: - """ - Returns `True` if the user has connected. - - This method is **not** asynchronous (don't use ``await`` on it). - - Example - .. code-block:: python - - while client.is_connected(): - await asyncio.sleep(1) - """ - sender = getattr(self, '_sender', None) - return sender and sender.is_connected() - - def disconnect(self: 'TelegramClient'): - """ - Disconnects from Telegram. - - If the event loop is already running, this method returns a - coroutine that you should await on your own code; otherwise - the loop is ran until said coroutine completes. - - Event handlers which are currently running will be cancelled before - this function returns (in order to properly clean-up their tasks). - In particular, this means that using ``disconnect`` in a handler - will cause code after the ``disconnect`` to never run. If this is - needed, consider spawning a separate task to do the remaining work. - - Example - .. code-block:: python - - # You don't need to use this if you used "with client" - await client.disconnect() - """ - if self.loop.is_running(): - # Disconnect may be called from an event handler, which would - # cancel itself during itself and never actually complete the - # disconnection. Shield the task to prevent disconnect itself - # from being cancelled. See issue #3942 for more details. - return asyncio.shield(self.loop.create_task(self._disconnect_coro())) - else: - try: - self.loop.run_until_complete(self._disconnect_coro()) - except RuntimeError: - # Python 3.5.x complains when called from - # `__aexit__` and there were pending updates with: - # "Event loop stopped before Future completed." - # - # However, it doesn't really make a lot of sense. - pass - - def set_proxy(self: 'TelegramClient', proxy: typing.Union[tuple, dict]): - """ - Changes the proxy which will be used on next (re)connection. - - Method has no immediate effects if the client is currently connected. - - The new proxy will take it's effect on the next reconnection attempt: - - on a call `await client.connect()` (after complete disconnect) - - on auto-reconnect attempt (e.g, after previous connection was lost) - """ - init_proxy = None if not issubclass(self._connection, TcpMTProxy) else \ - types.InputClientProxy(*self._connection.address_info(proxy)) - - self._init_request.proxy = init_proxy - self._proxy = proxy - - # While `await client.connect()` passes new proxy on each new call, - # auto-reconnect attempts use already set up `_connection` inside - # the `_sender`, so the only way to change proxy between those - # is to directly inject parameters. - - connection = getattr(self._sender, "_connection", None) - if connection: - if isinstance(connection, TcpMTProxy): - connection._ip = proxy[0] - connection._port = proxy[1] - else: - connection._proxy = proxy - - def _save_states_and_entities(self: 'TelegramClient'): - entities = self._mb_entity_cache.get_all_entities() - - # Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities. - # It doesn't matter if we put users in the list of chats. - self.session.process_entities(types.contacts.ResolvedPeer(None, [e._as_input_peer() for e in entities], [])) - - # As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``. - # This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved. - if self._mb_entity_cache.self_id: - self.session.process_entities(types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], [])) - - ss, cs = self._message_box.session_state() - self.session.set_update_state(0, types.updates.State(**ss, unread_count=0)) - now = datetime.datetime.now() # any datetime works; channels don't need it - for channel_id, pts in cs.items(): - self.session.set_update_state(channel_id, types.updates.State(pts, 0, now, 0, unread_count=0)) - - async def _disconnect_coro(self: 'TelegramClient'): - if self.session is None: - return # already logged out and disconnected - - await self._disconnect() - - # Also clean-up all exported senders because we're done with them - async with self._borrow_sender_lock: - for state, sender in self._borrowed_senders.values(): - # Note that we're not checking for `state.should_disconnect()`. - # If the user wants to disconnect the client, ALL connections - # to Telegram (including exported senders) should be closed. - # - # Disconnect should never raise, so there's no try/except. - await sender.disconnect() - # Can't use `mark_disconnected` because it may be borrowed. - state._connected = False - - # If any was borrowed - self._borrowed_senders.clear() - - # trio's nurseries would handle this for us, but this is asyncio. - # All tasks spawned in the background should properly be terminated. - if self._event_handler_tasks: - for task in self._event_handler_tasks: - task.cancel() - - await asyncio.wait(self._event_handler_tasks) - self._event_handler_tasks.clear() - - self._save_states_and_entities() - - self.session.close() - - async def _disconnect(self: 'TelegramClient'): - """ - Disconnect only, without closing the session. Used in reconnections - to different data centers, where we don't want to close the session - file; user disconnects however should close it since it means that - their job with the client is complete and we should clean it up all. - """ - await self._sender.disconnect() - await helpers._cancel(self._log[__name__], - updates_handle=self._updates_handle, - keepalive_handle=self._keepalive_handle) - - async def _switch_dc(self: 'TelegramClient', new_dc): - """ - Permanently switches the current connection to the new data center. - """ - self._log[__name__].info('Reconnecting to new data center %s', new_dc) - dc = await self._get_dc(new_dc) - - self.session.set_dc(dc.id, dc.ip_address, dc.port) - # auth_key's are associated with a server, which has now changed - # so it's not valid anymore. Set to None to force recreating it. - self._sender.auth_key.key = None - self.session.auth_key = None - self.session.save() - await self._disconnect() - return await self.connect() - - def _auth_key_callback(self: 'TelegramClient', auth_key): - """ - Callback from the sender whenever it needed to generate a - new authorization key. This means we are not authorized. - """ - self.session.auth_key = auth_key - self.session.save() - - # endregion - - # region Working with different connections/Data Centers - - async def _get_dc(self: 'TelegramClient', dc_id, cdn=False): - """Gets the Data Center (DC) associated to 'dc_id'""" - cls = self.__class__ - if not cls._config: - cls._config = await self(functions.help.GetConfigRequest()) - - if cdn and not self._cdn_config: - cls._cdn_config = await self(functions.help.GetCdnConfigRequest()) - for pk in cls._cdn_config.public_keys: - rsa.add_key(pk.public_key) - - try: - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id - and bool(dc.ipv6) == self._use_ipv6 and bool(dc.cdn) == cdn - ) - except StopIteration: - self._log[__name__].warning( - 'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check', - dc_id, cdn, self._use_ipv6 - ) - return next( - dc for dc in cls._config.dc_options - if dc.id == dc_id 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, loggers=self._log) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )) - self._log[__name__].info('Exporting auth for new borrowed sender in %s', dc) - auth = await self(functions.auth.ExportAuthorizationRequest(dc_id)) - self._init_request.query = functions.auth.ImportAuthorizationRequest(id=auth.id, bytes=auth.bytes) - req = functions.InvokeWithLayerRequest(LAYER, self._init_request) - await sender.send(req) - return sender - - async def _borrow_exported_sender(self: 'TelegramClient', dc_id): - """ - Borrows a connected `MTProtoSender` for the given `dc_id`. - If it's not cached, creates a new one if it doesn't exist yet, - and imports a freshly exported authorization key for it to be usable. - - Once its job is over it should be `_return_exported_sender`. - """ - async with self._borrow_sender_lock: - self._log[__name__].debug('Borrowing sender for dc_id %d', dc_id) - state, sender = self._borrowed_senders.get(dc_id, (None, None)) - - if state is None: - state = _ExportState() - sender = await self._create_exported_sender(dc_id) - sender.dc_id = dc_id - self._borrowed_senders[dc_id] = (state, sender) - - elif state.need_connect(): - dc = await self._get_dc(dc_id) - await sender.connect(self._connection( - dc.ip_address, - dc.port, - dc.id, - loggers=self._log, - proxy=self._proxy, - local_addr=self._local_addr - )) - - state.add_borrow() - return sender - - async def _return_exported_sender(self: 'TelegramClient', sender): - """ - Returns a borrowed exported sender. If all borrows have - been returned, the sender is cleanly disconnected. - """ - async with self._borrow_sender_lock: - self._log[__name__].debug('Returning borrowed sender for dc_id %d', sender.dc_id) - state, _ = self._borrowed_senders[sender.dc_id] - state.add_return() - - async def _clean_exported_senders(self: 'TelegramClient'): - """ - Cleans-up all unused exported senders by disconnecting them. - """ - async with self._borrow_sender_lock: - for dc_id, (state, sender) in self._borrowed_senders.items(): - if state.should_disconnect(): - self._log[__name__].info( - 'Disconnecting borrowed sender for DC %d', dc_id) - - # Disconnect should never raise - await sender.disconnect() - state.mark_disconnected() - - async def _get_cdn_client(self: 'TelegramClient', cdn_redirect): - """Similar to ._borrow_exported_client, but for CDNs""" - # 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() - 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 = TelegramBaseClient( - 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. - - flood_sleep_threshold (`int` | `None`, optional): - The flood sleep threshold to use for this request. This overrides - the default value stored in - `client.flood_sleep_threshold ` - - Returns: - The result of the request (often a `TLObject`) or a list of - results if more than one request was given. - """ - raise NotImplementedError - - @abc.abstractmethod - def _update_loop(self: 'TelegramClient'): - raise NotImplementedError - - @abc.abstractmethod - async def _handle_auto_reconnect(self: 'TelegramClient'): - raise NotImplementedError - - # endregion diff --git a/telethon/client/telegramclient.py b/telethon/client/telegramclient.py deleted file mode 100644 index 144a6b2f..00000000 --- a/telethon/client/telegramclient.py +++ /dev/null @@ -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 deleted file mode 100644 index a69aaf31..00000000 --- a/telethon/client/updates.py +++ /dev/null @@ -1,691 +0,0 @@ -import asyncio -import inspect -import itertools -import random -import sys -import time -import traceback -import typing -import logging -import warnings -from collections import deque - -from .. import events, utils, errors -from ..events.common import EventBuilder, EventCommon -from ..tl import types, functions -from .._updates import GapError, PrematureEndReason -from ..helpers import get_running_loop -from ..version import __version__ - - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -Callback = typing.Callable[[typing.Any], typing.Any] - -class UpdateMethods: - - # region Public methods - - async def _run_until_disconnected(self: 'TelegramClient'): - try: - # Make a high-level request to notify that we want updates - await self(functions.updates.GetStateRequest()) - result = await self.disconnected - if self._updates_error is not None: - raise self._updates_error - return result - except KeyboardInterrupt: - pass - finally: - await self.disconnect() - - async def set_receive_updates(self: 'TelegramClient', receive_updates): - """ - Change the value of `receive_updates`. - - This is an `async` method, because in order for Telegram to start - sending updates again, a request must be made. - """ - self._no_updates = not receive_updates - if receive_updates: - await self(functions.updates.GetStateRequest()) - - def run_until_disconnected(self: 'TelegramClient'): - """ - Runs the event loop until the library is disconnected. - - It also notifies Telegram that we want to receive updates - as described in https://core.telegram.org/api/updates. - If an unexpected error occurs during update handling, - the client will disconnect and said error will be raised. - - Manual disconnections can be made by calling `disconnect() - ` - or sending a ``KeyboardInterrupt`` (e.g. by pressing ``Ctrl+C`` on - the console window running the script). - - If a disconnection error occurs (i.e. the library fails to reconnect - automatically), said error will be raised through here, so you have a - chance to ``except`` it on your own code. - - If the loop is already running, this method returns a coroutine - that you should await on your own code. - - .. note:: - - If you want to handle ``KeyboardInterrupt`` in your code, - simply run the event loop in your code too in any way, such as - ``loop.run_forever()`` or ``await client.disconnected`` (e.g. - ``loop.run_until_complete(client.disconnected)``). - - Example - .. code-block:: python - - # Blocks the current task here until a disconnection occurs. - # - # You will still receive updates, since this prevents the - # script from exiting. - await client.run_until_disconnected() - """ - if self.loop.is_running(): - return self._run_until_disconnected() - try: - return self.loop.run_until_complete(self._run_until_disconnected()) - except KeyboardInterrupt: - pass - finally: - # No loop.run_until_complete; it's already syncified - self.disconnect() - - def on(self: 'TelegramClient', event: EventBuilder): - """ - Decorator used to `add_event_handler` more conveniently. - - - Arguments - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - - Example - .. code-block:: python - - from telethon import TelegramClient, events - client = TelegramClient(...) - - # Here we use client.on - @client.on(events.NewMessage) - async def handler(event): - ... - """ - def decorator(f): - self.add_event_handler(f, event) - return f - - return decorator - - def add_event_handler( - self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None): - """ - Registers a new event handler callback. - - The callback will be called when the specified event occurs. - - Arguments - callback (`callable`): - The callable function accepting one parameter to be used. - - Note that if you have used `telethon.events.register` in - the callback, ``event`` will be ignored, and instead the - events you previously registered will be used. - - event (`_EventBuilder` | `type`, optional): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - - If left unspecified, `telethon.events.raw.Raw` (the - :tl:`Update` objects with no further processing) will - be passed instead. - - Example - .. code-block:: python - - from telethon import TelegramClient, events - client = TelegramClient(...) - - async def handler(event): - ... - - client.add_event_handler(handler, events.NewMessage) - """ - builders = events._get_handlers(callback) - if builders is not None: - for event in builders: - self._event_builders.append((event, callback)) - return - - if isinstance(event, type): - event = event() - elif not event: - event = events.Raw() - - self._event_builders.append((event, callback)) - - def remove_event_handler( - self: 'TelegramClient', - callback: Callback, - event: EventBuilder = None) -> int: - """ - Inverse operation of `add_event_handler()`. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - - Example - .. code-block:: python - - @client.on(events.Raw) - @client.on(events.NewMessage) - async def handler(event): - ... - - # Removes only the "Raw" handling - # "handler" will still receive "events.NewMessage" - client.remove_event_handler(handler, events.Raw) - - # "handler" will stop receiving anything - client.remove_event_handler(handler) - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - i = len(self._event_builders) - while i: - i -= 1 - ev, cb = self._event_builders[i] - if cb == callback and (not event or isinstance(ev, event)): - del self._event_builders[i] - found += 1 - - return found - - def list_event_handlers(self: 'TelegramClient')\ - -> 'typing.Sequence[typing.Tuple[Callback, EventBuilder]]': - """ - Lists all registered event handlers. - - Returns - A list of pairs consisting of ``(callback, event)``. - - Example - .. code-block:: python - - @client.on(events.NewMessage(pattern='hello')) - async def on_greeting(event): - '''Greets someone''' - await event.reply('Hi') - - for callback, event in client.list_event_handlers(): - print(id(callback), type(event)) - """ - return [(callback, event) for event, callback in self._event_builders] - - async def catch_up(self: 'TelegramClient'): - """ - "Catches up" on the missed updates while the client was offline. - You should call this method after registering the event handlers - so that the updates it loads can by processed by your script. - - This can also be used to forcibly fetch new updates if there are any. - - Example - .. code-block:: python - - await client.catch_up() - """ - await self._updates_queue.put(types.UpdatesTooLong()) - - # endregion - - # region Private methods - - async def _update_loop(self: 'TelegramClient'): - # If the MessageBox is not empty, the account had to be logged-in to fill in its state. - # This flag is used to propagate the "you got logged-out" error up (but getting logged-out - # can only happen if it was once logged-in). - was_once_logged_in = self._authorized is True or not self._message_box.is_empty() - - self._updates_error = None - try: - if self._catch_up: - # User wants to catch up as soon as the client is up and running, - # so this is the best place to do it. - await self.catch_up() - - updates_to_dispatch = deque() - - while self.is_connected(): - if updates_to_dispatch: - if self._sequential_updates: - await self._dispatch_update(updates_to_dispatch.popleft()) - else: - while updates_to_dispatch: - # TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed - task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft())) - self._event_handler_tasks.add(task) - task.add_done_callback(self._event_handler_tasks.discard) - - continue - - if len(self._mb_entity_cache) >= self._entity_cache_limit: - self._log[__name__].info( - 'In-memory entity cache limit reached (%s/%s), flushing to session', - len(self._mb_entity_cache), - self._entity_cache_limit - ) - self._save_states_and_entities() - self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map) - if len(self._mb_entity_cache) >= self._entity_cache_limit: - warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit') - - self._log[__name__].info( - 'In-memory entity cache at %s/%s after flushing to session', - len(self._mb_entity_cache), - self._entity_cache_limit - ) - - - get_diff = self._message_box.get_difference() - if get_diff: - self._log[__name__].debug('Getting difference for account updates') - try: - diff = await self(get_diff) - except (errors.ServerError, errors.TimeoutError, ValueError) as e: - # Telegram is having issues - self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__) - self._message_box.end_difference() - continue - except (errors.UnauthorizedError, errors.AuthKeyError) as e: - # Not logged in or broken authorization key, can't get difference - self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__) - self._message_box.end_difference() - if was_once_logged_in: - self._updates_error = e - await self.disconnect() - break - continue - except errors.TypeNotFoundError as e: - # User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use - self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e) - self._message_box.end_difference() - self._updates_error = e - await self.disconnect() - break - except OSError as e: - # Network is likely down, but it's unclear for how long. - # If disconnect is called this task will be cancelled along with the sleep. - # If disconnect is not called, getting difference should be retried after a few seconds. - self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e) - await asyncio.sleep(5) - continue - updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache) - if updates: - self._log[__name__].info('Got difference for account updates') - - updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats)) - continue - - get_diff = self._message_box.get_channel_difference(self._mb_entity_cache) - if get_diff: - self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id) - try: - diff = await self(get_diff) - except (errors.UnauthorizedError, errors.AuthKeyError) as e: - # Not logged in or broken authorization key, can't get difference - self._log[__name__].warning( - 'Cannot get difference for channel %s since the account is not logged in: %s', - get_diff.channel.channel_id, type(e).__name__ - ) - self._message_box.end_channel_difference( - get_diff, - PrematureEndReason.TEMPORARY_SERVER_ISSUES, - self._mb_entity_cache - ) - if was_once_logged_in: - self._updates_error = e - await self.disconnect() - break - continue - except errors.TypeNotFoundError as e: - self._log[__name__].warning( - 'Cannot get difference for channel %s since the account is likely misusing the session: %s', - get_diff.channel.channel_id, e - ) - self._message_box.end_channel_difference( - get_diff, - PrematureEndReason.TEMPORARY_SERVER_ISSUES, - self._mb_entity_cache - ) - self._updates_error = e - await self.disconnect() - break - except ( - errors.PersistentTimestampOutdatedError, - errors.PersistentTimestampInvalidError, - errors.ServerError, - errors.TimeoutError, - ValueError - ) as e: - # According to Telegram's docs: - # "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)." - # We can treat this as "empty difference" and not update the local pts. - # Then this same call will be retried when another gap is detected or timeout expires. - # - # Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few - # seconds, but if Telegram is having issues it's probably best to wait for it to send another - # update (hinting it may be okay now) and retry then. - # - # This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts. - # Instead we manually extract the previously-known pts and use that. - # - # For PersistentTimestampInvalidError: - # Somehow our pts is either too new or the server does not know about this. - # We treat this as PersistentTimestampOutdatedError for now. - # TODO investigate why/when this happens and if this is the proper solution - self._log[__name__].warning( - 'Getting difference for channel updates %s caused %s;' - ' ending getting difference prematurely until server issues are resolved', - get_diff.channel.channel_id, type(e).__name__ - ) - self._message_box.end_channel_difference( - get_diff, - PrematureEndReason.TEMPORARY_SERVER_ISSUES, - self._mb_entity_cache - ) - continue - except (errors.ChannelPrivateError, errors.ChannelInvalidError): - # Timeout triggered a get difference, but we have been banned in the channel since then. - # Because we can no longer fetch updates from this channel, we should stop keeping track - # of it entirely. - self._log[__name__].info( - 'Account is now banned in %d so we can no longer fetch updates from it', - get_diff.channel.channel_id - ) - self._message_box.end_channel_difference( - get_diff, - PrematureEndReason.BANNED, - self._mb_entity_cache - ) - continue - except OSError as e: - self._log[__name__].info( - 'Cannot get difference for channel %d since the network is down: %s: %s', - get_diff.channel.channel_id, type(e).__name__, e - ) - await asyncio.sleep(5) - continue - - updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache) - if updates: - self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id) - - updates_to_dispatch.extend(self._preprocess_updates(updates, users, chats)) - continue - - deadline = self._message_box.check_deadlines() - deadline_delay = deadline - get_running_loop().time() - if deadline_delay > 0: - # Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs). - try: - updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay) - except asyncio.TimeoutError: - self._log[__name__].debug('Timeout waiting for updates expired') - continue - else: - continue - - processed = [] - try: - users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed) - except GapError: - continue # get(_channel)_difference will start returning requests - - updates_to_dispatch.extend(self._preprocess_updates(processed, users, chats)) - except asyncio.CancelledError: - pass - except Exception as e: - self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)') - self._updates_error = e - await self.disconnect() - - def _preprocess_updates(self, updates, users, chats): - self._mb_entity_cache.extend(users, chats) - entities = {utils.get_peer_id(x): x - for x in itertools.chain(users, chats)} - for u in updates: - u._entities = entities - return updates - - async def _keepalive_loop(self: 'TelegramClient'): - # Pings' ID don't really need to be secure, just "random" - rnd = lambda: random.randrange(-2**63, 2**63) - while self.is_connected(): - try: - await asyncio.wait_for( - self.disconnected, timeout=60 - ) - continue # We actually just want to act upon timeout - except asyncio.TimeoutError: - pass - except asyncio.CancelledError: - return - except Exception: - continue # Any disconnected exception should be ignored - - # Check if we have any exported senders to clean-up periodically - await self._clean_exported_senders() - - # Don't bother sending pings until the low-level connection is - # ready, otherwise a lot of pings will be batched to be sent upon - # reconnect, when we really don't care about that. - if not self._sender._transport_connected(): - continue - - # We also don't really care about their result. - # Just send them periodically. - try: - self._sender._keepalive_ping(rnd()) - except (ConnectionError, asyncio.CancelledError): - return - - # Entities and cached files are not saved when they are - # inserted because this is a rather expensive operation - # (default's sqlite3 takes ~0.1s to commit changes). Do - # it every minute instead. No-op if there's nothing new. - self._save_states_and_entities() - - self.session.save() - - async def _dispatch_update(self: 'TelegramClient', update): - # TODO only used for AlbumHack, and MessageBox is not really designed for this - others = None - - if not self._mb_entity_cache.self_id: - # Some updates require our own ID, so we must make sure - # that the event builder has offline access to it. Calling - # `get_me()` will cache it under `self._mb_entity_cache`. - # - # It will return `None` if we haven't logged in yet which is - # fine, we will just retry next time anyway. - try: - await self.get_me(input_peer=True) - except OSError: - pass # might not have connection - - built = EventBuilderDict(self, update, others) - for conv_set in self._conversations.values(): - for conv in conv_set: - ev = built[events.NewMessage] - if ev: - conv._on_new_message(ev) - - ev = built[events.MessageEdited] - if ev: - conv._on_edit(ev) - - ev = built[events.MessageRead] - if ev: - conv._on_read(ev) - - if conv._custom: - await conv._check_custom(built) - - for builder, callback in self._event_builders: - event = built[type(builder)] - if not event: - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except errors.AlreadyInConversationError: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" already has an open conversation, ' - 'ignoring new one', name) - except events.StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - - async def _dispatch_event(self: 'TelegramClient', event): - """ - Dispatches a single, out-of-order event. Used by `AlbumHack`. - """ - # We're duplicating a most logic from `_dispatch_update`, but all in - # the name of speed; we don't want to make it worse for all updates - # just because albums may need it. - for builder, callback in self._event_builders: - if isinstance(builder, events.Raw): - continue - if not isinstance(event, builder.Event): - continue - - if not builder.resolved: - await builder.resolve(self) - - filter = builder.filter(event) - if inspect.isawaitable(filter): - filter = await filter - if not filter: - continue - - try: - await callback(event) - except errors.AlreadyInConversationError: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" already has an open conversation, ' - 'ignoring new one', name) - except events.StopPropagation: - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].debug( - 'Event handler "%s" stopped chain of propagation ' - 'for event %s.', name, type(event).__name__ - ) - break - except Exception as e: - if not isinstance(e, asyncio.CancelledError) or self.is_connected(): - name = getattr(callback, '__name__', repr(callback)) - self._log[__name__].exception('Unhandled exception on %s', name) - - async def _handle_auto_reconnect(self: 'TelegramClient'): - # TODO Catch-up - # For now we make a high-level request to let Telegram - # know we are still interested in receiving more updates. - try: - await self.get_me() - except Exception as e: - self._log[__name__].warning('Error executing high-level request ' - 'after reconnect: %s: %s', type(e), e) - - return - try: - self._log[__name__].info( - 'Asking for the current state after reconnect...') - - # TODO consider: - # If there aren't many updates while the client is disconnected - # (I tried with up to 20), Telegram seems to send them without - # asking for them (via updates.getDifference). - # - # On disconnection, the library should probably set a "need - # difference" or "catching up" flag so that any new updates are - # ignored, and then the library should call updates.getDifference - # itself to fetch them. - # - # In any case (either there are too many updates and Telegram - # didn't send them, or there isn't a lot and Telegram sent them - # but we dropped them), we fetch the new difference to get all - # missed updates. I feel like this would be the best solution. - - # If a disconnection occurs, the old known state will be - # the latest one we were aware of, so we can catch up since - # the most recent state we were aware of. - await self.catch_up() - - self._log[__name__].info('Successfully fetched missed updates') - except errors.RPCError as e: - self._log[__name__].warning('Failed to get missed updates after ' - 'reconnect: %r', e) - except Exception: - self._log[__name__].exception( - 'Unhandled exception while getting update difference after reconnect') - - # endregion - - -class EventBuilderDict: - """ - Helper "dictionary" to return events from types and cache them. - """ - def __init__(self, client: 'TelegramClient', update, others): - self.client = client - self.update = update - self.others = others - - def __getitem__(self, builder): - try: - return self.__dict__[builder] - except KeyError: - event = self.__dict__[builder] = builder.build( - self.update, self.others, self.client._self_id) - - if isinstance(event, EventCommon): - event.original_update = self.update - event._entities = self.update._entities - event._set_client(self.client) - elif event: - event._client = self.client - - return event diff --git a/telethon/client/uploads.py b/telethon/client/uploads.py deleted file mode 100644 index 59228a72..00000000 --- a/telethon/client/uploads.py +++ /dev/null @@ -1,789 +0,0 @@ -import hashlib -import io -import itertools -import os -import pathlib -import re -import typing -from io import BytesIO - -from ..crypto import AES - -from .. import utils, helpers, hints -from ..tl import types, functions, custom - -try: - import PIL - import PIL.Image -except ImportError: - PIL = None - - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -class _CacheType: - """Like functools.partial but pretends to be the wrapped class.""" - def __init__(self, cls): - self._cls = cls - - def __call__(self, *args, **kwargs): - return self._cls(*args, file_reference=b'', **kwargs) - - def __eq__(self, other): - return self._cls == other - - -def _resize_photo_if_needed( - file, is_image, width=2560, height=2560, background=(255, 255, 255)): - - # https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254 - if (not is_image - or PIL is None - or (isinstance(file, io.IOBase) and not file.seekable())): - return file - - if isinstance(file, bytes): - file = io.BytesIO(file) - - 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) - try: - kwargs = {'exif': image.info['exif']} - except KeyError: - kwargs = {} - - 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', progressive=True, **kwargs) - 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, - file_size: int = None, - clear_draft: bool = False, - progress_callback: 'hints.ProgressCallback' = None, - reply_to: 'hints.MessageIDLike' = None, - attributes: 'typing.Sequence[types.TypeDocumentAttribute]' = None, - thumb: 'hints.FileLike' = None, - allow_cache: bool = True, - parse_mode: str = (), - formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None, - voice_note: bool = False, - video_note: bool = False, - buttons: typing.Optional['hints.MarkupLike'] = None, - silent: bool = None, - background: bool = None, - supports_streaming: bool = False, - schedule: 'hints.DateLike' = None, - comment_to: 'typing.Union[int, types.Message]' = None, - ttl: int = None, - nosound_video: bool = None, - **kwargs) -> '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`). - - * A :tl:`InputMedia` instance. For example, if you want to - send a dice use :tl:`InputMediaDice`, or if you want to - send a contact use :tl:`InputMediaContact`. - - To send an album, you should provide a list in this parameter. - - If a list or similar is provided, the files in it will be - sent as an album in the order in which they appear, sliced - in chunks of 10 if more than 10 are given. - - caption (`str`, optional): - Optional caption for the sent media message. When sending an - album, the caption may be a list of strings, which will be - assigned to the files pairwise. - - force_document (`bool`, optional): - If left to `False` and the file is a path that ends with - the extension of an image file or a video file, it will be - sent as such. Otherwise always as a document. - - file_size (`int`, optional): - The size of the file to be uploaded if it needs to be uploaded, - which will be determined automatically if not specified. - - If the file size can't be determined beforehand, the entire - file will be read in-memory to find out how large it is. - - clear_draft (`bool`, optional): - Whether the existing draft should be cleared or not. - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - reply_to (`int` | `Message `): - Same as `reply_to` from `send_message`. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - thumb (`str` | `bytes` | `file`, optional): - Optional JPEG thumbnail (for documents). **Telegram will - ignore this parameter** unless you pass a ``.jpg`` file! - - The file must also be small in dimensions and in disk size. - Successful thumbnails were files below 20kB and 320x320px. - Width/height and dimensions/size ratios may be important. - For Telegram to accept a thumbnail, you must provide the - dimensions of the underlying media through ``attributes=`` - with :tl:`DocumentAttributesVideo` or by installing the - optional ``hachoir`` dependency. - - - allow_cache (`bool`, optional): - This parameter currently does nothing, but is kept for - backward-compatibility (and it may get its use back in - the future). - - parse_mode (`object`, optional): - See the `TelegramClient.parse_mode - ` - property for allowed values. Markdown parsing will be used by - default. - - formatting_entities (`list`, optional): - A list of message formatting entities. When provided, the ``parse_mode`` is ignored. - - voice_note (`bool`, optional): - If `True` the audio will be sent as a voice note. - - video_note (`bool`, optional): - If `True` the video will be sent as a video note, - also known as a round video message. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`): - The matrix (list of lists), row list or button to be shown - after sending the message. This parameter will only work if - you have signed in as a bot. You can also pass your own - :tl:`ReplyMarkup` here. - - silent (`bool`, optional): - Whether the message should notify people with sound or not. - Defaults to `False` (send with a notification sound unless - the person has the chat muted). Set it to `True` to alter - this behaviour. - - background (`bool`, optional): - Whether the message should be send in background. - - supports_streaming (`bool`, optional): - Whether the sent video supports streaming or not. Note that - Telegram only recognizes as streamable some formats like MP4, - and others like AVI or MKV will not work. You should convert - these to MP4 before sending if you want them to be streamable. - Unsupported formats will result in ``VideoContentTypeError``. - - schedule (`hints.DateLike`, optional): - If set, the file won't send immediately, and instead - it will be scheduled to be automatically sent at a later - time. - - comment_to (`int` | `Message `, optional): - Similar to ``reply_to``, but replies in the linked group of a - broadcast channel instead (effectively leaving a "comment to" - the specified message). - - This parameter takes precedence over ``reply_to``. If there is - no linked chat, `telethon.errors.sgIdInvalidError` is raised. - - ttl (`int`. optional): - The Time-To-Live of the file (also known as "self-destruct timer" - or "self-destructing media"). If set, files can only be viewed for - a short period of time before they disappear from the message - history automatically. - - The value must be at least 1 second, and at most 60 seconds, - otherwise Telegram will ignore this parameter. - - Not all types of media can be used with this parameter, such - as text documents, which will fail with ``TtlMediaInvalidError``. - - nosound_video (`bool`, optional): - Only applicable when sending a video file without an audio - track. If set to ``True``, the video will be displayed in - Telegram as a video. If set to ``False``, Telegram will attempt - to display the video as an animated gif. (It may still display - as a video due to other factors.) The value is ignored if set - on non-video files. This is set to ``True`` for albums, as gifs - cannot be sent in albums. - - Returns - The `Message ` (or messages) - containing the sent file, or messages if a list of them was passed. - - Example - .. code-block:: python - - # Normal files like photos - await client.send_file(chat, '/my/photos/me.jpg', caption="It's me!") - # or - await client.send_message(chat, "It's me!", file='/my/photos/me.jpg') - - # Voice notes or round videos - await client.send_file(chat, '/my/songs/song.mp3', voice_note=True) - await client.send_file(chat, '/my/videos/video.mp4', video_note=True) - - # Custom thumbnails - await client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg') - - # Only documents - await client.send_file(chat, '/my/photos/photo.png', force_document=True) - - # Albums - await client.send_file(chat, [ - '/my/photos/holiday1.jpg', - '/my/photos/holiday2.jpg', - '/my/drawings/portrait.png' - ]) - - # Printing upload progress - def callback(current, total): - print('Uploaded', current, 'out of', total, - 'bytes: {:.2%}'.format(current / total)) - - await client.send_file(chat, file, progress_callback=callback) - - # Dices, including dart and other future emoji - from telethon.tl import types - await client.send_file(chat, types.InputMediaDice('')) - await client.send_file(chat, types.InputMediaDice('🎯')) - - # Contacts - await client.send_file(chat, types.InputMediaContact( - phone_number='+34 123 456 789', - first_name='Example', - last_name='', - vcard='' - )) - """ - # TODO Properly implement allow_cache to reuse the sha256 of the file - # i.e. `None` was used - if not file: - raise TypeError('Cannot use {!r} as file'.format(file)) - - if not caption: - caption = '' - - entity = await self.get_input_entity(entity) - if comment_to is not None: - entity, reply_to = await self._get_comment_data(entity, comment_to) - else: - reply_to = utils.get_message_id(reply_to) - - # First check if the user passed an iterable, in which case - # we may want to send grouped. - if utils.is_list_like(file): - sent_count = 0 - used_callback = None if not progress_callback else ( - lambda s, t: progress_callback(sent_count + s, len(file)) - ) - - if utils.is_list_like(caption): - captions = caption - else: - captions = [caption] - - result = [] - while file: - result += await self._send_album( - entity, file[:10], caption=captions[:10], - progress_callback=used_callback, reply_to=reply_to, - parse_mode=parse_mode, silent=silent, schedule=schedule, - supports_streaming=supports_streaming, clear_draft=clear_draft, - force_document=force_document, background=background, - ) - file = file[10:] - captions = captions[10:] - sent_count += 10 - - return result - - if formatting_entities is not None: - msg_entities = formatting_entities - else: - caption, msg_entities =\ - await self._parse_message_text(caption, parse_mode) - - file_handle, media, image = await self._file_to_media( - file, force_document=force_document, - file_size=file_size, - progress_callback=progress_callback, - attributes=attributes, allow_cache=allow_cache, thumb=thumb, - voice_note=voice_note, video_note=video_note, - supports_streaming=supports_streaming, ttl=ttl, - nosound_video=nosound_video, - ) - - # e.g. invalid cast from :tl:`MessageMediaWebPage` - if not media: - raise TypeError('Cannot use {!r} as file'.format(file)) - - markup = self.build_reply_markup(buttons) - request = functions.messages.SendMediaRequest( - entity, media, reply_to_msg_id=reply_to, message=caption, - entities=msg_entities, reply_markup=markup, silent=silent, - schedule_date=schedule, clear_draft=clear_draft, - background=background - ) - return self._get_response_message(request, await self(request), entity) - - async def _send_album(self: 'TelegramClient', entity, files, caption='', - progress_callback=None, reply_to=None, - parse_mode=(), silent=None, schedule=None, - supports_streaming=None, clear_draft=None, - force_document=False, background=None, ttl=None): - """Specialized version of .send_file for albums""" - # We don't care if the user wants to avoid cache, we will use it - # anyway. Why? The cached version will be exactly the same thing - # we need to produce right now to send albums (uploadMedia), and - # cache only makes a difference for documents where the user may - # want the attributes used on them to change. - # - # In theory documents can be sent inside the albums but they appear - # as different messages (not inside the album), and the logic to set - # the attributes/avoid cache is already written in .send_file(). - entity = await self.get_input_entity(entity) - if not utils.is_list_like(caption): - caption = (caption,) - - 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) - - used_callback = None if not progress_callback else ( - # use an integer when sent matches total, to easily determine a file has been fully sent - lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files)) - ) - - # Need to upload the media first, but only if they're not cached yet - media = [] - for sent_count, file in enumerate(files): - # Albums want :tl:`InputMedia` which, in theory, includes - # :tl:`InputMediaUploadedPhoto`. However, using that will - # make it `raise MediaInvalidError`, so we need to upload - # it as media and then convert that to :tl:`InputMediaPhoto`. - fh, fm, _ = await self._file_to_media( - file, supports_streaming=supports_streaming, - force_document=force_document, ttl=ttl, - progress_callback=used_callback, nosound_video=True) - if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)): - r = await self(functions.messages.UploadMediaRequest( - entity, media=fm - )) - - fm = utils.get_input_media(r.photo) - elif isinstance(fm, types.InputMediaUploadedDocument): - r = await self(functions.messages.UploadMediaRequest( - entity, media=fm - )) - - fm = utils.get_input_media( - r.document, supports_streaming=supports_streaming) - - if captions: - caption, msg_entities = captions.pop() - else: - caption, msg_entities = '', None - media.append(types.InputSingleMedia( - fm, - message=caption, - entities=msg_entities - # random_id is autogenerated - )) - - # Now we can construct the multi-media request - request = functions.messages.SendMultiMediaRequest( - entity, reply_to_msg_id=reply_to, multi_media=media, - silent=silent, schedule_date=schedule, clear_draft=clear_draft, - background=background - ) - result = await self(request) - - random_ids = [m.random_id for m in media] - return self._get_response_message(random_ids, result, entity) - - async def upload_file( - self: 'TelegramClient', - file: 'hints.FileLike', - *, - part_size_kb: float = None, - file_size: int = None, - file_name: str = None, - use_cache: type = None, - key: bytes = None, - iv: bytes = None, - progress_callback: 'hints.ProgressCallback' = None) -> 'types.TypeInputFile': - """ - Uploads a file to Telegram's servers, without sending it. - - .. note:: - - Generally, you want to use `send_file` instead. - - This method returns a handle (an instance of :tl:`InputFile` or - :tl:`InputFileBig`, as required) which can be later used before - it expires (they are usable during less than a day). - - Uploading a file will simply return a "handle" to the file stored - remotely in the Telegram servers, which can be later used on. This - will **not** upload the file to your own chat or any chat at all. - - Arguments - file (`str` | `bytes` | `file`): - The path of the file, byte array, or stream that will be sent. - Note that if a byte array or a stream is given, a filename - or its type won't be inferred, and it will be sent as an - "unnamed application/octet-stream". - - part_size_kb (`int`, optional): - Chunk size when uploading files. The larger, the less - requests will be made (up to 512KB maximum). - - file_size (`int`, optional): - The size of the file to be uploaded, which will be determined - automatically if not specified. - - If the file size can't be determined beforehand, the entire - file will be read in-memory to find out how large it is. - - file_name (`str`, optional): - The file name which will be used on the resulting InputFile. - If not specified, the name will be taken from the ``file`` - and if this is not a `str`, it will be ``"unnamed"``. - - use_cache (`type`, optional): - This parameter currently does nothing, but is kept for - backward-compatibility (and it may get its use back in - the future). - - key ('bytes', optional): - In case of an encrypted upload (secret chats) a key is supplied - - iv ('bytes', optional): - In case of an encrypted upload (secret chats) an iv is supplied - - progress_callback (`callable`, optional): - A callback function accepting two parameters: - ``(sent bytes, total)``. - - When sending an album, the callback will receive a number - between 0 and the amount of files as the "sent" parameter, - and the amount of files as the "total". Note that the first - parameter will be a floating point number to indicate progress - within a file (e.g. ``2.5`` means it has sent 50% of the third - file, because it's between 2 and 3). - - Returns - :tl:`InputFileBig` if the file size is larger than 10MB, - `InputSizedFile ` - (subclass of :tl:`InputFile`) otherwise. - - Example - .. code-block:: python - - # Photos as photo and document - file = await client.upload_file('photo.jpg') - await client.send_file(chat, file) # sends as photo - await client.send_file(chat, file, force_document=True) # sends as document - - file.name = 'not a photo.jpg' - await client.send_file(chat, file, force_document=True) # document, new name - - # As song or as voice note - file = await client.upload_file('song.ogg') - await client.send_file(chat, file) # sends as song - await client.send_file(chat, file, voice_note=True) # sends as voice note - """ - if isinstance(file, (types.InputFile, types.InputFileBig)): - return file # Already uploaded - - pos = 0 - async with helpers._FileStream(file, file_size=file_size) as stream: - # Opening the stream will determine the correct file size - file_size = stream.file_size - - if not part_size_kb: - part_size_kb = utils.get_appropriated_part_size(file_size) - - if part_size_kb > 512: - raise ValueError('The part size must be less or equal to 512KB') - - part_size = int(part_size_kb * 1024) - if part_size % 1024 != 0: - raise ValueError( - 'The part size must be evenly divisible by 1024') - - # Set a default file name if None was specified - file_id = helpers.generate_random_long() - if not file_name: - file_name = stream.name or str(file_id) - - # If the file name lacks extension, add it if possible. - # Else Telegram complains with `PHOTO_EXT_INVALID_ERROR` - # even if the uploaded image is indeed a photo. - if not os.path.splitext(file_name)[-1]: - file_name += utils._get_extension(stream) - - # Determine whether the file is too big (over 10MB) or not - # Telegram does make a distinction between smaller or larger files - is_big = file_size > 10 * 1024 * 1024 - hash_md5 = hashlib.md5() - - part_count = (file_size + part_size - 1) // part_size - self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d', - file_size, part_count, part_size) - - pos = 0 - for part_index in range(part_count): - # Read the file by in chunks of size part_size - part = await helpers._maybe_await(stream.read(part_size)) - - if not isinstance(part, bytes): - raise TypeError( - 'file descriptor returned {}, not bytes (you must ' - 'open the file in bytes mode)'.format(type(part))) - - # `file_size` could be wrong in which case `part` may not be - # `part_size` before reaching the end. - if len(part) != part_size and part_index < part_count - 1: - raise ValueError( - 'read less than {} before reaching the end; either ' - '`file_size` or `read` are wrong'.format(part_size)) - - pos += len(part) - - # Encryption part if needed - if key and iv: - part = AES.encrypt_ige(part, key, iv) - - if not is_big: - # Bit odd that MD5 is only needed for small files and not - # big ones with more chance for corruption, but that's - # what Telegram wants. - hash_md5.update(part) - - # The SavePartRequest is different depending on whether - # the file is too large or not (over or less than 10MB) - if is_big: - request = functions.upload.SaveBigFilePartRequest( - file_id, part_index, part_count, part) - else: - request = functions.upload.SaveFilePartRequest( - file_id, part_index, part) - - result = await self(request) - if result: - self._log[__name__].debug('Uploaded %d/%d', - part_index + 1, part_count) - if progress_callback: - await helpers._maybe_await(progress_callback(pos, file_size)) - else: - raise RuntimeError( - 'Failed to upload file part {}.'.format(part_index)) - - if is_big: - return types.InputFileBig(file_id, part_count, file_name) - else: - return custom.InputSizedFile( - file_id, part_count, file_name, md5=hash_md5, size=file_size - ) - - # endregion - - async def _file_to_media( - self, file, force_document=False, file_size=None, - progress_callback=None, attributes=None, thumb=None, - allow_cache=True, voice_note=False, video_note=False, - supports_streaming=False, mime_type=None, as_image=None, - ttl=None, nosound_video=None): - if not file: - return None, None, None - - if isinstance(file, pathlib.Path): - file = str(file.absolute()) - - is_image = utils.is_image(file) - if as_image is None: - as_image = is_image and not force_document - - # `aiofiles` do not base `io.IOBase` but do have `read`, so we - # just check for the read attribute to see if it's file-like. - if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\ - and not hasattr(file, 'read'): - # The user may pass a Message containing media (or the media, - # or anything similar) that should be treated as a file. Try - # getting the input media for whatever they passed and send it. - # - # We pass all attributes since these will be used if the user - # passed :tl:`InputFile`, and all information may be relevant. - try: - return (None, utils.get_input_media( - file, - is_photo=as_image, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - ttl=ttl - ), as_image) - except TypeError: - # Can't turn whatever was given into media - return None, None, as_image - - media = None - file_handle = None - - if isinstance(file, (types.InputFile, types.InputFileBig)): - file_handle = file - elif not isinstance(file, str) or os.path.isfile(file): - file_handle = await self.upload_file( - _resize_photo_if_needed(file, as_image), - file_size=file_size, - progress_callback=progress_callback - ) - elif re.match('https?://', file): - if as_image: - media = types.InputMediaPhotoExternal(file, ttl_seconds=ttl) - else: - media = types.InputMediaDocumentExternal(file, ttl_seconds=ttl) - else: - bot_file = utils.resolve_bot_file_id(file) - if bot_file: - media = utils.get_input_media(bot_file, ttl=ttl) - - if media: - pass # Already have media, don't check the rest - elif not file_handle: - raise ValueError( - 'Failed to convert {} to media. Not an existing file, ' - 'an HTTP URL or a valid bot-API-like file ID'.format(file) - ) - elif as_image: - media = types.InputMediaUploadedPhoto(file_handle, ttl_seconds=ttl) - else: - attributes, mime_type = utils.get_attributes( - file, - mime_type=mime_type, - attributes=attributes, - force_document=force_document and not is_image, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming, - thumb=thumb - ) - - if not thumb: - thumb = None - else: - if isinstance(thumb, pathlib.Path): - thumb = str(thumb.absolute()) - thumb = await self.upload_file(thumb, file_size=file_size) - - # setting `nosound_video` to `True` doesn't affect videos with sound - # instead it prevents sending silent videos as GIFs - nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None - - media = types.InputMediaUploadedDocument( - file=file_handle, - mime_type=mime_type, - attributes=attributes, - thumb=thumb, - force_file=force_document and not is_image, - ttl_seconds=ttl, - nosound_video=nosound_video - ) - return file_handle, media, as_image - - # endregion diff --git a/telethon/client/users.py b/telethon/client/users.py deleted file mode 100644 index eda3e040..00000000 --- a/telethon/client/users.py +++ /dev/null @@ -1,612 +0,0 @@ -import asyncio -import datetime -import itertools -import time -import typing - -from .. import errors, helpers, utils, hints -from ..errors import MultiError, RPCError -from ..helpers import retry_range -from ..tl import TLRequest, types, functions - -_NOT_A_REQUEST = lambda: TypeError('You can only invoke requests, not types!') - -if typing.TYPE_CHECKING: - from .telegramclient import TelegramClient - - -def _fmt_flood(delay, request, *, early=False, td=datetime.timedelta): - return ( - 'Sleeping%s for %ds (%s) on %s flood wait', - ' early' if early else '', - delay, - td(seconds=delay), - request.__class__.__name__ - ) - - -class UserMethods: - async def __call__(self: 'TelegramClient', request, ordered=False, flood_sleep_threshold=None): - return await self._call(self._sender, request, ordered=ordered) - - async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None): - if self._loop is not None and self._loop != helpers.get_running_loop(): - raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)') - # if the loop is None it will fail with a connection error later on - - if flood_sleep_threshold is None: - flood_sleep_threshold = self.flood_sleep_threshold - requests = (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 <= flood_sleep_threshold: - self._log[__name__].info(*_fmt_flood(diff, r, early=True)) - await asyncio.sleep(diff) - self._flood_waited_requests.pop(r.CONSTRUCTOR_ID, None) - else: - raise errors.FloodWaitError(request=r, capture=diff) - - if self._no_updates: - r = functions.InvokeWithoutUpdatesRequest(r) - - request_index = 0 - last_error = None - self._last_request = time.time() - - for attempt in retry_range(self._request_retries): - try: - future = sender.send(request, ordered=ordered) - if isinstance(future, list): - results = [] - exceptions = [] - for f in future: - try: - result = await f - except RPCError as e: - exceptions.append(e) - results.append(None) - continue - self.session.process_entities(result) - exceptions.append(None) - results.append(result) - request_index += 1 - if any(x is not None for x in exceptions): - raise MultiError(exceptions, results, requests) - else: - return results - else: - result = await future - self.session.process_entities(result) - return result - except (errors.ServerError, errors.RpcCallFailError, - errors.RpcMcgetFailError, errors.InterdcCallErrorError, - errors.InterdcCallRichErrorError) as e: - last_error = e - self._log[__name__].warning( - 'Telegram is having internal issues %s: %s', - e.__class__.__name__, e) - - await asyncio.sleep(2) - except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e: - last_error = e - if utils.is_list_like(request): - request = request[request_index] - - # SLOW_MODE_WAIT is chat-specific, not request-specific - if not isinstance(e, errors.SlowModeWaitError): - self._flood_waited_requests\ - [request.CONSTRUCTOR_ID] = time.time() + e.seconds - - # In test servers, FLOOD_WAIT_0 has been observed, and sleeping for - # such a short amount will cause retries very fast leading to issues. - if e.seconds == 0: - e.seconds = 1 - - if e.seconds <= self.flood_sleep_threshold: - self._log[__name__].info(*_fmt_flood(e.seconds, request)) - await asyncio.sleep(e.seconds) - else: - raise - except (errors.PhoneMigrateError, errors.NetworkMigrateError, - errors.UserMigrateError) as e: - last_error = e - self._log[__name__].info('Phone migrated to %d', e.new_dc) - should_raise = isinstance(e, ( - errors.PhoneMigrateError, errors.NetworkMigrateError - )) - if should_raise and await self.is_user_authorized(): - raise - await self._switch_dc(e.new_dc) - - if self._raise_last_call_error and last_error is not None: - raise last_error - raise ValueError('Request was unsuccessful {} time(s)' - .format(attempt)) - - # region Public methods - - async def get_me(self: 'TelegramClient', input_peer: bool = False) \ - -> 'typing.Union[types.User, types.InputPeerUser]': - """ - Gets "me", the current :tl:`User` who is logged in. - - If the user has not logged in yet, this method returns `None`. - - Arguments - input_peer (`bool`, optional): - Whether to return the :tl:`InputPeerUser` version or the normal - :tl:`User`. This can be useful if you just need to know the ID - of yourself. - - Returns - Your own :tl:`User`. - - Example - .. code-block:: python - - me = await client.get_me() - print(me.username) - """ - if input_peer and self._mb_entity_cache.self_id: - return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer() - - try: - me = (await self( - functions.users.GetUsersRequest([types.InputUserSelf()])))[0] - - if not self._mb_entity_cache.self_id: - self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash) - - return utils.get_input_peer(me, allow_self=False) if input_peer else me - except errors.UnauthorizedError: - return None - - @property - def _self_id(self: 'TelegramClient') -> typing.Optional[int]: - """ - Returns the ID of the logged-in user, if known. - - This property is used in every update, and some like `updateLoginToken` - occur prior to login, so it gracefully handles when no ID is known yet. - """ - return self._mb_entity_cache.self_id - - async def is_bot(self: 'TelegramClient') -> bool: - """ - Return `True` if the signed-in user is a bot, `False` otherwise. - - Example - .. code-block:: python - - if await client.is_bot(): - print('Beep') - else: - print('Hello') - """ - if self._mb_entity_cache.self_bot is None: - await self.get_me(input_peer=True) - - return self._mb_entity_cache.self_bot - - async def is_user_authorized(self: 'TelegramClient') -> bool: - """ - Returns `True` if the user is authorized (logged in). - - Example - .. code-block:: python - - if not await client.is_user_authorized(): - await client.send_code_request(phone) - code = input('enter code: ') - await client.sign_in(phone, code) - """ - if self._authorized is None: - try: - # Any request that requires authorization will work - await self(functions.updates.GetStateRequest()) - self._authorized = True - except errors.RPCError: - self._authorized = False - - return self._authorized - - async def get_entity( - self: 'TelegramClient', - entity: 'hints.EntitiesLike') -> 'hints.Entity': - """ - Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat` - or :tl:`Channel`. You can also pass a list or iterable of entities, - and they will be efficiently fetched from the network. - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username is given, **the username will be resolved** making - an API call every time. Resolving usernames is an expensive - operation and will start hitting flood waits around 50 usernames - in a short period of time. - - If you want to get the entity for a *cached* username, you should - first `get_input_entity(username) ` which will - use the cache), and then use `get_entity` with the result of the - previous call. - - Similar limits apply to invite links, and you should use their - ID instead. - - Using phone numbers (from people in your contact list), exact - names, integer IDs or :tl:`Peer` rely on a `get_input_entity` - first, which in turn needs the entity to be in cache, unless - a :tl:`InputPeer` was passed. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`User`, :tl:`Chat` or :tl:`Channel` corresponding to the - input entity. A list will be returned if more than one was given. - - Example - .. code-block:: python - - from telethon import utils - - me = await client.get_entity('me') - print(utils.get_display_name(me)) - - chat = await client.get_input_entity('username') - async for message in client.iter_messages(chat): - ... - - # Note that you could have used the username directly, but it's - # good to use get_input_entity if you will reuse it a lot. - async for message in client.iter_messages('username'): - ... - - # Note that for this to work the phone number must be in your contacts - some_id = await client.get_peer_id('+34123456789') - """ - single = not utils.is_list_like(entity) - if single: - entity = (entity,) - - # Group input entities by string (resolve username), - # input users (get users), input chat (get chats) and - # input channels (get channels) to get the most entities - # in the less amount of calls possible. - inputs = [] - for x in entity: - if isinstance(x, str): - inputs.append(x) - else: - inputs.append(await self.get_input_entity(x)) - - lists = { - helpers._EntityType.USER: [], - helpers._EntityType.CHAT: [], - helpers._EntityType.CHANNEL: [], - } - for x in inputs: - try: - lists[helpers._entity_type(x)].append(x) - except TypeError: - pass - - users = lists[helpers._EntityType.USER] - chats = lists[helpers._EntityType.CHAT] - channels = lists[helpers._EntityType.CHANNEL] - if users: - # GetUsersRequest has a limit of 200 per call - tmp = [] - while users: - curr, users = users[:200], users[200:] - tmp.extend(await self(functions.users.GetUsersRequest(curr))) - users = tmp - if chats: # TODO Handle chats slice? - chats = (await self( - functions.messages.GetChatsRequest([x.chat_id for x in chats]))).chats - if channels: - channels = (await self( - functions.channels.GetChannelsRequest(channels))).chats - - # Merge users, chats and channels into a single dictionary - id_entity = { - # `get_input_entity` might've guessed the type from a non-marked ID, - # so the only way to match that with the input is by not using marks here. - utils.get_peer_id(x, add_mark=False): x - for x in itertools.chain(users, chats, channels) - } - - # We could check saved usernames and put them into the users, - # chats and channels list from before. While this would reduce - # the amount of ResolveUsername calls, it would fail to catch - # username changes. - result = [] - for x in inputs: - if isinstance(x, str): - result.append(await self._get_entity_from_string(x)) - elif not isinstance(x, types.InputPeerSelf): - result.append(id_entity[utils.get_peer_id(x, add_mark=False)]) - else: - result.append(next( - u for u in id_entity.values() - if isinstance(u, types.User) and u.is_self - )) - - return result[0] if single else result - - async def get_input_entity( - self: 'TelegramClient', - peer: 'hints.EntityLike') -> 'types.TypeInputPeer': - """ - Turns the given entity into its input entity version. - - Most requests use this kind of :tl:`InputPeer`, so this is the most - suitable call to make for those cases. **Generally you should let the - library do its job** and don't worry about getting the input entity - first, but if you're going to use an entity often, consider making the - call: - - Arguments - entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`): - If a username or invite link is given, **the library will - use the cache**. This means that it's possible to be using - a username that *changed* or an old invite link (this only - happens if an invite link for a small group chat is used - after it was upgraded to a mega-group). - - If the username or ID from the invite link is not found in - the cache, it will be fetched. The same rules apply to phone - numbers (``'+34 123456789'``) from people in your contact list. - - If an exact name is given, it must be in the cache too. This - is not reliable as different people can share the same name - and which entity is returned is arbitrary, and should be used - only for quick tests. - - If a positive integer ID is given, the entity will be searched - in cached users, chats or channels, without making any call. - - If a negative integer ID is given, the entity will be searched - exactly as either a chat (prefixed with ``-``) or as a channel - (prefixed with ``-100``). - - If a :tl:`Peer` is given, it will be searched exactly in the - cache as either a user, chat or channel. - - If the given object can be turned into an input entity directly, - said operation will be done. - - Unsupported types will raise ``TypeError``. - - If the entity can't be found, ``ValueError`` will be raised. - - Returns - :tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel` - or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``. - - If you need to get the ID of yourself, you should use - `get_me` with ``input_peer=True``) instead. - - Example - .. code-block:: python - - # If you're going to use "username" often in your code - # (make a lot of calls), consider getting its input entity - # once, and then using the "user" everywhere instead. - user = await client.get_input_entity('username') - - # The same applies to IDs, chats or channels. - chat = await client.get_input_entity(-123456789) - """ - # Short-circuit if the input parameter directly maps to an InputPeer - try: - return utils.get_input_peer(peer) - except TypeError: - pass - - # Next in priority is having a peer (or its ID) cached in-memory - try: - # 0x2d45687 == crc32(b'Peer') - if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687: - return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer() - except AttributeError: - pass - - # Then come known strings that take precedence - if peer in ('me', 'self'): - return types.InputPeerSelf() - - # No InputPeer, cached peer, or known string. Fetch from disk cache - try: - return self.session.get_input_entity(peer) - except ValueError: - pass - - # Only network left to try - if isinstance(peer, str): - return utils.get_input_peer( - await self._get_entity_from_string(peer)) - - # If we're a bot and the user has messaged us privately users.getUsers - # will work with access_hash = 0. Similar for channels.getChannels. - # If we're not a bot but the user is in our contacts, it seems to work - # regardless. These are the only two special-cased requests. - peer = utils.get_peer(peer) - if isinstance(peer, types.PeerUser): - users = await self(functions.users.GetUsersRequest([ - types.InputUser(peer.user_id, access_hash=0)])) - if users and not isinstance(users[0], types.UserEmpty): - # If the user passed a valid ID they expect to work for - # channels but would be valid for users, we get UserEmpty. - # Avoid returning the invalid empty input peer for that. - # - # We *could* try to guess if it's a channel first, and if - # it's not, work as a chat and try to validate it through - # another request, but that becomes too much work. - return utils.get_input_peer(users[0]) - elif isinstance(peer, types.PeerChat): - return types.InputPeerChat(peer.chat_id) - elif isinstance(peer, types.PeerChannel): - try: - channels = await self(functions.channels.GetChannelsRequest([ - types.InputChannel(peer.channel_id, access_hash=0)])) - return utils.get_input_peer(channels.chats[0]) - except errors.ChannelInvalidError: - pass - - raise ValueError( - 'Could not find the input entity for {} ({}). Please read https://' - 'docs.telethon.dev/en/stable/concepts/entities.html to' - ' find out more details.' - .format(peer, type(peer).__name__) - ) - - async def _get_peer(self: 'TelegramClient', peer: 'hints.EntityLike'): - i, cls = utils.resolve_id(await self.get_peer_id(peer)) - return cls(i) - - async def get_peer_id( - self: 'TelegramClient', - peer: 'hints.EntityLike', - add_mark: bool = True) -> int: - """ - Gets the ID for the given entity. - - This method needs to be ``async`` because `peer` supports usernames, - invite-links, phone numbers (from people in your contact list), etc. - - If ``add_mark is False``, then a positive ID will be returned - instead. By default, bot-API style IDs (signed) are returned. - - Example - .. code-block:: python - - print(await client.get_peer_id('me')) - """ - if isinstance(peer, int): - return utils.get_peer_id(peer, add_mark=add_mark) - - try: - if peer.SUBCLASS_OF_ID not in (0x2d45687, 0xc91c90b6): - # 0x2d45687, 0xc91c90b6 == crc32(b'Peer') and b'InputPeer' - peer = await self.get_input_entity(peer) - except AttributeError: - peer = await self.get_input_entity(peer) - - if isinstance(peer, types.InputPeerSelf): - peer = await self.get_me(input_peer=True) - - return utils.get_peer_id(peer, add_mark=add_mark) - - # endregion - - # region Private methods - - async def _get_entity_from_string(self: 'TelegramClient', string): - """ - Gets a full entity from the given string, which may be a phone or - a username, and processes all the found entities on the session. - The string may also be a user link, or a channel/chat invite link. - - This method has the side effect of adding the found users to the - session database, so it can be queried later without API calls, - if this option is enabled on the session. - - Returns the found entity, or raises TypeError if not found. - """ - phone = utils.parse_phone(string) - if phone: - try: - for user in (await self( - functions.contacts.GetContactsRequest(0))).users: - if user.phone == phone: - return user - except errors.BotMethodInvalidError: - raise ValueError('Cannot get entity by phone number as a ' - 'bot (try using integer IDs, not strings)') - elif string.lower() in ('me', 'self'): - return await self.get_me() - else: - username, is_join_chat = utils.parse_username(string) - if is_join_chat: - invite = await self( - functions.messages.CheckChatInviteRequest(username)) - - if isinstance(invite, types.ChatInvite): - raise ValueError( - 'Cannot get entity from a channel (or group) ' - 'that you are not part of. Join the group and retry' - ) - elif isinstance(invite, types.ChatInviteAlready): - return invite.chat - elif username: - try: - result = await self( - functions.contacts.ResolveUsernameRequest(username)) - except errors.UsernameNotOccupiedError as e: - raise ValueError('No user has "{}" as username' - .format(username)) from e - - try: - pid = utils.get_peer_id(result.peer, add_mark=False) - if isinstance(result.peer, types.PeerUser): - return next(x for x in result.users if x.id == pid) - else: - return next(x for x in result.chats if x.id == pid) - except StopIteration: - pass - try: - # Nobody with this username, maybe it's an exact name/title - return await self.get_entity( - self.session.get_input_entity(string)) - except ValueError: - pass - - raise ValueError( - 'Cannot find any entity corresponding to "{}"'.format(string) - ) - - async def _get_input_dialog(self: 'TelegramClient', dialog): - """ - Returns a :tl:`InputDialogPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - dialog.peer = await self.get_input_entity(dialog.peer) - return dialog - elif dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return types.InputDialogPeer(dialog) - except AttributeError: - pass - - return types.InputDialogPeer(await self.get_input_entity(dialog)) - - async def _get_input_notify(self: 'TelegramClient', notify): - """ - Returns a :tl:`InputNotifyPeer`. This is a bit tricky because - it may or not need access to the client to convert what's given - into an input entity. - """ - try: - if notify.SUBCLASS_OF_ID == 0x58981615: - if isinstance(notify, types.InputNotifyPeer): - notify.peer = await self.get_input_entity(notify.peer) - return notify - except AttributeError: - pass - - return types.InputNotifyPeer(await self.get_input_entity(notify)) - - # endregion diff --git a/telethon/crypto/__init__.py b/telethon/crypto/__init__.py deleted file mode 100644 index 69be1da8..00000000 --- a/telethon/crypto/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index 3cfcc1af..00000000 --- a/telethon/crypto/aes.py +++ /dev/null @@ -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 deleted file mode 100644 index 34422904..00000000 --- a/telethon/crypto/aesctr.py +++ /dev/null @@ -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 deleted file mode 100644 index 8475ec17..00000000 --- a/telethon/crypto/authkey.py +++ /dev/null @@ -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 deleted file mode 100644 index e0fea00e..00000000 --- a/telethon/crypto/libssl.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Helper module around the system's libssl library if available for IGE mode. -""" -import ctypes -import ctypes.util -import platform -import sys -try: - import ctypes.macholib.dyld -except ImportError: - pass -import logging -import os - -__log__ = logging.getLogger(__name__) - - -def _find_ssl_lib(): - lib = ctypes.util.find_library('ssl') - # macOS 10.15 segfaults on unversioned crypto libraries. - # We therefore pin the current stable version here - # Credit for fix goes to Sarah Harvey (@worldwise001) - # https://www.shh.sh/2020/01/04/python-abort-trap-6.html - if sys.platform == 'darwin': - release, _version_info, _machine = platform.mac_ver() - ver, major, *_ = release.split('.') - # macOS 10.14 "mojave" is the last known major release - # to support unversioned libssl.dylib. Anything above - # needs specific versions - if int(ver) > 10 or int(ver) == 10 and int(major) > 14: - lib = ( - ctypes.util.find_library('libssl.46') or - ctypes.util.find_library('libssl.44') or - ctypes.util.find_library('libssl.42') - ) - if not lib: - raise OSError('no library called "ssl" found') - - # First, let ctypes try to handle it itself. - try: - libssl = ctypes.cdll.LoadLibrary(lib) - except OSError: - pass - else: - return libssl - - # This is a best-effort attempt at finding the full real path of lib. - # - # Unfortunately ctypes doesn't tell us *where* it finds the library, - # so we have to do that ourselves. - try: - # This is not documented, so it could fail. Be on the safe side. - paths = ctypes.macholib.dyld.DEFAULT_LIBRARY_FALLBACK - except AttributeError: - paths = [ - os.path.expanduser("~/lib"), - "/usr/local/lib", - "/lib", - "/usr/lib", - ] - - for path in paths: - if os.path.isdir(path): - for root, _, files in os.walk(path): - if lib in files: - # Manually follow symbolic links on *nix systems. - # Fix for https://github.com/LonamiWebs/Telethon/issues/1167 - lib = os.path.realpath(os.path.join(root, lib)) - return ctypes.cdll.LoadLibrary(lib) - else: - raise OSError('no absolute path for "%s" and cannot load by name' % lib) - - -try: - _libssl = _find_ssl_lib() -except OSError as e: - # See https://github.com/LonamiWebs/Telethon/issues/1167 - # Sometimes `find_library` returns improper filenames. - __log__.info('Failed to load SSL library: %s (%s)', type(e), e) - _libssl = None - -if not _libssl: - decrypt_ige = None - encrypt_ige = None -else: - # https://github.com/openssl/openssl/blob/master/include/openssl/aes.h - AES_ENCRYPT = ctypes.c_int(1) - AES_DECRYPT = ctypes.c_int(0) - AES_MAXNR = 14 - - class AES_KEY(ctypes.Structure): - """Helper class representing an AES key""" - _fields_ = [ - ('rd_key', ctypes.c_uint32 * (4 * (AES_MAXNR + 1))), - ('rounds', ctypes.c_uint), - ] - - def decrypt_ige(cipher_text, key, iv): - aes_key = AES_KEY() - key_len = ctypes.c_int(8 * len(key)) - key = (ctypes.c_ubyte * len(key))(*key) - iv = (ctypes.c_ubyte * len(iv))(*iv) - - in_len = ctypes.c_size_t(len(cipher_text)) - in_ptr = (ctypes.c_ubyte * len(cipher_text))(*cipher_text) - out_ptr = (ctypes.c_ubyte * len(cipher_text))() - - _libssl.AES_set_decrypt_key(key, key_len, ctypes.byref(aes_key)) - _libssl.AES_ige_encrypt( - ctypes.byref(in_ptr), - ctypes.byref(out_ptr), - in_len, - ctypes.byref(aes_key), - ctypes.byref(iv), - AES_DECRYPT - ) - - return bytes(out_ptr) - - def encrypt_ige(plain_text, key, iv): - aes_key = AES_KEY() - key_len = ctypes.c_int(8 * len(key)) - key = (ctypes.c_ubyte * len(key))(*key) - iv = (ctypes.c_ubyte * len(iv))(*iv) - - in_len = ctypes.c_size_t(len(plain_text)) - in_ptr = (ctypes.c_ubyte * len(plain_text))(*plain_text) - out_ptr = (ctypes.c_ubyte * len(plain_text))() - - _libssl.AES_set_encrypt_key(key, key_len, ctypes.byref(aes_key)) - _libssl.AES_ige_encrypt( - ctypes.byref(in_ptr), - ctypes.byref(out_ptr), - in_len, - ctypes.byref(aes_key), - ctypes.byref(iv), - AES_ENCRYPT - ) - - return bytes(out_ptr) diff --git a/telethon/crypto/rsa.py b/telethon/crypto/rsa.py deleted file mode 100644 index 91ca7bad..00000000 --- a/telethon/crypto/rsa.py +++ /dev/null @@ -1,165 +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, old)} dictionary -_server_keys = {} - - -def get_byte_array(integer): - """Return the variable length bytes corresponding to the given int""" - # Operate in big endian (unlike most of Telegram API) since: - # > "...pq is a representation of a natural number - # (in binary *big endian* format)..." - # > "...current value of dh_prime equals - # (in *big-endian* byte order)..." - # Reference: https://core.telegram.org/mtproto/auth_key - return int.to_bytes( - integer, - (integer.bit_length() + 8 - 1) // 8, # 8 bits per byte, - byteorder='big', - signed=False - ) - - -def _compute_fingerprint(key): - """ - Given a RSA key, computes its fingerprint like Telegram does. - - :param key: the Crypto.RSA key. - :return: its 8-bytes-long fingerprint. - """ - n = TLObject.serialize_bytes(get_byte_array(key.n)) - e = TLObject.serialize_bytes(get_byte_array(key.e)) - # Telegram uses the last 8 bytes as the fingerprint - return struct.unpack('>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage) - ... async def delete(event): - ... await event.delete() - ... # No other event handler will have a chance to handle this event - ... raise StopPropagation - ... - >>> @client.on(events.NewMessage) - ... async def _(event): - ... # Will never be reached, because it is the second handler - ... pass - """ - # For some reason Sphinx wants the silly >>> or - # it will show warnings and look bad when generated. - pass - - -def register(event=None): - """ - Decorator method to *register* event handlers. This is the client-less - `add_event_handler() - ` variant. - - Note that this method only registers callbacks as handlers, - and does not attach them to any client. This is useful for - external modules that don't have access to the client, but - still want to define themselves as a handler. Example: - - >>> from telethon import events - >>> @events.register(events.NewMessage) - ... async def handler(event): - ... ... - ... - >>> # (somewhere else) - ... - >>> from telethon import TelegramClient - >>> client = TelegramClient(...) - >>> client.add_event_handler(handler) - - Remember that you can use this as a non-decorator - through ``register(event)(callback)``. - - Args: - event (`_EventBuilder` | `type`): - The event builder class or instance to be used, - for instance ``events.NewMessage``. - """ - if isinstance(event, type): - event = event() - elif not event: - event = Raw() - - def decorator(callback): - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append(event) - setattr(callback, _HANDLERS_ATTRIBUTE, handlers) - return callback - - return decorator - - -def unregister(callback, event=None): - """ - Inverse operation of `register` (though not a decorator). Client-less - `remove_event_handler - ` - variant. **Note that this won't remove handlers from the client**, - because it simply can't, so you would generally use this before - adding the handlers to the client. - - This method is here for symmetry. You will rarely need to - unregister events, since you can simply just not add them - to any client. - - If no event is given, all events for this callback are removed. - Returns how many callbacks were removed. - """ - found = 0 - if event and not isinstance(event, type): - event = type(event) - - handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) - handlers.append((event, callback)) - i = len(handlers) - while i: - i -= 1 - ev = handlers[i] - if not event or isinstance(ev, event): - del handlers[i] - found += 1 - - return found - - -def is_handler(callback): - """ - Returns `True` if the given callback is an - event handler (i.e. you used `register` on it). - """ - return hasattr(callback, _HANDLERS_ATTRIBUTE) - - -def list(callback): - """ - Returns a list containing the registered event - builders inside the specified callback handler. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] - - -def _get_handlers(callback): - """ - Like ``list`` but returns `None` if the callback was never registered. - """ - return getattr(callback, _HANDLERS_ATTRIBUTE, None) diff --git a/telethon/events/album.py b/telethon/events/album.py deleted file mode 100644 index 481fedf4..00000000 --- a/telethon/events/album.py +++ /dev/null @@ -1,343 +0,0 @@ -import asyncio -import time -import weakref - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types -from ..tl.custom.sendergetter import SenderGetter - -_IGNORE_MAX_SIZE = 100 # len() -_IGNORE_MAX_AGE = 5 # seconds - -# IDs to ignore, and when they were added. If it grows too large, we will -# remove old entries. Although it should generally not be bigger than 10, -# it may be possible some updates are not processed and thus not removed. -_IGNORE_DICT = {} - - -_HACK_DELAY = 0.5 - - -class AlbumHack: - """ - When receiving an album from a different data-center, they will come in - separate `Updates`, so we need to temporarily remember them for a while - and only after produce the event. - - Of course events are not designed for this kind of wizardy, so this is - a dirty hack that gets the job done. - - When cleaning up the code base we may want to figure out a better way - to do this, or just leave the album problem to the users; the update - handling code is bad enough as it is. - """ - def __init__(self, client, event): - # It's probably silly to use a weakref here because this object is - # very short-lived but might as well try to do "the right thing". - self._client = weakref.ref(client) - self._event = event # parent event - self._due = client.loop.time() + _HACK_DELAY - - client.loop.create_task(self.deliver_event()) - - def extend(self, messages): - client = self._client() - if client: # weakref may be dead - self._event.messages.extend(messages) - self._due = client.loop.time() + _HACK_DELAY - - async def deliver_event(self): - while True: - client = self._client() - if client is None: - return # weakref is dead, nothing to deliver - - diff = self._due - client.loop.time() - if diff <= 0: - # We've hit our due time, deliver event. It won't respect - # sequential updates but fixing that would just worsen this. - await client._dispatch_event(self._event) - return - - del client # Clear ref and sleep until our due time - await asyncio.sleep(diff) - - -@name_inner_event -class Album(EventBuilder): - """ - Occurs whenever you receive an album. This event only exists - to ease dealing with an unknown amount of messages that belong - to the same album. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.Album) - async def handler(event): - # Counting how many photos or videos the album has - print('Got an album with', len(event), 'items') - - # Forwarding the album as a whole to some chat - event.forward_to(chat) - - # Printing the caption - print(event.text) - - # Replying to the fifth item in the album - await event.messages[4].reply('Cool!') - """ - - def __init__( - self, chats=None, *, blacklist_chats=False, func=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - - @classmethod - def build(cls, update, others=None, self_id=None): - # TODO normally we'd only check updates if they come with other updates - # but MessageBox is not designed for this so others will always be None. - # In essence we always rely on AlbumHack rather than returning early if not others. - others = [update] - - if isinstance(update, - (types.UpdateNewMessage, types.UpdateNewChannelMessage)): - if not isinstance(update.message, types.Message): - return # We don't care about MessageService's here - - group = update.message.grouped_id - if group is None: - return # It must be grouped - - # Check whether we are supposed to skip this update, and - # if we do also remove it from the ignore list since we - # won't need to check against it again. - if _IGNORE_DICT.pop(id(update), None): - return - - # Check if the ignore list is too big, and if it is clean it - # TODO time could technically go backwards; time is not monotonic - now = time.time() - if len(_IGNORE_DICT) > _IGNORE_MAX_SIZE: - for i in [i for i, t in _IGNORE_DICT.items() if now - t > _IGNORE_MAX_AGE]: - del _IGNORE_DICT[i] - - # Add the other updates to the ignore list - for u in others: - if u is not update: - _IGNORE_DICT[id(u)] = now - - # Figure out which updates share the same group and use those - return cls.Event([ - u.message for u in others - if (isinstance(u, (types.UpdateNewMessage, types.UpdateNewChannelMessage)) - and isinstance(u.message, types.Message) - and u.message.grouped_id == group) - ]) - - def filter(self, event): - # Albums with less than two messages require a few hacks to work. - if len(event.messages) > 1: - return super().filter(event) - - class Event(EventCommon, SenderGetter): - """ - Represents the event of a new album. - - Members: - messages (Sequence[`Message `]): - The list of messages belonging to the same album. - """ - def __init__(self, messages): - message = messages[0] - super().__init__(chat_peer=message.peer_id, - msg_id=message.id, broadcast=bool(message.post)) - SenderGetter.__init__(self, message.sender_id) - self.messages = messages - - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._mb_entity_cache) - - for msg in self.messages: - msg._finish_init(client, self._entities, None) - - if len(self.messages) == 1: - # This will require hacks to be a proper album event - hack = client._albums.get(self.grouped_id) - if hack is None: - client._albums[self.grouped_id] = AlbumHack(client, self) - else: - hack.extend(self.messages) - - @property - def grouped_id(self): - """ - The shared ``grouped_id`` between all the messages. - """ - return self.messages[0].grouped_id - - @property - def text(self): - """ - The message text of the first photo with a caption, - formatted using the client's default parse mode. - """ - return next((m.text for m in self.messages if m.text), '') - - @property - def raw_text(self): - """ - The raw message text of the first photo - with a caption, ignoring any formatting. - """ - return next((m.raw_text for m in self.messages if m.raw_text), '') - - @property - def is_reply(self): - """ - `True` if the album is a reply to some other message. - - Remember that you can access the ID of the message - this one is replying to through `reply_to_msg_id`, - and the `Message` object with `get_reply_message()`. - """ - # Each individual message in an album all reply to the same message - return self.messages[0].is_reply - - @property - def forward(self): - """ - The `Forward ` - information for the first message in the album if it was forwarded. - """ - # Each individual message in an album all reply to the same message - return self.messages[0].forward - - # endregion Public Properties - - # region Public Methods - - async def get_reply_message(self): - """ - The `Message ` - that this album is replying to, or `None`. - - The result will be cached after its first use. - """ - return await self.messages[0].get_reply_message() - - async def respond(self, *args, **kwargs): - """ - Responds to the album (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with ``entity`` already set. - """ - return await self.messages[0].respond(*args, **kwargs) - - async def reply(self, *args, **kwargs): - """ - Replies to the first photo in the album (as a reply). Shorthand - for `telethon.client.messages.MessageMethods.send_message` - with both ``entity`` and ``reply_to`` already set. - """ - return await self.messages[0].reply(*args, **kwargs) - - async def forward_to(self, *args, **kwargs): - """ - Forwards the entire album. Shorthand for - `telethon.client.messages.MessageMethods.forward_messages` - with both ``messages`` and ``from_peer`` already set. - """ - if self._client: - kwargs['messages'] = self.messages - kwargs['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) - - async def edit(self, *args, **kwargs): - """ - Edits the first caption or the message, or the first messages' - caption if no caption is set, iff it's outgoing. Shorthand for - `telethon.client.messages.MessageMethods.edit_message` - with both ``entity`` and ``message`` already set. - - Returns `None` if the message was incoming, - or the edited `Message` otherwise. - - .. note:: - - This is different from `client.edit_message - ` - and **will respect** the previous state of the message. - For example, if the message didn't have a link preview, - the edit won't add one by default, and you should force - it by setting it to `True` if you want it. - - This is generally the most desired and convenient behaviour, - and will work for link previews and message buttons. - """ - for msg in self.messages: - if msg.raw_text: - return await msg.edit(*args, **kwargs) - - return await self.messages[0].edit(*args, **kwargs) - - async def delete(self, *args, **kwargs): - """ - Deletes the entire album. You're responsible for checking whether - you have the permission to do so, or to except the error otherwise. - Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - """ - if self._client: - return await self._client.delete_messages( - await self.get_input_chat(), self.messages, - *args, **kwargs - ) - - async def mark_read(self): - """ - Marks the entire album as read. Shorthand for - `client.send_read_acknowledge() - ` - with both ``entity`` and ``message`` already set. - """ - if self._client: - await self._client.send_read_acknowledge( - await self.get_input_chat(), max_id=self.messages[-1].id) - - async def pin(self, *, notify=False): - """ - Pins the first photo in the album. Shorthand for - `telethon.client.messages.MessageMethods.pin_message` - with both ``entity`` and ``message`` already set. - """ - return await self.messages[0].pin(notify=notify) - - def __len__(self): - """ - Return the amount of messages in the album. - - Equivalent to ``len(self.messages)``. - """ - return len(self.messages) - - def __iter__(self): - """ - Iterate over the messages in the album. - - Equivalent to ``iter(self.messages)``. - """ - return iter(self.messages) - - def __getitem__(self, n): - """ - Access the n'th message in the album. - - Equivalent to ``event.messages[n]``. - """ - return self.messages[n] diff --git a/telethon/events/callbackquery.py b/telethon/events/callbackquery.py deleted file mode 100644 index 408e3399..00000000 --- a/telethon/events/callbackquery.py +++ /dev/null @@ -1,344 +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_')``. - - pattern (`bytes`, `str`, `callable`, `Pattern`, optional): - If set, only buttons with payload matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the payload data, a callable function that returns `True` - if a the payload data is acceptable, or a compiled regex pattern. - - Example - .. code-block:: python - - from telethon import events, Button - - # Handle all callback queries and check data inside the handler - @client.on(events.CallbackQuery) - async def handler(event): - if event.data == b'yes': - await event.answer('Correct answer!') - - # Handle only callback queries with data being b'no' - @client.on(events.CallbackQuery(data=b'no')) - async def handler(event): - # Pop-up message with alert - await event.answer('Wrong answer!', alert=True) - - # Send a message with buttons users can click - async def main(): - await client.send_message(user, 'Yes or no?', buttons=[ - Button.inline('Yes!', b'yes'), - Button.inline('Nope', b'no') - ]) - """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, data=None, pattern=None): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - - if data and pattern: - raise ValueError("Only pass either data or pattern not both.") - - if isinstance(data, str): - data = data.encode('utf-8') - if isinstance(pattern, str): - pattern = pattern.encode('utf-8') - - match = data if data else pattern - - if isinstance(match, bytes): - self.match = data if data else re.compile(pattern).match - elif not match or callable(match): - self.match = match - elif hasattr(match, 'match') and callable(match.match): - if not isinstance(getattr(match, 'pattern', b''), bytes): - match = re.compile(match.pattern.encode('utf-8'), - match.flags & (~re.UNICODE)) - - self.match = match.match - else: - raise TypeError('Invalid data or pattern type given') - - self._no_check = all(x is None for x in ( - self.chats, self.func, self.match, - )) - - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateBotCallbackQuery): - return cls.Event(update, update.peer, update.msg_id) - elif isinstance(update, types.UpdateInlineBotCallbackQuery): - # See https://github.com/LonamiWebs/Telethon/pull/1005 - # The long message ID is actually just msg_id + peer_id - mid, pid = struct.unpack('`, - since the message object is normally not present. - """ - self._client.loop.create_task(self.answer()) - if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)): - return await self._client.edit_message( - self.query.msg_id, *args, **kwargs - ) - else: - return await self._client.edit_message( - await self.get_input_chat(), self.query.msg_id, - *args, **kwargs - ) - - async def delete(self, *args, **kwargs): - """ - Deletes the message. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - - If you need to delete more than one message at once, don't use - this `delete` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - - This method also creates a task to `answer` the callback. - - This method will likely fail if `via_inline` is `True`. - """ - self._client.loop.create_task(self.answer()) - 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 deleted file mode 100644 index b8fc3dc2..00000000 --- a/telethon/events/chataction.py +++ /dev/null @@ -1,458 +0,0 @@ -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types - - -@name_inner_event -class ChatAction(EventBuilder): - """ - Occurs on certain chat actions: - - * Whenever a new chat is created. - * Whenever a chat's title or photo is changed or removed. - * Whenever a new message is pinned. - * Whenever a user scores in a game. - * Whenever a user joins or is added to the group. - * Whenever a user is removed or leaves a group if it has - less than 50 members or the removed user was a bot. - - Note that "chat" refers to "small group, megagroup and broadcast - channel", whereas "group" refers to "small group and megagroup" only. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.ChatAction) - async def handler(event): - # Welcome every new user - if event.user_joined: - await event.reply('Welcome to the group!') - """ - - @classmethod - def build(cls, update, others=None, self_id=None): - # Rely on specific pin updates for unpins, but otherwise ignore them - # for new pins (we'd rather handle the new service message with pin, - # so that we can act on that message'). - if isinstance(update, types.UpdatePinnedChannelMessages) and not update.pinned: - return cls.Event(types.PeerChannel(update.channel_id), - pin_ids=update.messages, - pin=update.pinned) - - elif isinstance(update, types.UpdatePinnedMessages) and not update.pinned: - return cls.Event(update.peer, - pin_ids=update.messages, - pin=update.pinned) - - elif isinstance(update, types.UpdateChatParticipantAdd): - return cls.Event(types.PeerChat(update.chat_id), - added_by=update.inviter_id or True, - users=update.user_id) - - elif isinstance(update, types.UpdateChatParticipantDelete): - return cls.Event(types.PeerChat(update.chat_id), - kicked_by=True, - users=update.user_id) - - # UpdateChannel is sent if we leave a channel, and the update._entities - # set by _process_update would let us make some guesses. However it's - # better not to rely on this. Rely only in MessageActionChatDeleteUser. - - elif (isinstance(update, ( - types.UpdateNewMessage, types.UpdateNewChannelMessage)) - and isinstance(update.message, types.MessageService)): - msg = update.message - action = update.message.action - if isinstance(action, types.MessageActionChatJoinedByLink): - return cls.Event(msg, - added_by=True, - users=msg.from_id) - elif isinstance(action, types.MessageActionChatAddUser): - # If a user adds itself, it means they joined via the public chat username - added_by = ([msg.sender_id] == action.users) or msg.from_id - return cls.Event(msg, - added_by=added_by, - users=action.users) - elif isinstance(action, types.MessageActionChatDeleteUser): - return cls.Event(msg, - kicked_by=utils.get_peer_id(msg.from_id) if msg.from_id else True, - users=action.user_id) - elif isinstance(action, types.MessageActionChatCreate): - return cls.Event(msg, - users=action.users, - created=True, - new_title=action.title) - elif isinstance(action, types.MessageActionChannelCreate): - return cls.Event(msg, - created=True, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditTitle): - return cls.Event(msg, - users=msg.from_id, - new_title=action.title) - elif isinstance(action, types.MessageActionChatEditPhoto): - return cls.Event(msg, - users=msg.from_id, - new_photo=action.photo) - elif isinstance(action, types.MessageActionChatDeletePhoto): - return cls.Event(msg, - users=msg.from_id, - new_photo=True) - elif isinstance(action, types.MessageActionPinMessage) and msg.reply_to: - return cls.Event(msg, - pin_ids=[msg.reply_to_msg_id]) - elif isinstance(action, types.MessageActionGameScore): - return cls.Event(msg, - new_score=action.score) - - elif isinstance(update, types.UpdateChannelParticipant) \ - and bool(update.new_participant) != bool(update.prev_participant): - # If members are hidden, bots will receive this update instead, - # as there won't be a service message. Promotions and demotions - # seem to have both new and prev participant, which are ignored - # by this event. - return cls.Event(types.PeerChannel(update.channel_id), - users=update.user_id, - added_by=update.actor_id if update.new_participant else None, - kicked_by=update.actor_id if update.prev_participant else None) - - class Event(EventCommon): - """ - Represents the event of a new chat action. - - Members: - action_message (`MessageAction `_): - The message invoked by this Chat Action. - - new_pin (`bool`): - `True` if there is a new pin. - - new_photo (`bool`): - `True` if there's a new chat photo (or it was removed). - - photo (:tl:`Photo`, optional): - The new photo (or `None` if it was removed). - - user_added (`bool`): - `True` if the user was added by some other. - - user_joined (`bool`): - `True` if the user joined on their own. - - user_left (`bool`): - `True` if the user left on their own. - - user_kicked (`bool`): - `True` if the user was kicked by some other. - - created (`bool`, optional): - `True` if this chat was just created. - - new_title (`str`, optional): - The new title string for the chat, if applicable. - - new_score (`str`, optional): - The new score string for the game, if applicable. - - unpin (`bool`): - `True` if the existing pin gets unpinned. - """ - - def __init__(self, where, new_photo=None, - added_by=None, kicked_by=None, created=None, - users=None, new_title=None, pin_ids=None, pin=None, new_score=None): - if isinstance(where, types.MessageService): - self.action_message = where - where = where.peer_id - else: - self.action_message = None - - # TODO needs some testing (can there be more than one id, and do they follow pin order?) - # same in get_pinned_message - super().__init__(chat_peer=where, msg_id=pin_ids[0] if pin_ids else None) - - self.new_pin = pin_ids is not None - self._pin_ids = pin_ids - self._pinned_messages = None - - self.new_photo = new_photo is not None - self.photo = \ - new_photo if isinstance(new_photo, types.Photo) else None - - self._added_by = None - self._kicked_by = None - self.user_added = self.user_joined = self.user_left = \ - self.user_kicked = self.unpin = False - - if added_by is True: - self.user_joined = True - elif added_by: - self.user_added = True - self._added_by = added_by - - # If `from_id` was not present (it's `True`) or the affected - # user was "kicked by itself", then it left. Else it was kicked. - if kicked_by is True or (users is not None and kicked_by == users): - self.user_left = True - elif kicked_by: - self.user_kicked = True - self._kicked_by = kicked_by - - self.created = bool(created) - - if isinstance(users, list): - self._user_ids = [utils.get_peer_id(u) for u in users] - elif users: - self._user_ids = [utils.get_peer_id(users)] - else: - self._user_ids = [] - - self._users = None - self._input_users = None - self.new_title = new_title - self.new_score = new_score - self.unpin = not pin - - def _set_client(self, client): - super()._set_client(client) - if self.action_message: - self.action_message._finish_init(client, self._entities, None) - - async def respond(self, *args, **kwargs): - """ - Responds to the chat action message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def reply(self, *args, **kwargs): - """ - Replies to the chat action message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` with - both ``entity`` and ``reply_to`` already set. - - Has the same effect as `respond` if there is no message. - """ - if not self.action_message: - return await self.respond(*args, **kwargs) - - kwargs['reply_to'] = self.action_message.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def delete(self, *args, **kwargs): - """ - Deletes the chat action message. You're responsible for checking - whether you have the permission to do so, or to except the error - otherwise. Shorthand for - `telethon.client.messages.MessageMethods.delete_messages` with - ``entity`` and ``message_ids`` already set. - - Does nothing if no message action triggered this event. - """ - if not self.action_message: - return - - return await self._client.delete_messages( - await self.get_input_chat(), [self.action_message], - *args, **kwargs - ) - - async def get_pinned_message(self): - """ - If ``new_pin`` is `True`, this returns the `Message - ` object that was pinned. - """ - if self._pinned_messages is None: - await self.get_pinned_messages() - - if self._pinned_messages: - return self._pinned_messages[0] - - async def get_pinned_messages(self): - """ - If ``new_pin`` is `True`, this returns a `list` of `Message - ` objects that were pinned. - """ - if not self._pin_ids: - return self._pin_ids # either None or empty list - - chat = await self.get_input_chat() - if chat: - self._pinned_messages = await self._client.get_messages( - self._input_chat, ids=self._pin_ids) - - return self._pinned_messages - - @property - def added_by(self): - """ - The user who added ``users``, if applicable (`None` otherwise). - """ - if self._added_by and not isinstance(self._added_by, types.User): - aby = self._entities.get(utils.get_peer_id(self._added_by)) - if aby: - self._added_by = aby - - return self._added_by - - async def get_added_by(self): - """ - Returns `added_by` but will make an API call if necessary. - """ - if not self.added_by and self._added_by: - self._added_by = await self._client.get_entity(self._added_by) - - return self._added_by - - @property - def kicked_by(self): - """ - The user who kicked ``users``, if applicable (`None` otherwise). - """ - if self._kicked_by and not isinstance(self._kicked_by, types.User): - kby = self._entities.get(utils.get_peer_id(self._kicked_by)) - if kby: - self._kicked_by = kby - - return self._kicked_by - - async def get_kicked_by(self): - """ - Returns `kicked_by` but will make an API call if necessary. - """ - if not self.kicked_by and self._kicked_by: - self._kicked_by = await self._client.get_entity(self._kicked_by) - - return self._kicked_by - - @property - def user(self): - """ - The first user that takes part in this action. For example, who joined. - - Might be `None` if the information can't be retrieved or - there is no user taking part. - """ - if self.users: - return self._users[0] - - async def get_user(self): - """ - Returns `user` but will make an API call if necessary. - """ - if self.users or await self.get_users(): - return self._users[0] - - @property - def input_user(self): - """ - Input version of the ``self.user`` property. - """ - if self.input_users: - return self._input_users[0] - - async def get_input_user(self): - """ - Returns `input_user` but will make an API call if necessary. - """ - if self.input_users or await self.get_input_users(): - return self._input_users[0] - - @property - def user_id(self): - """ - Returns the marked signed ID of the first user, if any. - """ - if self._user_ids: - return self._user_ids[0] - - @property - def users(self): - """ - A list of users that take part in this action. For example, who joined. - - Might be empty if the information can't be retrieved or there - are no users taking part. - """ - if not self._user_ids: - return [] - - if self._users is None: - self._users = [ - self._entities[user_id] - for user_id in self._user_ids - if user_id in self._entities - ] - - return self._users - - async def get_users(self): - """ - Returns `users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.users is None or len(self._users) != len(self._user_ids)) and self.action_message: - await self.action_message._reload_message() - self._users = [ - u for u in self.action_message.action_entities - if isinstance(u, (types.User, types.UserEmpty))] - - return self._users - - @property - def input_users(self): - """ - Input version of the ``self.users`` property. - """ - if self._input_users is None and self._user_ids: - self._input_users = [] - for user_id in self._user_ids: - # First try to get it from our entities - try: - self._input_users.append(utils.get_input_peer(self._entities[user_id])) - continue - except (KeyError, TypeError): - pass - - # If missing, try from the entity cache - try: - self._input_users.append(self._client._mb_entity_cache.get( - utils.resolve_id(user_id)[0])._as_input_peer()) - continue - except AttributeError: - pass - - return self._input_users or [] - - async def get_input_users(self): - """ - Returns `input_users` but will make an API call if necessary. - """ - if not self._user_ids: - return [] - - # Note: we access the property first so that it fills if needed - if (self.input_users is None or len(self._input_users) != len(self._user_ids)) and self.action_message: - self._input_users = [ - utils.get_input_peer(u) - for u in self.action_message.action_entities - if isinstance(u, (types.User, types.UserEmpty))] - - return self._input_users or [] - - @property - def user_ids(self): - """ - Returns the marked signed ID of the users, if any. - """ - if self._user_ids: - return self._user_ids[:] diff --git a/telethon/events/common.py b/telethon/events/common.py deleted file mode 100644 index e295adb2..00000000 --- a/telethon/events/common.py +++ /dev/null @@ -1,186 +0,0 @@ -import abc -import asyncio -import warnings - -from .. import utils -from ..tl import TLObject, types -from ..tl.custom.chatgetter import ChatGetter - - -async def _into_id_set(client, chats): - """Helper util to turn the input chat or chats into a set of IDs.""" - if chats is None: - return None - - if not utils.is_list_like(chats): - chats = (chats,) - - result = set() - for chat in chats: - if isinstance(chat, int): - if chat < 0: - result.add(chat) # Explicitly marked IDs are negative - else: - result.update({ # Support all valid types of peers - utils.get_peer_id(types.PeerUser(chat)), - utils.get_peer_id(types.PeerChat(chat)), - utils.get_peer_id(types.PeerChannel(chat)), - }) - elif isinstance(chat, TLObject) and chat.SUBCLASS_OF_ID == 0x2d45687: - # 0x2d45687 == crc32(b'Peer') - result.add(utils.get_peer_id(chat)) - else: - chat = await client.get_input_entity(chat) - if isinstance(chat, types.InputPeerSelf): - chat = await client.get_me(input_peer=True) - result.add(utils.get_peer_id(chat)) - - return result - - -class EventBuilder(abc.ABC): - """ - The common event builder, with builtin support to filter per chat. - - Args: - chats (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only matching chats will be handled. - - blacklist_chats (`bool`, optional): - Whether to treat the chats as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``chats`` - which will be ignored if ``blacklist_chats=True``. - - func (`callable`, optional): - A callable (async or not) function that should accept the event as input - parameter, and return a value indicating whether the event - should be dispatched or not (any truthy value will do, it - does not need to be a `bool`). It works like a custom filter: - - .. code-block:: python - - @client.on(events.NewMessage(func=lambda e: e.is_private)) - async def handler(event): - pass # code here - """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None): - self.chats = chats - self.blacklist_chats = bool(blacklist_chats) - self.resolved = False - self.func = func - self._resolve_lock = None - - @classmethod - @abc.abstractmethod - def build(cls, update, others=None, self_id=None): - """ - Builds an event for the given update if possible, or returns None. - - `others` are the rest of updates that came in the same container - as the current `update`. - - `self_id` should be the current user's ID, since it is required - for some events which lack this information but still need it. - """ - # TODO So many parameters specific to only some update types seems dirty - - async def resolve(self, client): - """Helper method to allow event builders to be resolved before usage""" - if self.resolved: - return - - if not self._resolve_lock: - self._resolve_lock = asyncio.Lock() - - async with self._resolve_lock: - if not self.resolved: - await self._resolve(client) - self.resolved = True - - async def _resolve(self, client): - self.chats = await _into_id_set(client, self.chats) - - def filter(self, event): - """ - Returns a truthy value if the event passed the filter and should be - used, or falsy otherwise. The return value may need to be awaited. - - The events must have been resolved before this can be called. - """ - if not self.resolved: - return - - if self.chats is not None: - # Note: the `event.chat_id` property checks if it's `None` for us - inside = event.chat_id in self.chats - if inside == self.blacklist_chats: - # If this chat matches but it's a blacklist ignore. - # If it doesn't match but it's a whitelist ignore. - return - - if not self.func: - return True - - # Return the result of func directly as it may need to be awaited - return self.func(event) - - -class EventCommon(ChatGetter, abc.ABC): - """ - Intermediate class with common things to all events. - - Remember that this class implements `ChatGetter - ` which - means you have access to all chat properties and methods. - - In addition, you can access the `original_update` - field which contains the original :tl:`Update`. - """ - _event_name = 'Event' - - def __init__(self, chat_peer=None, msg_id=None, broadcast=None): - super().__init__(chat_peer, broadcast=broadcast) - self._entities = {} - self._client = None - self._message_id = msg_id - self.original_update = None - - def _set_client(self, client): - """ - Setter so subclasses can act accordingly when the client is set. - """ - self._client = client - if self._chat_peer: - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, self._entities, client._mb_entity_cache) - else: - self._chat = self._input_chat = None - - @property - def client(self): - """ - The `telethon.TelegramClient` that created this event. - """ - return self._client - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) - - def to_dict(self): - d = {k: v for k, v in self.__dict__.items() if k[0] != '_'} - d['_'] = self._event_name - return d - - -def name_inner_event(cls): - """Decorator to rename cls.Event 'Event' as 'cls.Event'""" - if hasattr(cls, 'Event'): - cls.Event._event_name = '{}.Event'.format(cls.__name__) - else: - warnings.warn('Class {} does not have a inner Event'.format(cls)) - return cls diff --git a/telethon/events/inlinequery.py b/telethon/events/inlinequery.py deleted file mode 100644 index 7a065e55..00000000 --- a/telethon/events/inlinequery.py +++ /dev/null @@ -1,247 +0,0 @@ -import inspect -import re - -import asyncio - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils, helpers -from ..tl import types, functions, custom -from ..tl.custom.sendergetter import SenderGetter - - -@name_inner_event -class InlineQuery(EventBuilder): - """ - Occurs whenever you sign in as a bot and a user - sends an inline query such as ``@bot query``. - - Args: - users (`entity`, optional): - May be one or more entities (username/peer/etc.), preferably IDs. - By default, only inline queries from these users will be handled. - - blacklist_users (`bool`, optional): - Whether to treat the users as a blacklist instead of - as a whitelist (default). This means that every chat - will be handled *except* those specified in ``users`` - which will be ignored if ``blacklist_users=True``. - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only queries matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns `True` - if a message is acceptable, or a compiled regex pattern. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.InlineQuery) - async def handler(event): - builder = event.builder - - # Two options (convert user text to UPPERCASE or lowercase) - await event.answer([ - builder.article('UPPERCASE', text=event.text.upper()), - builder.article('lowercase', text=event.text.lower()), - ]) - """ - def __init__( - self, users=None, *, blacklist_users=False, func=None, pattern=None): - super().__init__(users, blacklist_chats=blacklist_users, func=func) - - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateBotInlineQuery): - return cls.Event(update) - - def filter(self, event): - if self.pattern: - match = self.pattern(event.text) - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon, SenderGetter): - """ - Represents the event of a new callback query. - - Members: - query (:tl:`UpdateBotInlineQuery`): - The original :tl:`UpdateBotInlineQuery`. - - Make sure to access the `text` property of the query if - you want the text rather than the actual query object. - - pattern_match (`obj`, optional): - The resulting object from calling the passed ``pattern`` - function, which is ``re.compile(...).match`` by default. - """ - def __init__(self, query): - super().__init__(chat_peer=types.PeerUser(query.user_id)) - SenderGetter.__init__(self, query.user_id) - self.query = query - self.pattern_match = None - self._answered = False - - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._mb_entity_cache) - - @property - def id(self): - """ - Returns the unique identifier for the query ID. - """ - return self.query.query_id - - @property - def text(self): - """ - Returns the text the user used to make the inline query. - """ - return self.query.query - - @property - def offset(self): - """ - The string the user's client used as an offset for the query. - This will either be empty or equal to offsets passed to `answer`. - """ - return self.query.offset - - @property - def geo(self): - """ - If the user location is requested when using inline mode - and the user's device is able to send it, this will return - the :tl:`GeoPoint` with the position of the user. - """ - return self.query.geo - - @property - def builder(self): - """ - Returns a new `InlineBuilder - ` instance. - """ - return custom.InlineBuilder(self._client) - - async def answer( - self, results=None, cache_time=0, *, - gallery=False, next_offset=None, private=False, - switch_pm=None, switch_pm_param=''): - """ - Answers the inline query with the given results. - - See the documentation for `builder` to know what kind of answers - can be given. - - Args: - results (`list`, optional): - A list of :tl:`InputBotInlineResult` to use. - You should use `builder` to create these: - - .. code-block:: python - - builder = inline.builder - r1 = builder.article('Be nice', text='Have a nice day') - r2 = builder.article('Be bad', text="I don't like you") - await inline.answer([r1, r2]) - - You can send up to 50 results as documented in - https://core.telegram.org/bots/api#answerinlinequery. - Sending more will raise ``ResultsTooMuchError``, - and you should consider using `next_offset` to - paginate them. - - cache_time (`int`, optional): - For how long this result should be cached on - the user's client. Defaults to 0 for no cache. - - gallery (`bool`, optional): - Whether the results should show as a gallery (grid) or not. - - next_offset (`str`, optional): - The offset the client will send when the user scrolls the - results and it repeats the request. - - private (`bool`, optional): - Whether the results should be cached by Telegram - (not private) or by the user's client (private). - - switch_pm (`str`, optional): - If set, this text will be shown in the results - to allow the user to switch to private messages. - - switch_pm_param (`str`, optional): - Optional parameter to start the bot with if - `switch_pm` was used. - - Example: - - .. code-block:: python - - @bot.on(events.InlineQuery) - async def handler(event): - builder = event.builder - - rev_text = event.text[::-1] - await event.answer([ - builder.article('Reverse text', text=rev_text), - builder.photo('/path/to/photo.jpg') - ]) - """ - if self._answered: - return - - if results: - futures = [self._as_future(x) for x in results] - - await asyncio.wait(futures) - - # All futures will be in the `done` *set* that `wait` returns. - # - # Precisely because it's a `set` and not a `list`, it - # will not preserve the order, but since all futures - # completed we can use our original, ordered `list`. - results = [x.result() for x in futures] - else: - results = [] - - if switch_pm: - switch_pm = types.InlineBotSwitchPM(switch_pm, switch_pm_param) - - return await self._client( - functions.messages.SetInlineBotResultsRequest( - query_id=self.query.query_id, - results=results, - cache_time=cache_time, - gallery=gallery, - next_offset=next_offset, - private=private, - switch_pm=switch_pm - ) - ) - - @staticmethod - def _as_future(obj): - if inspect.isawaitable(obj): - return asyncio.ensure_future(obj) - - f = helpers.get_running_loop().create_future() - f.set_result(obj) - return f diff --git a/telethon/events/messagedeleted.py b/telethon/events/messagedeleted.py deleted file mode 100644 index f631fd4f..00000000 --- a/telethon/events/messagedeleted.py +++ /dev/null @@ -1,57 +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. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.MessageDeleted) - async def handler(event): - # Log all deleted message IDs - for msg_id in event.deleted_ids: - print('Message', msg_id, 'was deleted in', event.chat_id) - """ - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateDeleteMessages): - return cls.Event( - deleted_ids=update.messages, - peer=None - ) - elif isinstance(update, types.UpdateDeleteChannelMessages): - return cls.Event( - deleted_ids=update.messages, - peer=types.PeerChannel(update.channel_id) - ) - - class Event(EventCommon): - def __init__(self, deleted_ids, peer): - super().__init__( - chat_peer=peer, msg_id=(deleted_ids or [0])[0] - ) - self.deleted_id = None if not deleted_ids else deleted_ids[0] - self.deleted_ids = deleted_ids diff --git a/telethon/events/messageedited.py b/telethon/events/messageedited.py deleted file mode 100644 index c4a2b4a7..00000000 --- a/telethon/events/messageedited.py +++ /dev/null @@ -1,52 +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). - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.MessageEdited) - async def handler(event): - # Log the date of new edits - print('Message', event.id, 'changed at', event.date) - """ - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, (types.UpdateEditMessage, - types.UpdateEditChannelMessage)): - return cls.Event(update.message) - - class Event(NewMessage.Event): - pass # Required if we want a different name for it diff --git a/telethon/events/messageread.py b/telethon/events/messageread.py deleted file mode 100644 index 29f17ab8..00000000 --- a/telethon/events/messageread.py +++ /dev/null @@ -1,143 +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. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.MessageRead) - async def handler(event): - # Log when someone reads your messages - print('Someone has read all your messages until', event.max_id) - - @client.on(events.MessageRead(inbox=True)) - async def handler(event): - # Log when you read message in a chat (from your "inbox") - print('You have read messages until', event.max_id) - """ - def __init__( - self, chats=None, *, blacklist_chats=False, func=None, inbox=False): - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.inbox = inbox - - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateReadHistoryInbox): - return cls.Event(update.peer, update.max_id, False) - elif isinstance(update, types.UpdateReadHistoryOutbox): - return cls.Event(update.peer, update.max_id, True) - elif isinstance(update, types.UpdateReadChannelInbox): - return cls.Event(types.PeerChannel(update.channel_id), - update.max_id, False) - elif isinstance(update, types.UpdateReadChannelOutbox): - return cls.Event(types.PeerChannel(update.channel_id), - update.max_id, True) - elif isinstance(update, types.UpdateReadMessagesContents): - return cls.Event(message_ids=update.messages, - contents=True) - elif isinstance(update, types.UpdateChannelReadMessagesContents): - return cls.Event(types.PeerChannel(update.channel_id), - message_ids=update.messages, - contents=True) - - def filter(self, event): - if self.inbox == event.outbox: - return - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of one or more messages being read. - - Members: - max_id (`int`): - Up to which message ID has been read. Every message - with an ID equal or lower to it have been read. - - outbox (`bool`): - `True` if someone else has read your messages. - - contents (`bool`): - `True` if what was read were the contents of a message. - This will be the case when e.g. you play a voice note. - It may only be set on ``inbox`` events. - """ - def __init__(self, peer=None, max_id=None, out=False, contents=False, - message_ids=None): - self.outbox = out - self.contents = contents - self._message_ids = message_ids or [] - self._messages = None - self.max_id = max_id or max(message_ids or [], default=None) - super().__init__(peer, self.max_id) - - @property - def inbox(self): - """ - `True` if you have read someone else's messages. - """ - return not self.outbox - - @property - def message_ids(self): - """ - The IDs of the messages **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - return self._message_ids - - async def get_messages(self): - """ - Returns the list of `Message ` - **which contents'** were read. - - Use :meth:`is_read` if you need to check whether a message - was read instead checking if it's in here. - """ - if self._messages is None: - chat = await self.get_input_chat() - if not chat: - self._messages = [] - else: - self._messages = await self._client.get_messages( - chat, ids=self._message_ids) - - return self._messages - - def is_read(self, message): - """ - Returns `True` if the given message (or its ID) has been read. - - If a list-like argument is provided, this method will return a - list of booleans indicating which messages have been read. - """ - if utils.is_list_like(message): - return [(m if isinstance(m, int) else m.id) <= self.max_id - for m in message] - else: - return (message if isinstance(message, int) - else message.id) <= self.max_id - - def __contains__(self, message): - """`True` if the message(s) are read message.""" - if utils.is_list_like(message): - return all(self.is_read(message)) - else: - return self.is_read(message) diff --git a/telethon/events/newmessage.py b/telethon/events/newmessage.py deleted file mode 100644 index d2077a71..00000000 --- a/telethon/events/newmessage.py +++ /dev/null @@ -1,223 +0,0 @@ -import re - -from .common import EventBuilder, EventCommon, name_inner_event, _into_id_set -from .. import utils -from ..tl import types - - -@name_inner_event -class NewMessage(EventBuilder): - """ - Occurs whenever a new text message or a message with media arrives. - - Args: - incoming (`bool`, optional): - If set to `True`, only **incoming** messages will be handled. - Mutually exclusive with ``outgoing`` (can only set one of either). - - outgoing (`bool`, optional): - If set to `True`, only **outgoing** messages will be handled. - Mutually exclusive with ``incoming`` (can only set one of either). - - from_users (`entity`, optional): - Unlike `chats`, this parameter filters the *senders* of the - message. That is, only messages *sent by these users* will be - handled. Use `chats` if you want private messages with this/these - users. `from_users` lets you filter by messages sent by *one or - more* users across the desired chats (doesn't need a list). - - forwards (`bool`, optional): - Whether forwarded messages should be handled or not. By default, - both forwarded and normal messages are included. If it's `True` - *only* forwards will be handled. If it's `False` only messages - that are *not* forwards will be handled. - - pattern (`str`, `callable`, `Pattern`, optional): - If set, only messages matching this pattern will be handled. - You can specify a regex-like string which will be matched - against the message, a callable function that returns `True` - if a message is acceptable, or a compiled regex pattern. - - Example - .. code-block:: python - - import asyncio - from telethon import events - - @client.on(events.NewMessage(pattern='(?i)hello.+')) - async def handler(event): - # Respond whenever someone says "Hello" and something else - await event.reply('Hey!') - - @client.on(events.NewMessage(outgoing=True, pattern='!ping')) - async def handler(event): - # Say "!pong" whenever you send "!ping", then delete both messages - m = await event.respond('!pong') - await asyncio.sleep(5) - await client.delete_messages(event.chat_id, [event.id, m.id]) - """ - def __init__(self, chats=None, *, blacklist_chats=False, func=None, - incoming=None, outgoing=None, - from_users=None, forwards=None, pattern=None): - if incoming and outgoing: - incoming = outgoing = None # Same as no filter - elif incoming is not None and outgoing is None: - outgoing = not incoming - elif outgoing is not None and incoming is None: - incoming = not outgoing - elif all(x is not None and not x for x in (incoming, outgoing)): - raise ValueError("Don't create an event handler if you " - "don't want neither incoming nor outgoing!") - - super().__init__(chats, blacklist_chats=blacklist_chats, func=func) - self.incoming = incoming - self.outgoing = outgoing - self.from_users = from_users - self.forwards = forwards - if isinstance(pattern, str): - self.pattern = re.compile(pattern).match - elif not pattern or callable(pattern): - self.pattern = pattern - elif hasattr(pattern, 'match') and callable(pattern.match): - self.pattern = pattern.match - else: - raise TypeError('Invalid pattern type given') - - # Should we short-circuit? E.g. perform no check at all - self._no_check = all(x is None for x in ( - self.chats, self.incoming, self.outgoing, self.pattern, - self.from_users, self.forwards, self.from_users, self.func - )) - - async def _resolve(self, client): - await super()._resolve(client) - self.from_users = await _into_id_set(client, self.from_users) - - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, - (types.UpdateNewMessage, types.UpdateNewChannelMessage)): - if not isinstance(update.message, types.Message): - return # We don't care about MessageService's here - event = cls.Event(update.message) - elif isinstance(update, types.UpdateShortMessage): - event = cls.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - peer_id=types.PeerUser(update.user_id), - from_id=types.PeerUser(self_id if update.out else update.user_id), - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to=update.reply_to, - entities=update.entities, - ttl_period=update.ttl_period - )) - elif isinstance(update, types.UpdateShortChatMessage): - event = cls.Event(types.Message( - out=update.out, - mentioned=update.mentioned, - media_unread=update.media_unread, - silent=update.silent, - id=update.id, - from_id=types.PeerUser(self_id if update.out else update.from_id), - peer_id=types.PeerChat(update.chat_id), - message=update.message, - date=update.date, - fwd_from=update.fwd_from, - via_bot_id=update.via_bot_id, - reply_to=update.reply_to, - entities=update.entities, - ttl_period=update.ttl_period - )) - else: - return - - return event - - def filter(self, event): - if self._no_check: - return event - - if self.incoming and event.message.out: - return - if self.outgoing and not event.message.out: - return - if self.forwards is not None: - if bool(self.forwards) != bool(event.message.fwd_from): - return - - if self.from_users is not None: - if event.message.sender_id not in self.from_users: - return - - if self.pattern: - match = self.pattern(event.message.message or '') - if not match: - return - event.pattern_match = match - - return super().filter(event) - - class Event(EventCommon): - """ - Represents the event of a new message. This event can be treated - to all effects as a `Message `, - so please **refer to its documentation** to know what you can do - with this event. - - Members: - message (`Message `): - This is the only difference with the received - `Message `, and will - return the `telethon.tl.custom.message.Message` itself, - not the text. - - See `Message ` for - the rest of available members and methods. - - pattern_match (`obj`): - The resulting object from calling the passed ``pattern`` function. - Here's an example using a string (defaults to regex match): - - >>> from telethon import TelegramClient, events - >>> client = TelegramClient(...) - >>> - >>> @client.on(events.NewMessage(pattern=r'hi (\\w+)!')) - ... async def handler(event): - ... # In this case, the result is a ``Match`` object - ... # since the `str` pattern was converted into - ... # the ``re.compile(pattern).match`` function. - ... print('Welcomed', event.pattern_match.group(1)) - ... - >>> - """ - def __init__(self, message): - self.__dict__['_init'] = False - super().__init__(chat_peer=message.peer_id, - msg_id=message.id, broadcast=bool(message.post)) - - self.pattern_match = None - self.message = message - - def _set_client(self, client): - super()._set_client(client) - m = self.message - m._finish_init(client, self._entities, None) - self.__dict__['_init'] = True # No new attributes can be set - - def __getattr__(self, item): - if item in self.__dict__: - return self.__dict__[item] - else: - return getattr(self.message, item) - - def __setattr__(self, name, value): - if not self.__dict__['_init'] or name in self.__dict__: - self.__dict__[name] = value - else: - setattr(self.message, name, value) diff --git a/telethon/events/raw.py b/telethon/events/raw.py deleted file mode 100644 index 84910778..00000000 --- a/telethon/events/raw.py +++ /dev/null @@ -1,53 +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``. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.Raw) - async def handler(update): - # Print all incoming updates - print(update.stringify()) - """ - def __init__(self, types=None, *, func=None): - super().__init__(func=func) - if not types: - self.types = None - elif not utils.is_list_like(types): - if not isinstance(types, type): - raise TypeError('Invalid input type given: {}'.format(types)) - - self.types = types - else: - if not all(isinstance(x, type) for x in types): - raise TypeError('Invalid input types given: {}'.format(types)) - - self.types = tuple(types) - - async def resolve(self, client): - self.resolved = True - - @classmethod - def build(cls, update, others=None, self_id=None): - return update - - def filter(self, event): - if not self.types or isinstance(event, self.types): - if self.func: - # Return the result of func directly as it may need to be awaited - return self.func(event) - return event diff --git a/telethon/events/userupdate.py b/telethon/events/userupdate.py deleted file mode 100644 index 450172e5..00000000 --- a/telethon/events/userupdate.py +++ /dev/null @@ -1,310 +0,0 @@ -import datetime -import functools - -from .common import EventBuilder, EventCommon, name_inner_event -from .. import utils -from ..tl import types -from ..tl.custom.sendergetter import SenderGetter - - -# TODO Either the properties are poorly named or they should be -# different events, but that would be a breaking change. -# -# TODO There are more "user updates", but bundling them all up -# in a single place will make it annoying to use (since -# the user needs to check for the existence of `None`). -# -# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser - -def _requires_action(function): - @functools.wraps(function) - def wrapped(self): - return None if self.action is None else function(self) - - return wrapped - - -def _requires_status(function): - @functools.wraps(function) - def wrapped(self): - return None if self.status is None else function(self) - - return wrapped - - -@name_inner_event -class UserUpdate(EventBuilder): - """ - Occurs whenever a user goes online, starts typing, etc. - - Example - .. code-block:: python - - from telethon import events - - @client.on(events.UserUpdate) - async def handler(event): - # If someone is uploading, say something - if event.uploading: - await client.send_message(event.user_id, 'What are you sending?') - """ - @classmethod - def build(cls, update, others=None, self_id=None): - if isinstance(update, types.UpdateUserStatus): - return cls.Event(types.PeerUser(update.user_id), - status=update.status) - elif isinstance(update, types.UpdateChannelUserTyping): - return cls.Event(update.from_id, - chat_peer=types.PeerChannel(update.channel_id), - typing=update.action) - elif isinstance(update, types.UpdateChatUserTyping): - return cls.Event(update.from_id, - chat_peer=types.PeerChat(update.chat_id), - typing=update.action) - elif isinstance(update, types.UpdateUserTyping): - return cls.Event(update.user_id, - typing=update.action) - - class Event(EventCommon, SenderGetter): - """ - Represents the event of a user update - such as gone online, started typing, etc. - - Members: - status (:tl:`UserStatus`, optional): - The user status if the update is about going online or offline. - - You should check this attribute first before checking any - of the seen within properties, since they will all be `None` - if the status is not set. - - action (:tl:`SendMessageAction`, optional): - The "typing" action if any the user is performing if any. - - You should check this attribute first before checking any - of the typing properties, since they will all be `None` - if the action is not set. - """ - def __init__(self, peer, *, status=None, chat_peer=None, typing=None): - super().__init__(chat_peer or peer) - SenderGetter.__init__(self, utils.get_peer_id(peer)) - - self.status = status - self.action = typing - - def _set_client(self, client): - super()._set_client(client) - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, self._entities, client._mb_entity_cache) - - @property - def user(self): - """Alias for `sender `.""" - return self.sender - - async def get_user(self): - """Alias for `get_sender `.""" - return await self.get_sender() - - @property - def input_user(self): - """Alias for `input_sender `.""" - return self.input_sender - - async def get_input_user(self): - """Alias for `get_input_sender `.""" - return await self.get_input_sender() - - @property - def user_id(self): - """Alias for `sender_id `.""" - return self.sender_id - - @property - @_requires_action - def typing(self): - """ - `True` if the action is typing a message. - """ - return isinstance(self.action, types.SendMessageTypingAction) - - @property - @_requires_action - def uploading(self): - """ - `True` if the action is uploading something. - """ - return isinstance(self.action, ( - types.SendMessageChooseContactAction, - types.SendMessageChooseStickerAction, - types.SendMessageUploadAudioAction, - types.SendMessageUploadDocumentAction, - types.SendMessageUploadPhotoAction, - types.SendMessageUploadRoundAction, - types.SendMessageUploadVideoAction - )) - - @property - @_requires_action - def recording(self): - """ - `True` if the action is recording something. - """ - return isinstance(self.action, ( - types.SendMessageRecordAudioAction, - types.SendMessageRecordRoundAction, - types.SendMessageRecordVideoAction - )) - - @property - @_requires_action - def playing(self): - """ - `True` if the action is playing a game. - """ - return isinstance(self.action, types.SendMessageGamePlayAction) - - @property - @_requires_action - def cancel(self): - """ - `True` if the action was cancelling other actions. - """ - return isinstance(self.action, types.SendMessageCancelAction) - - @property - @_requires_action - def geo(self): - """ - `True` if what's being uploaded is a geo. - """ - return isinstance(self.action, types.SendMessageGeoLocationAction) - - @property - @_requires_action - def audio(self): - """ - `True` if what's being recorded/uploaded is an audio. - """ - return isinstance(self.action, ( - types.SendMessageRecordAudioAction, - types.SendMessageUploadAudioAction - )) - - @property - @_requires_action - def round(self): - """ - `True` if what's being recorded/uploaded is a round video. - """ - return isinstance(self.action, ( - types.SendMessageRecordRoundAction, - types.SendMessageUploadRoundAction - )) - - @property - @_requires_action - def video(self): - """ - `True` if what's being recorded/uploaded is an video. - """ - return isinstance(self.action, ( - types.SendMessageRecordVideoAction, - types.SendMessageUploadVideoAction - )) - - @property - @_requires_action - def contact(self): - """ - `True` if what's being uploaded (selected) is a contact. - """ - return isinstance(self.action, types.SendMessageChooseContactAction) - - @property - @_requires_action - def document(self): - """ - `True` if what's being uploaded is document. - """ - return isinstance(self.action, types.SendMessageUploadDocumentAction) - - @property - @_requires_action - def sticker(self): - """ - `True` if what's being uploaded is a sticker. - """ - return isinstance(self.action, types.SendMessageChooseStickerAction) - - @property - @_requires_action - def photo(self): - """ - `True` if what's being uploaded is a photo. - """ - return isinstance(self.action, types.SendMessageUploadPhotoAction) - - @property - @_requires_status - def last_seen(self): - """ - Exact `datetime.datetime` when the user was last seen if known. - """ - if isinstance(self.status, types.UserStatusOffline): - return self.status.was_online - - @property - @_requires_status - def until(self): - """ - The `datetime.datetime` until when the user should appear online. - """ - if isinstance(self.status, types.UserStatusOnline): - return self.status.expires - - def _last_seen_delta(self): - if isinstance(self.status, types.UserStatusOffline): - return datetime.datetime.now(tz=datetime.timezone.utc) - self.status.was_online - elif isinstance(self.status, types.UserStatusOnline): - return datetime.timedelta(days=0) - elif isinstance(self.status, types.UserStatusRecently): - return datetime.timedelta(days=1) - elif isinstance(self.status, types.UserStatusLastWeek): - return datetime.timedelta(days=7) - elif isinstance(self.status, types.UserStatusLastMonth): - return datetime.timedelta(days=30) - else: - return datetime.timedelta(days=365) - - @property - @_requires_status - def online(self): - """ - `True` if the user is currently online, - """ - return self._last_seen_delta() <= datetime.timedelta(days=0) - - @property - @_requires_status - def recently(self): - """ - `True` if the user was seen within a day. - """ - return self._last_seen_delta() <= datetime.timedelta(days=1) - - @property - @_requires_status - def within_weeks(self): - """ - `True` if the user was seen within 7 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=7) - - @property - @_requires_status - def within_months(self): - """ - `True` if the user was seen within 30 days. - """ - return self._last_seen_delta() <= datetime.timedelta(days=30) diff --git a/telethon/extensions/__init__.py b/telethon/extensions/__init__.py deleted file mode 100644 index a3c77295..00000000 --- a/telethon/extensions/__init__.py +++ /dev/null @@ -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 a 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 deleted file mode 100644 index 996f362e..00000000 --- a/telethon/extensions/binaryreader.py +++ /dev/null @@ -1,185 +0,0 @@ -""" -This module contains the BinaryReader utility class. -""" -import os -import time -from datetime import datetime, timezone, timedelta -from io import BytesIO -from struct import unpack - -from ..errors import TypeNotFoundError -from ..tl.alltlobjects import tlobjects -from ..tl.core import core_objects - -_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) -_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) - - -class BinaryReader: - """ - Small utility class to read binary data. - """ - - def __init__(self, data): - self.stream = BytesIO(data) - self._last = None # Should come in handy to spot -404 errors - - # region Reading - - # "All numbers are written as little endian." - # https://core.telegram.org/mtproto - def read_byte(self): - """Reads a single byte value.""" - return self.read(1)[0] - - def read_int(self, signed=True): - """Reads an integer (4 bytes) value.""" - return int.from_bytes(self.read(4), byteorder='little', signed=signed) - - def read_long(self, signed=True): - """Reads a long integer (8 bytes) value.""" - return int.from_bytes(self.read(8), byteorder='little', signed=signed) - - def read_float(self): - """Reads a real floating point (4 bytes) value.""" - return unpack('= 0) and (len(result) != length): - raise BufferError( - 'No more data left to read (need {}, got {}: {}); last read {}' - .format(length, len(result), repr(result), repr(self._last)) - ) - - self._last = result - return result - - def get_bytes(self): - """Gets the byte array representing the current buffer as a whole.""" - return self.stream.getvalue() - - # endregion - - # region Telegram custom reading - - def tgread_bytes(self): - """ - Reads a Telegram-encoded byte array, without the need of - specifying its length. - """ - first_byte = self.read_byte() - if first_byte == 254: - length = self.read_byte() | (self.read_byte() << 8) | ( - self.read_byte() << 16) - padding = length % 4 - else: - length = first_byte - padding = (length + 1) % 4 - - data = self.read(length) - if padding > 0: - padding = 4 - padding - self.read(padding) - - return data - - def tgread_string(self): - """Reads a Telegram-encoded string.""" - return str(self.tgread_bytes(), encoding='utf-8', errors='replace') - - def tgread_bool(self): - """Reads a Telegram boolean value.""" - value = self.read_int(signed=False) - if value == 0x997275b5: # boolTrue - return True - elif value == 0xbc799737: # boolFalse - return False - else: - raise RuntimeError('Invalid boolean code {}'.format(hex(value))) - - def tgread_date(self): - """Reads and converts Unix time (used by Telegram) - into a Python datetime object. - """ - value = self.read_int() - return _EPOCH + timedelta(seconds=value) - - def tgread_object(self): - """Reads a Telegram object.""" - constructor_id = self.read_int(signed=False) - clazz = tlobjects.get(constructor_id, None) - if clazz is None: - # The class was None, but there's still a - # chance of it being a manually parsed value like bool! - value = constructor_id - if value == 0x997275b5: # boolTrue - return True - elif value == 0xbc799737: # boolFalse - return False - elif value == 0x1cb5c415: # Vector - return [self.tgread_object() for _ in range(self.read_int())] - - clazz = core_objects.get(constructor_id, None) - if clazz is None: - # If there was still no luck, give up - self.seek(-4) # Go back - pos = self.tell_position() - error = TypeNotFoundError(constructor_id, self.read()) - self.set_position(pos) - raise error - - return clazz.from_reader(self) - - def tgread_vector(self): - """Reads a vector (a list) of Telegram objects.""" - if 0x1cb5c415 != self.read_int(signed=False): - raise RuntimeError('Invalid constructor code, vector was expected') - - count = self.read_int() - return [self.tgread_object() for _ in range(count)] - - # endregion - - def close(self): - """Closes the reader, freeing the BytesIO stream.""" - self.stream.close() - - # region Position related - - def tell_position(self): - """Tells the current position on the stream.""" - return self.stream.tell() - - def set_position(self, position): - """Sets the current position on the stream.""" - self.stream.seek(position) - - def seek(self, offset): - """ - Seeks the stream position given an offset from the current position. - The offset may be negative. - """ - self.stream.seek(offset, os.SEEK_CUR) - - # endregion - - # region with block - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.close() - - # endregion diff --git a/telethon/extensions/html.py b/telethon/extensions/html.py deleted file mode 100644 index a25ed58b..00000000 --- a/telethon/extensions/html.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -Simple HTML -> Telegram entity parser. -""" -import struct -from collections import deque -from html import escape -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'] = _del_surrogate(url)
-                    url = None
-            self._open_tags_meta.popleft()
-            self._open_tags_meta.appendleft(url)
-
-        if EntityType and tag not in self._building_entities:
-            self._building_entities[tag] = EntityType(
-                offset=len(self.text),
-                # The length will be determined when closing the tag.
-                length=0,
-                **args)
-
-    def handle_data(self, text):
-        previous_tag = self._open_tags[0] if len(self._open_tags) > 0 else ''
-        if previous_tag == 'a':
-            url = self._open_tags_meta[0]
-            if url:
-                text = url
-
-        for tag, entity in self._building_entities.items():
-            entity.length += len(text)
-
-        self.text += text
-
-    def handle_endtag(self, tag):
-        try:
-            self._open_tags.popleft()
-            self._open_tags_meta.popleft()
-        except IndexError:
-            pass
-        entity = self._building_entities.pop(tag, None)
-        if entity:
-            self.entities.append(entity)
-
-
-def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
-    """
-    Parses the given HTML message and returns its stripped representation
-    plus a list of the MessageEntity's that were found.
-
-    :param html: the message with HTML to be parsed.
-    :return: a tuple consisting of (clean message, [message entities]).
-    """
-    if not html:
-        return html, []
-
-    parser = HTMLToTelegramParser()
-    parser.feed(_add_surrogate(html))
-    text = 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
-        length = entity.length
-
-        # If we are in the middle of a surrogate nudge the position by +1.
-        # Otherwise we would end up with malformed text and fail to encode.
-        # For example of bad input: "Hi \ud83d\ude1c"
-        # https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF
-        while helpers.within_surrogate(text, relative_offset, length=_length):
-            relative_offset += 1
-
-        while helpers.within_surrogate(text, relative_offset + length, length=_length):
-            length += 1
-
-        entity_text = unparse(text=text[relative_offset:relative_offset + length],
-                              entities=entities[i + 1:],
-                              _offset=entity.offset, _length=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 length) - - while helpers.within_surrogate(text, last_offset, length=_length): - last_offset += 1 - - html.append(escape(text[last_offset:])) - return _del_surrogate(''.join(html)) diff --git a/telethon/extensions/markdown.py b/telethon/extensions/markdown.py deleted file mode 100644 index d52fc347..00000000 --- a/telethon/extensions/markdown.py +++ /dev/null @@ -1,197 +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, within_surrogate, strip_text -from ..tl import TLObject -from ..tl.types import ( - MessageEntityBold, MessageEntityItalic, MessageEntityCode, - MessageEntityPre, MessageEntityTextUrl, MessageEntityMentionName, - MessageEntityStrike -) - -DEFAULT_DELIMITERS = { - '**': MessageEntityBold, - '__': MessageEntityItalic, - '~~': MessageEntityStrike, - '`': MessageEntityCode, - '```': MessageEntityPre -} - -DEFAULT_URL_RE = re.compile(r'\[([^\]]+)\]\(([^)]+)\)') -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. - # Note that the largest delimiter should go first, we don't - # want ``` to be interpreted as a single back-tick in a code block. - delim_re = re.compile('|'.join('({})'.format(re.escape(k)) - for k in sorted(delimiters, key=len, reverse=True))) - - # Cannot use a for loop because we need to skip some indices - i = 0 - result = [] - - # Work on byte level with the utf-16le encoding to get the offsets right. - # The offset will just be half the index we're at. - message = add_surrogate(message) - while i < len(message): - m = delim_re.match(message, pos=i) - - # Did we find some delimiter here at `i`? - if m: - delim = next(filter(None, m.groups())) - - # +1 to avoid matching right after (e.g. "****") - end = message.find(delim, i + len(delim) + 1) - - # Did we find the earliest closing tag? - if end != -1: - - # Remove the delimiter from the string - message = ''.join(( - message[:i], - message[i + len(delim):end], - message[end + len(delim):] - )) - - # Check other affected entities - for ent in result: - # If the end is after our start, it is affected - if ent.offset + ent.length > i: - # If the old start is also before ours, it is fully enclosed - if ent.offset <= i: - ent.length -= len(delim) * 2 - else: - ent.length -= len(delim) - - # Append the found entity - ent = delimiters[delim] - if ent == MessageEntityPre: - result.append(ent(i, end - i - len(delim), '')) # has 'lang' - else: - result.append(ent(i, end - i - len(delim))) - - # No nested entities inside code blocks - if ent in (MessageEntityCode, MessageEntityPre): - i = end - len(delim) - - continue - - elif url_re: - m = url_re.match(message, pos=i) - if m: - # Replace the whole match with only the inline URL text. - message = ''.join(( - message[:m.start()], - m.group(1), - message[m.end():] - )) - - delim_size = m.end() - m.start() - len(m.group()) - 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() - - # If we are in the middle of a surrogate nudge the position by -1. - # Otherwise we would end up with malformed text and fail to encode. - # For example of bad input: "Hi \ud83d\ude1c" - # https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF - while within_surrogate(text, at): - at += 1 - - text = text[:at] + what + text[at:] - - return del_surrogate(text) diff --git a/telethon/extensions/messagepacker.py b/telethon/extensions/messagepacker.py deleted file mode 100644 index c0f46f48..00000000 --- a/telethon/extensions/messagepacker.py +++ /dev/null @@ -1,111 +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, loggers): - self._state = state - self._deque = collections.deque() - self._ready = asyncio.Event() - self._log = loggers[__name__] - - def append(self, state): - self._deque.append(state) - self._ready.set() - - def extend(self, states): - self._deque.extend(states) - self._ready.set() - - async def get(self): - """ - Returns (batch, data) if one or more items could be retrieved. - - If the cancellation occurs or only invalid items were in the - queue, (None, None) will be returned instead. - """ - if not self._deque: - self._ready.clear() - await self._ready.wait() - - buffer = io.BytesIO() - batch = [] - size = 0 - - # Fill a new batch to return while the size is small enough, - # as long as we don't exceed the maximum length of messages. - while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH: - state = self._deque.popleft() - size += len(state.data) + TLMessage.SIZE_OVERHEAD - - if size <= MessageContainer.MAXIMUM_SIZE: - state.msg_id = self._state.write_data_as_message( - buffer, state.data, isinstance(state.request, TLRequest), - after_id=state.after.msg_id if state.after else None - ) - batch.append(state) - self._log.debug('Assigned msg_id = %d to %s (%x)', - state.msg_id, state.request.__class__.__name__, - id(state.request)) - continue - - if batch: - # Put the item back since it can't be sent in this batch - self._deque.appendleft(state) - break - - # If a single message exceeds the maximum size, then the - # message payload cannot be sent. Telegram would forcibly - # close the connection; message would never be confirmed. - # - # We don't put the item back because it can never be sent. - # If we did, we would loop again and reach this same path. - # Setting the exception twice results in `InvalidStateError` - # and this method should never return with error, which we - # really want to avoid. - self._log.warning( - 'Message payload for %s is too long (%d) and cannot be sent', - state.request.__class__.__name__, len(state.data) - ) - state.future.set_exception( - ValueError('Request payload is too big')) - - size = 0 - continue - - if not batch: - return None, None - - if len(batch) > 1: - # Inlined code to pack several messages into a container - data = struct.pack( - ' Surrogate Pairs (Telegram offsets are calculated with these). - # See https://en.wikipedia.org/wiki/Plane_(Unicode)#Overview for more. - ''.join(chr(y) for y in struct.unpack(' left_offset: - if e.offset >= left_offset: - # 0 1|2 3 4 5 | 0 1|2 3 4 5 - # ^ ^ | ^ - # lo(2) o(5) | o(2)/lo(2) - e.offset -= left_offset - # |0 1 2 3 | |0 1 2 3 - # ^ | ^ - # o=o-lo(3=5-2) | o=o-lo(0=2-2) - else: - # e.offset < left_offset and e.offset + e.length > left_offset - # 0 1 2 3|4 5 6 7 8 9 10 - # ^ ^ ^ - # o(1) lo(4) o+l(1+9) - e.length = e.offset + e.length - left_offset - e.offset = 0 - # |0 1 2 3 4 5 6 - # ^ ^ - # o(0) o+l=0+o+l-lo(6=0+6=0+1+9-4) - else: - # e.offset + e.length <= left_offset - # 0 1 2 3|4 5 - # ^ ^ - # o(0) o+l(4) - # lo(4) - del entities[i] - continue - - if e.offset + e.length <= len_final: - # |0 1 2 3 4 5 6 7 8 9 - # ^ ^ - # o(1) o+l(1+9)/lf(10) - continue - if e.offset >= len_final: - # |0 1 2 3 4 - # ^ - # o(5)/lf(5) - del entities[i] - else: - # e.offset < len_final and e.offset + e.length > len_final - # |0 1 2 3 4 5 (6) (7) (8) (9) - # ^ ^ ^ - # o(1) lf(6) o+l(1+8) - e.length = len_final - e.offset - # |0 1 2 3 4 5 - # ^ ^ - # o(1) o+l=o+lf-o=lf(6=1+5=1+6-1) - - return text - - -def retry_range(retries, force_retry=True): - """ - Generates an integer sequence starting from 1. If `retries` is - not a zero or a positive integer value, the sequence will be - infinite, otherwise it will end at `retries + 1`. - """ - - # We need at least one iteration even if the retries are 0 - # when force_retry is True. - if force_retry and not (retries is None or retries < 0): - retries += 1 - - attempt = 0 - while attempt != retries: - attempt += 1 - yield attempt - - - -async def _maybe_await(value): - if inspect.isawaitable(value): - return await value - else: - return value - - -async def _cancel(log, **tasks): - """ - Helper to cancel one or more tasks gracefully, logging exceptions. - """ - for name, task in tasks.items(): - if not task: - continue - - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - except RuntimeError: - # Probably: RuntimeError: await wasn't used with future - # - # See: https://github.com/python/cpython/blob/12d3061c7819a73d891dcce44327410eaf0e1bc2/Lib/asyncio/futures.py#L265 - # - # Happens with _asyncio.Task instances (in "Task cancelling" state) - # trying to SIGINT the program right during initial connection, on - # _recv_loop coroutine (but we're creating its task explicitly with - # a loop, so how can it bug out like this?). - # - # Since we're aware of this error there's no point in logging it. - # *May* be https://bugs.python.org/issue37172 - pass - except AssertionError as e: - # In Python 3.6, the above RuntimeError is an AssertionError - # See https://github.com/python/cpython/blob/7df32f844efed33ca781a016017eab7050263b90/Lib/asyncio/futures.py#L328 - if e.args != ("yield from wasn't used with future",): - log.exception('Unhandled exception from %s after cancelling ' - '%s (%s)', name, type(task), task) - except Exception: - log.exception('Unhandled exception from %s after cancelling ' - '%s (%s)', name, type(task), task) - - -def _sync_enter(self): - """ - Helps to cut boilerplate on async context - managers that offer synchronous variants. - """ - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - if loop.is_running(): - raise RuntimeError( - 'You must use "async with" if the event loop ' - 'is running (i.e. you are inside an "async def")' - ) - - return loop.run_until_complete(self.__aenter__()) - - -def _sync_exit(self, *args): - if hasattr(self, 'loop'): - loop = self.loop - else: - loop = self._client.loop - - return loop.run_until_complete(self.__aexit__(*args)) - - -def _entity_type(entity): - # This could be a `utils` method that just ran a few `isinstance` on - # `utils.get_peer(...)`'s result. However, there are *a lot* of auto - # casts going on, plenty of calls and temporary short-lived objects. - # - # So we just check if a string is in the class name. - # Still, assert that it's the right type to not return false results. - try: - if entity.SUBCLASS_OF_ID not in ( - 0x2d45687, # crc32(b'Peer') - 0xc91c90b6, # crc32(b'InputPeer') - 0xe669bf46, # crc32(b'InputUser') - 0x40f202fd, # crc32(b'InputChannel') - 0x2da17977, # crc32(b'User') - 0xc5af5d94, # crc32(b'Chat') - 0x1f4661b9, # crc32(b'UserFull') - 0xd49a2697, # crc32(b'ChatFull') - ): - raise TypeError('{} does not have any entity type'.format(entity)) - except AttributeError: - raise TypeError('{} is not a TLObject, cannot determine entity type'.format(entity)) - - name = entity.__class__.__name__ - if 'User' in name: - return _EntityType.USER - elif 'Chat' in name: - return _EntityType.CHAT - elif 'Channel' in name: - return _EntityType.CHANNEL - elif 'Self' in name: - return _EntityType.USER - - # 'Empty' in name or not found, we don't care, not a valid entity. - raise TypeError('{} does not have any entity type'.format(entity)) - -# endregion - -# region Cryptographic related utils - - -def generate_key_data_from_nonce(server_nonce, new_nonce): - """Generates the key data corresponding to the given nonce""" - server_nonce = server_nonce.to_bytes(16, 'little', signed=True) - new_nonce = new_nonce.to_bytes(32, 'little', signed=True) - hash1 = sha1(new_nonce + server_nonce).digest() - hash2 = sha1(server_nonce + new_nonce).digest() - hash3 = sha1(new_nonce + new_nonce).digest() - - key = hash1 + hash2[:12] - iv = hash2[12:20] + hash3 + new_nonce[:4] - return key, iv - - -# endregion - -# region Custom Classes - - -class TotalList(list): - """ - A list with an extra `total` property, which may not match its `len` - since the total represents the total amount of items *available* - somewhere else, not the items *in this list*. - - Examples: - - .. code-block:: python - - # Telethon returns these lists in some cases (for example, - # only when a chunk is returned, but the "total" count - # is available). - result = await client.get_messages(chat, limit=10) - - print(result.total) # large number - print(len(result)) # 10 - print(result[0]) # latest message - - for x in result: # show the 10 messages - print(x.text) - - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.total = 0 - - def __str__(self): - return '[{}, total={}]'.format( - ', '.join(str(x) for x in self), self.total) - - def __repr__(self): - return '[{}, total={}]'.format( - ', '.join(repr(x) for x in self), self.total) - - -class _FileStream(io.IOBase): - """ - Proxy around things that represent a file and need to be used as streams - which may or not need to be closed. - - This will handle `pathlib.Path`, `str` paths, in-memory `bytes`, and - anything IO-like (including `aiofiles`). - - It also provides access to the name and file size (also necessary). - """ - def __init__(self, file, *, file_size=None): - if isinstance(file, Path): - file = str(file.absolute()) - - self._file = file - self._name = None - self._size = file_size - self._stream = None - self._close_stream = None - - async def __aenter__(self): - if isinstance(self._file, str): - self._name = os.path.basename(self._file) - self._size = os.path.getsize(self._file) - self._stream = open(self._file, 'rb') - self._close_stream = True - - elif isinstance(self._file, bytes): - self._size = len(self._file) - self._stream = io.BytesIO(self._file) - self._close_stream = True - - elif not callable(getattr(self._file, 'read', None)): - raise TypeError('file description should have a `read` method') - - elif self._size is not None: - self._name = getattr(self._file, 'name', None) - self._stream = self._file - self._close_stream = False - - else: - if callable(getattr(self._file, 'seekable', None)): - seekable = await _maybe_await(self._file.seekable()) - else: - seekable = False - - if seekable: - pos = await _maybe_await(self._file.tell()) - await _maybe_await(self._file.seek(0, os.SEEK_END)) - self._size = await _maybe_await(self._file.tell()) - await _maybe_await(self._file.seek(pos, os.SEEK_SET)) - self._stream = self._file - self._close_stream = False - else: - _log.warning( - 'Could not determine file size beforehand so the entire ' - 'file will be read in-memory') - - data = await _maybe_await(self._file.read()) - self._size = len(data) - self._stream = io.BytesIO(data) - self._close_stream = True - - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - if self._close_stream and self._stream: - await _maybe_await(self._stream.close()) - - @property - def file_size(self): - return self._size - - @property - def name(self): - return self._name - - # Proxy all the methods. Doesn't need to be readable (makes multiline edits easier) - def read(self, *args, **kwargs): return self._stream.read(*args, **kwargs) - def readinto(self, *args, **kwargs): return self._stream.readinto(*args, **kwargs) - def write(self, *args, **kwargs): return self._stream.write(*args, **kwargs) - def fileno(self, *args, **kwargs): return self._stream.fileno(*args, **kwargs) - def flush(self, *args, **kwargs): return self._stream.flush(*args, **kwargs) - def isatty(self, *args, **kwargs): return self._stream.isatty(*args, **kwargs) - def readable(self, *args, **kwargs): return self._stream.readable(*args, **kwargs) - def readline(self, *args, **kwargs): return self._stream.readline(*args, **kwargs) - def readlines(self, *args, **kwargs): return self._stream.readlines(*args, **kwargs) - def seek(self, *args, **kwargs): return self._stream.seek(*args, **kwargs) - def seekable(self, *args, **kwargs): return self._stream.seekable(*args, **kwargs) - def tell(self, *args, **kwargs): return self._stream.tell(*args, **kwargs) - def truncate(self, *args, **kwargs): return self._stream.truncate(*args, **kwargs) - def writable(self, *args, **kwargs): return self._stream.writable(*args, **kwargs) - def writelines(self, *args, **kwargs): return self._stream.writelines(*args, **kwargs) - - # close is special because it will be called by __del__ but we do NOT - # want to close the file unless we have to (we're just a wrapper). - # Instead, we do nothing (we should be used through the decorator which - # has its own mechanism to close the file correctly). - def close(self, *args, **kwargs): - pass - -# endregion - -def get_running_loop(): - if sys.version_info >= (3, 7): - try: - return asyncio.get_running_loop() - except RuntimeError: - return asyncio.get_event_loop_policy().get_event_loop() - else: - return asyncio.get_event_loop() diff --git a/telethon/hints.py b/telethon/hints.py deleted file mode 100644 index 67f830b6..00000000 --- a/telethon/hints.py +++ /dev/null @@ -1,67 +0,0 @@ -import datetime -import typing - -from . import helpers -from .tl import types, custom - -Phone = str -Username = str -PeerID = int -Entity = typing.Union[types.User, types.Chat, types.Channel] -FullEntity = typing.Union[types.UserFull, types.messages.ChatFull, types.ChatFull, types.ChannelFull] - -EntityLike = typing.Union[ - Phone, - Username, - PeerID, - types.TypePeer, - types.TypeInputPeer, - Entity, - FullEntity -] -EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] - -ButtonLike = typing.Union[types.TypeKeyboardButton, custom.Button] -MarkupLike = typing.Union[ - types.TypeReplyMarkup, - ButtonLike, - typing.Sequence[ButtonLike], - typing.Sequence[typing.Sequence[ButtonLike]] -] - -TotalList = helpers.TotalList - -DateLike = typing.Optional[typing.Union[float, datetime.datetime, datetime.date, datetime.timedelta]] - -LocalPath = str -ExternalUrl = str -BotFileID = str -FileLike = typing.Union[ - LocalPath, - ExternalUrl, - BotFileID, - bytes, - typing.BinaryIO, - types.TypeMessageMedia, - types.TypeInputFile, - types.TypeInputFileLocation -] - -# Can't use `typing.Type` in Python 3.5.2 -# See https://github.com/python/typing/issues/266 -try: - OutFileLike = typing.Union[ - str, - typing.Type[bytes], - typing.BinaryIO - ] -except TypeError: - OutFileLike = typing.Union[ - str, - typing.BinaryIO - ] - -MessageLike = typing.Union[str, types.Message] -MessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage] - -ProgressCallback = typing.Callable[[int, int], None] diff --git a/telethon/network/__init__.py b/telethon/network/__init__.py deleted file mode 100644 index 0b985d58..00000000 --- a/telethon/network/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -This module contains several classes regarding network, low level connection -with Telegram's servers and the protocol used (TCP full, abridged, etc.). -""" -from .mtprotoplainsender import MTProtoPlainSender -from .authenticator import do_authentication -from .mtprotosender import MTProtoSender -from .connection import ( - Connection, - ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, - ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, - ConnectionTcpMTProxyIntermediate, - ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy -) diff --git a/telethon/network/authenticator.py b/telethon/network/authenticator.py deleted file mode 100644 index ea476207..00000000 --- a/telethon/network/authenticator.py +++ /dev/null @@ -1,212 +0,0 @@ -""" -This module contains several functions that authenticate the client machine -with Telegram's servers, effectively creating an authorization key. -""" -import os -import time -from hashlib import sha1 - -from ..tl.types import ( - ResPQ, PQInnerData, ServerDHParamsFail, ServerDHParamsOk, - ServerDHInnerData, ClientDHInnerData, DhGenOk, DhGenRetry, DhGenFail -) -from .. import helpers -from ..crypto import AES, AuthKey, Factorization, rsa -from ..errors import SecurityError -from ..extensions import BinaryReader -from ..tl.functions import ( - ReqPqMultiRequest, ReqDHParamsRequest, SetClientDHParamsRequest -) - - -async def do_authentication(sender): - """ - Executes the authentication process with the Telegram servers. - - :param sender: a connected `MTProtoPlainSender`. - :return: returns a (authorization key, time offset) tuple. - """ - # Step 1 sending: PQ Request, endianness doesn't matter since it's random - nonce = int.from_bytes(os.urandom(16), 'big', signed=True) - res_pq = await sender.send(ReqPqMultiRequest(nonce)) - assert isinstance(res_pq, ResPQ), 'Step 1 answer was %s' % res_pq - - if res_pq.nonce != nonce: - raise SecurityError('Step 1 invalid nonce from server') - - pq = get_int(res_pq.pq) - - # Step 2 sending: DH Exchange - p, q = Factorization.factorize(pq) - p, q = rsa.get_byte_array(p), rsa.get_byte_array(q) - new_nonce = int.from_bytes(os.urandom(32), 'little', signed=True) - - pq_inner_data = bytes(PQInnerData( - pq=rsa.get_byte_array(pq), p=p, q=q, - nonce=res_pq.nonce, - server_nonce=res_pq.server_nonce, - new_nonce=new_nonce - )) - - # sha_digest + data + random_bytes - cipher_text, target_fingerprint = None, None - for fingerprint in res_pq.server_public_key_fingerprints: - cipher_text = rsa.encrypt(fingerprint, pq_inner_data) - if cipher_text is not None: - target_fingerprint = fingerprint - break - - if cipher_text is None: - # Second attempt, but now we're allowed to use old keys - for fingerprint in res_pq.server_public_key_fingerprints: - cipher_text = rsa.encrypt(fingerprint, pq_inner_data, use_old=True) - if cipher_text is not None: - target_fingerprint = fingerprint - break - - if cipher_text is None: - raise SecurityError( - 'Step 2 could not find a valid key for fingerprints: {}' - .format(', '.join( - [str(f) for f in res_pq.server_public_key_fingerprints]) - ) - ) - - server_dh_params = await sender.send(ReqDHParamsRequest( - nonce=res_pq.nonce, - server_nonce=res_pq.server_nonce, - p=p, q=q, - public_key_fingerprint=target_fingerprint, - encrypted_data=cipher_text - )) - - assert isinstance( - server_dh_params, (ServerDHParamsOk, ServerDHParamsFail)),\ - 'Step 2.1 answer was %s' % server_dh_params - - if server_dh_params.nonce != res_pq.nonce: - raise SecurityError('Step 2 invalid nonce from server') - - if server_dh_params.server_nonce != res_pq.server_nonce: - raise SecurityError('Step 2 invalid server nonce from server') - - if isinstance(server_dh_params, ServerDHParamsFail): - nnh = int.from_bytes( - sha1(new_nonce.to_bytes(32, 'little', signed=True)).digest()[4:20], - 'little', signed=True - ) - if server_dh_params.new_nonce_hash != nnh: - raise SecurityError('Step 2 invalid DH fail nonce from server') - - assert isinstance(server_dh_params, ServerDHParamsOk),\ - 'Step 2.2 answer was %s' % server_dh_params - - # Step 3 sending: Complete DH Exchange - key, iv = helpers.generate_key_data_from_nonce( - res_pq.server_nonce, new_nonce - ) - if len(server_dh_params.encrypted_answer) % 16 != 0: - # See PR#453 - raise SecurityError('Step 3 AES block size mismatch') - - plain_text_answer = AES.decrypt_ige( - server_dh_params.encrypted_answer, key, iv - ) - - with BinaryReader(plain_text_answer) as reader: - reader.read(20) # hash sum - server_dh_inner = reader.tgread_object() - assert isinstance(server_dh_inner, ServerDHInnerData),\ - 'Step 3 answer was %s' % server_dh_inner - - if server_dh_inner.nonce != res_pq.nonce: - raise SecurityError('Step 3 Invalid nonce in encrypted answer') - - if server_dh_inner.server_nonce != res_pq.server_nonce: - raise SecurityError('Step 3 Invalid server nonce in encrypted answer') - - dh_prime = get_int(server_dh_inner.dh_prime, signed=False) - g = server_dh_inner.g - g_a = get_int(server_dh_inner.g_a, signed=False) - time_offset = server_dh_inner.server_time - int(time.time()) - - b = get_int(os.urandom(256), signed=False) - g_b = pow(g, b, dh_prime) - gab = pow(g_a, b, dh_prime) - - # IMPORTANT: Apart from the conditions on the Diffie-Hellman prime - # dh_prime and generator g, both sides are to check that g, g_a and - # g_b are greater than 1 and less than dh_prime - 1. We recommend - # checking that g_a and g_b are between 2^{2048-64} and - # dh_prime - 2^{2048-64} as well. - # (https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete) - if not (1 < g < (dh_prime - 1)): - raise SecurityError('g_a is not within (1, dh_prime - 1)') - - if not (1 < g_a < (dh_prime - 1)): - raise SecurityError('g_a is not within (1, dh_prime - 1)') - - if not (1 < g_b < (dh_prime - 1)): - raise SecurityError('g_b is not within (1, dh_prime - 1)') - - safety_range = 2 ** (2048 - 64) - if not (safety_range <= g_a <= (dh_prime - safety_range)): - raise SecurityError('g_a is not within (2^{2048-64}, dh_prime - 2^{2048-64})') - - if not (safety_range <= g_b <= (dh_prime - safety_range)): - raise SecurityError('g_b is not within (2^{2048-64}, dh_prime - 2^{2048-64})') - - # Prepare client DH Inner Data - client_dh_inner = bytes(ClientDHInnerData( - nonce=res_pq.nonce, - server_nonce=res_pq.server_nonce, - retry_id=0, # TODO Actual retry ID - g_b=rsa.get_byte_array(g_b) - )) - - client_dh_inner_hashed = sha1(client_dh_inner).digest() + client_dh_inner - - # Encryption - client_dh_encrypted = AES.encrypt_ige(client_dh_inner_hashed, key, iv) - - # Prepare Set client DH params - dh_gen = await sender.send(SetClientDHParamsRequest( - nonce=res_pq.nonce, - server_nonce=res_pq.server_nonce, - encrypted_data=client_dh_encrypted, - )) - - nonce_types = (DhGenOk, DhGenRetry, DhGenFail) - assert isinstance(dh_gen, nonce_types), 'Step 3.1 answer was %s' % dh_gen - name = dh_gen.__class__.__name__ - if dh_gen.nonce != res_pq.nonce: - raise SecurityError('Step 3 invalid {} nonce from server'.format(name)) - - if dh_gen.server_nonce != res_pq.server_nonce: - raise SecurityError( - 'Step 3 invalid {} server nonce from server'.format(name)) - - auth_key = AuthKey(rsa.get_byte_array(gab)) - nonce_number = 1 + nonce_types.index(type(dh_gen)) - new_nonce_hash = auth_key.calc_new_nonce_hash(new_nonce, nonce_number) - - dh_hash = getattr(dh_gen, 'new_nonce_hash{}'.format(nonce_number)) - if dh_hash != new_nonce_hash: - raise SecurityError('Step 3 invalid new nonce hash') - - if not isinstance(dh_gen, DhGenOk): - raise AssertionError('Step 3.2 answer was %s' % dh_gen) - - return auth_key, time_offset - - -def get_int(byte_array, signed=True): - """ - Gets the specified integer from its byte array. - This should be used by this module alone, as it works with big endian. - - :param byte_array: the byte array representing th integer. - :param signed: whether the number is signed or not. - :return: the integer representing the given byte array. - """ - return int.from_bytes(byte_array, byteorder='big', signed=signed) diff --git a/telethon/network/connection/__init__.py b/telethon/network/connection/__init__.py deleted file mode 100644 index 88771866..00000000 --- a/telethon/network/connection/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .connection import Connection -from .tcpfull import ConnectionTcpFull -from .tcpintermediate import ConnectionTcpIntermediate -from .tcpabridged import ConnectionTcpAbridged -from .tcpobfuscated import ConnectionTcpObfuscated -from .tcpmtproxy import ( - TcpMTProxy, - ConnectionTcpMTProxyAbridged, - ConnectionTcpMTProxyIntermediate, - ConnectionTcpMTProxyRandomizedIntermediate -) -from .http import ConnectionHttp diff --git a/telethon/network/connection/connection.py b/telethon/network/connection/connection.py deleted file mode 100644 index 838b3b88..00000000 --- a/telethon/network/connection/connection.py +++ /dev/null @@ -1,434 +0,0 @@ -import abc -import asyncio -import socket -import sys - -try: - import ssl as ssl_mod -except ImportError: - ssl_mod = None - -try: - import python_socks -except ImportError: - python_socks = None - -from ...errors import InvalidChecksumError, InvalidBufferError -from ... import helpers - - -class Connection(abc.ABC): - """ - The `Connection` class is a wrapper around ``asyncio.open_connection``. - - Subclasses will implement different transport modes as atomic operations, - which this class eases doing since the exposed interface simply puts and - gets complete data payloads to and from queues. - - The only error that will raise from send and receive methods is - ``ConnectionError``, which will raise when attempting to send if - the client is disconnected (includes remote disconnections). - """ - # this static attribute should be redefined by `Connection` subclasses and - # should be one of `PacketCodec` implementations - packet_codec = None - - def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None): - self._ip = ip - self._port = port - self._dc_id = dc_id # only for MTProxy, it's an abstraction leak - self._log = loggers[__name__] - self._proxy = proxy - self._local_addr = local_addr - self._reader = None - self._writer = None - self._connected = False - self._send_task = None - self._recv_task = None - self._codec = None - self._obfuscation = None # TcpObfuscated and MTProxy - self._send_queue = asyncio.Queue(1) - self._recv_queue = asyncio.Queue(1) - - @staticmethod - def _wrap_socket_ssl(sock): - if ssl_mod is None: - raise RuntimeError( - 'Cannot use proxy that requires SSL ' - 'without the SSL module being available' - ) - - return ssl_mod.wrap_socket( - sock, - do_handshake_on_connect=True, - ssl_version=ssl_mod.PROTOCOL_SSLv23, - ciphers='ADH-AES256-SHA') - - @staticmethod - def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None): - if isinstance(proxy_type, str): - proxy_type = proxy_type.lower() - - # Always prefer `python_socks` when available - if python_socks: - from python_socks import ProxyType - - # We do the check for numerical values here - # to be backwards compatible with PySocks proxy format, - # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3) - if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5": - protocol = ProxyType.SOCKS5 - elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4": - protocol = ProxyType.SOCKS4 - elif proxy_type == ProxyType.HTTP or proxy_type == 3 or proxy_type == "http": - protocol = ProxyType.HTTP - else: - raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) - - # This tuple must be compatible with `python_socks`' `Proxy.create()` signature - return protocol, addr, port, username, password, rdns - - else: - from socks import SOCKS5, SOCKS4, HTTP - - if proxy_type == 2 or proxy_type == "socks5": - protocol = SOCKS5 - elif proxy_type == 1 or proxy_type == "socks4": - protocol = SOCKS4 - elif proxy_type == 3 or proxy_type == "http": - protocol = HTTP - else: - raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) - - # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature - return protocol, addr, port, rdns, username, password - - async def _proxy_connect(self, timeout=None, local_addr=None): - if isinstance(self._proxy, (tuple, list)): - parsed = self._parse_proxy(*self._proxy) - elif isinstance(self._proxy, dict): - parsed = self._parse_proxy(**self._proxy) - else: - raise TypeError("Proxy of unknown format: {}".format(type(self._proxy))) - - # Always prefer `python_socks` when available - if python_socks: - # python_socks internal errors are not inherited from - # builtin IOError (just from Exception). Instead of adding those - # in exceptions clauses everywhere through the code, we - # rather monkey-patch them in place. - - python_socks._errors.ProxyError = ConnectionError - python_socks._errors.ProxyConnectionError = ConnectionError - python_socks._errors.ProxyTimeoutError = ConnectionError - - from python_socks.async_.asyncio import Proxy - - proxy = Proxy.create(*parsed) - - # WARNING: If `local_addr` is set we use manual socket creation, because, - # unfortunately, `Proxy.connect()` does not expose `local_addr` - # argument, so if we want to bind socket locally, we need to manually - # create, bind and connect socket, and then pass to `Proxy.connect()` method. - - if local_addr is None: - sock = await proxy.connect( - dest_host=self._ip, - dest_port=self._port, - timeout=timeout - ) - else: - # Here we start manual setup of the socket. - # The `address` represents the proxy ip and proxy port, - # not the destination one (!), because the socket - # connects to the proxy server, not destination server. - # IPv family is also checked on proxy address. - if ':' in proxy.proxy_host: - mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0) - else: - mode, address = socket.AF_INET, (proxy.proxy_host, proxy.proxy_port) - - # Create a non-blocking socket and bind it (if local address is specified). - sock = socket.socket(mode, socket.SOCK_STREAM) - sock.setblocking(False) - sock.bind(local_addr) - - # Actual TCP connection is performed here. - await asyncio.wait_for( - helpers.get_running_loop().sock_connect(sock=sock, address=address), - timeout=timeout - ) - - # As our socket is already created and connected, - # this call sets the destination host/port and - # starts protocol negotiations with the proxy server. - sock = await proxy.connect( - dest_host=self._ip, - dest_port=self._port, - timeout=timeout, - _socket=sock - ) - - else: - import socks - - # Here `address` represents destination address (not proxy), because of - # the `PySocks` implementation of the connection routine. - # IPv family is checked on proxy address, not destination address. - if ':' in parsed[1]: - mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) - else: - mode, address = socket.AF_INET, (self._ip, self._port) - - # Setup socket, proxy, timeout and bind it (if necessary). - sock = socks.socksocket(mode, socket.SOCK_STREAM) - sock.set_proxy(*parsed) - sock.settimeout(timeout) - - if local_addr is not None: - sock.bind(local_addr) - - # Actual TCP connection and negotiation performed here. - await asyncio.wait_for( - helpers.get_running_loop().sock_connect(sock=sock, address=address), - timeout=timeout - ) - - sock.setblocking(False) - - return sock - - async def _connect(self, timeout=None, ssl=None): - if self._local_addr is not None: - # NOTE: If port is not specified, we use 0 port - # to notify the OS that port should be chosen randomly - # from the available ones. - if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2: - local_addr = self._local_addr - elif isinstance(self._local_addr, str): - local_addr = (self._local_addr, 0) - else: - raise ValueError("Unknown local address format: {}".format(self._local_addr)) - else: - local_addr = None - - if not self._proxy: - self._reader, self._writer = await asyncio.wait_for( - asyncio.open_connection( - host=self._ip, - port=self._port, - ssl=ssl, - local_addr=local_addr - ), timeout=timeout) - else: - # Proxy setup, connection and negotiation is performed here. - sock = await self._proxy_connect( - timeout=timeout, - local_addr=local_addr - ) - - # Wrap socket in SSL context (if provided) - if ssl: - sock = self._wrap_socket_ssl(sock) - - self._reader, self._writer = await asyncio.open_connection(sock=sock) - - self._codec = self.packet_codec(self) - self._init_conn() - await self._writer.drain() - - async def connect(self, timeout=None, ssl=None): - """ - Establishes a connection with the server. - """ - await self._connect(timeout=timeout, ssl=ssl) - self._connected = True - - loop = helpers.get_running_loop() - self._send_task = loop.create_task(self._send_loop()) - self._recv_task = loop.create_task(self._recv_loop()) - - async def disconnect(self): - """ - Disconnects from the server, and clears - pending outgoing and incoming messages. - """ - if not self._connected: - return - - self._connected = False - - await helpers._cancel( - self._log, - send_task=self._send_task, - recv_task=self._recv_task - ) - - if self._writer: - self._writer.close() - if sys.version_info >= (3, 7): - try: - await asyncio.wait_for(self._writer.wait_closed(), timeout=10) - except asyncio.TimeoutError: - # See issue #3917. For some users, this line was hanging indefinitely. - # The hard timeout is not ideal (connection won't be properly closed), - # but the code will at least be able to procceed. - self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup') - except Exception as e: - # Disconnecting should never raise. Seen: - # * OSError: No route to host and - # * OSError: [Errno 32] Broken pipe - # * ConnectionResetError - self._log.info('%s during disconnect: %s', type(e), e) - - def send(self, data): - """ - Sends a packet of data through this connection mode. - - This method returns a coroutine. - """ - if not self._connected: - raise ConnectionError('Not connected') - - return self._send_queue.put(data) - - async def recv(self): - """ - Receives a packet of data through this connection mode. - - This method returns a coroutine. - """ - while self._connected: - result, err = await self._recv_queue.get() - if err: - raise err - if result: - return result - - raise ConnectionError('Not connected') - - async def _send_loop(self): - """ - This loop is constantly popping items off the queue to send them. - """ - try: - while self._connected: - self._send(await self._send_queue.get()) - await self._writer.drain() - except asyncio.CancelledError: - pass - except Exception as e: - if isinstance(e, IOError): - self._log.info('The server closed the connection while sending') - else: - self._log.exception('Unexpected exception in the send loop') - - await self.disconnect() - - async def _recv_loop(self): - """ - This loop is constantly putting items on the queue as they're read. - """ - try: - while self._connected: - try: - data = await self._recv() - except asyncio.CancelledError: - break - except (IOError, asyncio.IncompleteReadError) as e: - self._log.warning('Server closed the connection: %s', e) - await self._recv_queue.put((None, e)) - await self.disconnect() - except InvalidChecksumError as e: - self._log.warning('Server response had invalid checksum: %s', e) - await self._recv_queue.put((None, e)) - except InvalidBufferError as e: - self._log.warning('Server response had invalid buffer: %s', e) - await self._recv_queue.put((None, e)) - except Exception as e: - self._log.exception('Unexpected exception in the receive loop') - await self._recv_queue.put((None, e)) - await self.disconnect() - else: - await self._recv_queue.put((data, None)) - finally: - await self.disconnect() - - - def _init_conn(self): - """ - This method will be called after `connect` is called. - After this method finishes, the writer will be drained. - - Subclasses should make use of this if they need to send - data to Telegram to indicate which connection mode will - be used. - """ - if self._codec.tag: - self._writer.write(self._codec.tag) - - def _send(self, data): - self._writer.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._reader) - - def __str__(self): - return '{}:{}/{}'.format( - self._ip, self._port, - self.__class__.__name__.replace('Connection', '') - ) - - -class ObfuscatedConnection(Connection): - """ - Base class for "obfuscated" connections ("obfuscated2", "mtproto proxy") - """ - """ - This attribute should be redefined by subclasses - """ - obfuscated_io = None - - def _init_conn(self): - self._obfuscation = self.obfuscated_io(self) - self._writer.write(self._obfuscation.header) - - def _send(self, data): - self._obfuscation.write(self._codec.encode_packet(data)) - - async def _recv(self): - return await self._codec.read_packet(self._obfuscation) - - -class PacketCodec(abc.ABC): - """ - Base class for packet codecs - """ - - """ - This attribute should be re-defined by subclass to define if some - "magic bytes" should be sent to server right after connection is made to - signal which protocol will be used - """ - tag = None - - def __init__(self, connection): - """ - Codec is created when connection is just made. - """ - self._conn = connection - - @abc.abstractmethod - def encode_packet(self, data): - """ - Encodes single packet and returns encoded bytes. - """ - raise NotImplementedError - - @abc.abstractmethod - async def read_packet(self, reader): - """ - Reads single packet from `reader` object that should have - `readexactly(n)` method. - """ - raise NotImplementedError diff --git a/telethon/network/connection/http.py b/telethon/network/connection/http.py deleted file mode 100644 index e2d976f7..00000000 --- a/telethon/network/connection/http.py +++ /dev/null @@ -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 deleted file mode 100644 index 171b1d8c..00000000 --- a/telethon/network/connection/tcpabridged.py +++ /dev/null @@ -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 deleted file mode 100644 index 69a43bce..00000000 --- a/telethon/network/connection/tcpmtproxy.py +++ /dev/null @@ -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, *, loggers, proxy=None, local_addr=None): - # connect to proxy's host and port instead of telegram's ones - proxy_host, proxy_port = self.address_info(proxy) - self._secret = bytes.fromhex(proxy[2]) - super().__init__( - proxy_host, proxy_port, dc_id, loggers=loggers) - - async def _connect(self, timeout=None, ssl=None): - await super()._connect(timeout=timeout, ssl=ssl) - - # Wait for EOF for 2 seconds (or if _wait_for_data's definition - # is missing or different, just sleep for 2 seconds). This way - # we give the proxy a chance to close the connection if the current - # codec (which the proxy detects with the data we sent) cannot - # be used for this proxy. This is a work around for #1134. - # TODO Sleeping for N seconds may not be the best solution - # TODO This fix could be welcome for HTTP proxies as well - try: - await asyncio.wait_for(self._reader._wait_for_data('proxy'), 2) - except asyncio.TimeoutError: - pass - except Exception: - await asyncio.sleep(2) - - if self._reader.at_eof(): - await self.disconnect() - raise ConnectionError( - 'Proxy closed the connection after sending initial payload') - - @staticmethod - def address_info(proxy_info): - if proxy_info is None: - raise ValueError("No proxy info specified for MTProxy connection") - return proxy_info[:2] - - -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 deleted file mode 100644 index cf2e6af5..00000000 --- a/telethon/network/connection/tcpobfuscated.py +++ /dev/null @@ -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:8] != b'\0\0\0\0'): - break - - random = bytearray(random) - random_reversed = random[55:7:-1] # Reversed (8, len=48) - - # Encryption has "continuous buffer" enabled - encrypt_key = bytes(random[8:40]) - encrypt_iv = bytes(random[40:56]) - decrypt_key = bytes(random_reversed[:32]) - decrypt_iv = bytes(random_reversed[32:48]) - - encryptor = AESModeCTR(encrypt_key, encrypt_iv) - decryptor = AESModeCTR(decrypt_key, decrypt_iv) - - random[56:60] = packet_codec.obfuscate_tag - random[56:64] = encryptor.encrypt(bytes(random))[56:64] - return (random, encryptor, decryptor) - - async def readexactly(self, n): - return self._decrypt.encrypt(await self._reader.readexactly(n)) - - def write(self, data): - self._writer.write(self._encrypt.encrypt(data)) - - -class ConnectionTcpObfuscated(ObfuscatedConnection): - """ - Mode that Telegram defines as "obfuscated2". Encodes the packet - just like `ConnectionTcpAbridged`, but encrypts every message with - a randomly generated key using the AES-CTR mode so the packets are - harder to discern. - """ - obfuscated_io = ObfuscatedIO - packet_codec = AbridgedPacketCodec diff --git a/telethon/network/mtprotoplainsender.py b/telethon/network/mtprotoplainsender.py deleted file mode 100644 index 563affd7..00000000 --- a/telethon/network/mtprotoplainsender.py +++ /dev/null @@ -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 deleted file mode 100644 index 86e2f9fa..00000000 --- a/telethon/network/mtprotosender.py +++ /dev/null @@ -1,909 +0,0 @@ -import asyncio -import collections -import struct - -from . import authenticator -from ..extensions.messagepacker import MessagePacker -from .mtprotoplainsender import MTProtoPlainSender -from .requeststate import RequestState -from .mtprotostate import MTProtoState -from ..tl.tlobject import TLRequest -from .. import helpers, utils -from ..errors import ( - BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError, - TypeNotFoundError, rpc_message_to_error -) -from ..extensions import BinaryReader -from ..tl.core import RpcResult, MessageContainer, GzipPacked -from ..tl.functions.auth import LogOutRequest -from ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest -from ..tl.types import ( - MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts, - MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq, - MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone, - DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail -) -from ..tl import types as _tl -from ..crypto import AuthKey -from ..helpers import retry_range - - -class MTProtoSender: - """ - MTProto Mobile Protocol sender - (https://core.telegram.org/mtproto/description). - - This class is responsible for wrapping requests into `TLMessage`'s, - sending them over the network and receiving them in a safe manner. - - Automatic reconnection due to temporary network issues is a concern - for this class as well, including retry of messages that could not - be sent successfully. - - A new authorization key will be generated on connection if no other - key exists yet. - """ - def __init__(self, auth_key, *, loggers, - retries=5, delay=1, auto_reconnect=True, connect_timeout=None, - auth_key_callback=None, - updates_queue=None, auto_reconnect_callback=None): - self._connection = None - self._loggers = loggers - self._log = loggers[__name__] - self._retries = retries - self._delay = delay - self._auto_reconnect = auto_reconnect - self._connect_timeout = connect_timeout - self._auth_key_callback = auth_key_callback - self._updates_queue = updates_queue - self._auto_reconnect_callback = auto_reconnect_callback - self._connect_lock = asyncio.Lock() - self._ping = None - - # Whether the user has explicitly connected or disconnected. - # - # If a disconnection happens for any other reason and it - # was *not* user action then the pending messages won't - # be cleared but on explicit user disconnection all the - # pending futures should be cancelled. - self._user_connected = False - self._reconnecting = False - self._disconnected = helpers.get_running_loop().create_future() - self._disconnected.set_result(None) - - # We need to join the loops upon disconnection - self._send_loop_handle = None - self._recv_loop_handle = None - - # Preserving the references of the AuthKey and state is important - self.auth_key = auth_key or AuthKey(None) - self._state = MTProtoState(self.auth_key, loggers=self._loggers) - - # Outgoing messages are put in a queue and sent in a batch. - # Note that here we're also storing their ``_RequestState``. - self._send_queue = MessagePacker(self._state, loggers=self._loggers) - - # Sent states are remembered until a response is received. - self._pending_state = {} - - # Responses must be acknowledged, and we can also batch these. - self._pending_ack = set() - - # Similar to pending_messages but only for the last acknowledges. - # These can't go in pending_messages because no acknowledge for them - # is received, but we may still need to resend their state on bad salts. - self._last_acks = collections.deque(maxlen=10) - - # Jump table from response ID to method that handles it - self._handlers = { - RpcResult.CONSTRUCTOR_ID: self._handle_rpc_result, - MessageContainer.CONSTRUCTOR_ID: self._handle_container, - GzipPacked.CONSTRUCTOR_ID: self._handle_gzip_packed, - Pong.CONSTRUCTOR_ID: self._handle_pong, - BadServerSalt.CONSTRUCTOR_ID: self._handle_bad_server_salt, - BadMsgNotification.CONSTRUCTOR_ID: self._handle_bad_notification, - MsgDetailedInfo.CONSTRUCTOR_ID: self._handle_detailed_info, - MsgNewDetailedInfo.CONSTRUCTOR_ID: self._handle_new_detailed_info, - NewSessionCreated.CONSTRUCTOR_ID: self._handle_new_session_created, - MsgsAck.CONSTRUCTOR_ID: self._handle_ack, - FutureSalts.CONSTRUCTOR_ID: self._handle_future_salts, - MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten, - MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all, - DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session, - DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session, - DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key, - DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key, - DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key, - } - - # Public API - - async def connect(self, connection): - """ - Connects to the specified given connection using the given auth key. - """ - async with self._connect_lock: - if self._user_connected: - self._log.info('User is already connected!') - return False - - self._connection = connection - await self._connect() - self._user_connected = True - return True - - def is_connected(self): - return self._user_connected - - def _transport_connected(self): - return ( - not self._reconnecting - and self._connection is not None - and self._connection._connected - ) - - async def disconnect(self): - """ - Cleanly disconnects the instance from the network, cancels - all pending requests, and closes the send and receive loops. - """ - await self._disconnect() - - def send(self, request, ordered=False): - """ - This method enqueues the given request to be sent. Its send - state will be saved until a response arrives, and a ``Future`` - that will be resolved when the response arrives will be returned: - - .. code-block:: python - - async def method(): - # Sending (enqueued for the send loop) - future = sender.send(request) - # Receiving (waits for the receive loop to read the result) - result = await future - - Designed like this because Telegram may send the response at - any point, and it can send other items while one waits for it. - Once the response for this future arrives, it is set with the - received result, quite similar to how a ``receive()`` call - would otherwise work. - - Since the receiving part is "built in" the future, it's - impossible to await receive a result that was never sent. - """ - if not self._user_connected: - raise ConnectionError('Cannot send requests while disconnected') - - if not utils.is_list_like(request): - try: - state = RequestState(request) - except struct.error as e: - # "struct.error: required argument is not an integer" is not - # very helpful; log the request to find out what wasn't int. - self._log.error('Request caused struct.error: %s: %s', e, request) - raise - - self._send_queue.append(state) - return state.future - else: - states = [] - futures = [] - state = None - for req in request: - try: - state = RequestState(req, after=ordered and state) - except struct.error as e: - self._log.error('Request caused struct.error: %s: %s', e, request) - raise - - states.append(state) - futures.append(state.future) - - self._send_queue.extend(states) - return futures - - @property - def disconnected(self): - """ - Future that resolves when the connection to Telegram - ends, either by user action or in the background. - - Note that it may resolve in either a ``ConnectionError`` - or any other unexpected error that could not be handled. - """ - return asyncio.shield(self._disconnected) - - # Private methods - - async def _connect(self): - """ - Performs the actual connection, retrying, generating the - authorization key if necessary, and starting the send and - receive loops. - """ - self._log.info('Connecting to %s...', self._connection) - - connected = False - - for attempt in retry_range(self._retries): - if not connected: - connected = await self._try_connect(attempt) - if not connected: - continue # skip auth key generation until we're connected - - if not self.auth_key: - try: - if not await self._try_gen_auth_key(attempt): - continue # keep retrying until we have the auth key - except (IOError, asyncio.TimeoutError) as e: - # Sometimes, specially during user-DC migrations, - # Telegram may close the connection during auth_key - # generation. If that's the case, we will need to - # connect again. - self._log.warning('Connection error %d during auth_key gen: %s: %s', - attempt, type(e).__name__, e) - - # Whatever the IOError was, make sure to disconnect so we can - # reconnect cleanly after. - await self._connection.disconnect() - connected = False - await asyncio.sleep(self._delay) - continue # next iteration we will try to reconnect - - break # all steps done, break retry loop - else: - if not connected: - raise ConnectionError('Connection to Telegram failed {} time(s)'.format(self._retries)) - - e = ConnectionError('auth_key generation failed {} time(s)'.format(self._retries)) - await self._disconnect(error=e) - raise e - - loop = helpers.get_running_loop() - self._log.debug('Starting send loop') - self._send_loop_handle = loop.create_task(self._send_loop()) - - self._log.debug('Starting receive loop') - self._recv_loop_handle = loop.create_task(self._recv_loop()) - - # _disconnected only completes after manual disconnection - # or errors after which the sender cannot continue such - # as failing to reconnect or any unexpected error. - if self._disconnected.done(): - self._disconnected = loop.create_future() - - self._log.info('Connection to %s complete!', self._connection) - - async def _try_connect(self, attempt): - try: - self._log.debug('Connection attempt %d...', attempt) - await self._connection.connect(timeout=self._connect_timeout) - self._log.debug('Connection success!') - return True - except (IOError, asyncio.TimeoutError) as e: - self._log.warning('Attempt %d at connecting failed: %s: %s', - attempt, type(e).__name__, e) - await asyncio.sleep(self._delay) - return False - - async def _try_gen_auth_key(self, attempt): - plain = MTProtoPlainSender(self._connection, loggers=self._loggers) - try: - self._log.debug('New auth_key attempt %d...', attempt) - self.auth_key.key, self._state.time_offset = \ - await authenticator.do_authentication(plain) - - # This is *EXTREMELY* important since we don't control - # external references to the authorization key, we must - # notify whenever we change it. This is crucial when we - # switch to different data centers. - if self._auth_key_callback: - self._auth_key_callback(self.auth_key) - - self._log.debug('auth_key generation success!') - return True - except (SecurityError, AssertionError) as e: - self._log.warning('Attempt %d at new auth_key failed: %s', attempt, e) - await asyncio.sleep(self._delay) - return False - - async def _disconnect(self, error=None): - if self._connection is None: - self._log.info('Not disconnecting (already have no connection)') - return - - self._log.info('Disconnecting from %s...', self._connection) - self._user_connected = False - try: - self._log.debug('Closing current connection...') - await self._connection.disconnect() - finally: - self._log.debug('Cancelling %d pending message(s)...', len(self._pending_state)) - for state in self._pending_state.values(): - if error and not state.future.done(): - state.future.set_exception(error) - else: - state.future.cancel() - - self._pending_state.clear() - await helpers._cancel( - self._log, - send_loop_handle=self._send_loop_handle, - recv_loop_handle=self._recv_loop_handle - ) - - self._log.info('Disconnection from %s complete!', self._connection) - self._connection = None - - if self._disconnected and not self._disconnected.done(): - if error: - self._disconnected.set_exception(error) - else: - self._disconnected.set_result(None) - - async def _reconnect(self, last_error): - """ - Cleanly disconnects and then reconnects. - """ - self._log.info('Closing current connection to begin reconnect...') - await self._connection.disconnect() - - await helpers._cancel( - self._log, - send_loop_handle=self._send_loop_handle, - recv_loop_handle=self._recv_loop_handle - ) - - # TODO See comment in `_start_reconnect` - # Perhaps this should be the last thing to do? - # But _connect() creates tasks which may run and, - # if they see that reconnecting is True, they will end. - # Perhaps that task creation should not belong in connect? - self._reconnecting = False - - # Start with a clean state (and thus session ID) to avoid old msgs - self._state.reset() - - retries = self._retries if self._auto_reconnect else 0 - - attempt = 0 - ok = True - # We're already "retrying" to connect, so we don't want to force retries - for attempt in retry_range(retries, force_retry=False): - try: - await self._connect() - except (IOError, asyncio.TimeoutError) as e: - last_error = e - self._log.info('Failed reconnection attempt %d with %s', - attempt, e.__class__.__name__) - await asyncio.sleep(self._delay) - except BufferError as e: - # TODO there should probably only be one place to except all these errors - if isinstance(e, InvalidBufferError) and e.code == 404: - self._log.info('Server does not know about the current auth key; the session may need to be recreated') - last_error = AuthKeyNotFound() - ok = False - break - else: - self._log.warning('Invalid buffer %s', e) - - except Exception as e: - last_error = e - self._log.exception('Unexpected exception reconnecting on ' - 'attempt %d', attempt) - - await asyncio.sleep(self._delay) - else: - self._send_queue.extend(self._pending_state.values()) - self._pending_state.clear() - - if self._auto_reconnect_callback: - helpers.get_running_loop().create_task(self._auto_reconnect_callback()) - - break - else: - ok = False - - if not ok: - self._log.error('Automatic reconnection failed %d time(s)', attempt) - # There may be no error (e.g. automatic reconnection was turned off). - error = last_error.with_traceback(None) if last_error else None - await self._disconnect(error=error) - - def _start_reconnect(self, error): - """Starts a reconnection in the background.""" - if self._user_connected and not self._reconnecting: - # We set reconnecting to True here and not inside the new task - # because it may happen that send/recv loop calls this again - # while the new task hasn't had a chance to run yet. This race - # condition puts `self.connection` in a bad state with two calls - # to its `connect` without disconnecting, so it creates a second - # receive loop. There can't be two tasks receiving data from - # the reader, since that causes an error, and the library just - # gets stuck. - # TODO It still gets stuck? Investigate where and why. - self._reconnecting = True - helpers.get_running_loop().create_task(self._reconnect(error)) - - def _keepalive_ping(self, rnd_id): - """ - Send a keep-alive ping. If a pong for the last ping was not received - yet, this means we're probably not connected. - """ - # TODO this is ugly, update loop shouldn't worry about this, sender should - if self._ping is None: - self._ping = rnd_id - self.send(PingRequest(rnd_id)) - else: - self._start_reconnect(None) - - # Loops - - async def _send_loop(self): - """ - This loop is responsible for popping items off the send - queue, encrypting them, and sending them over the network. - - Besides `connect`, only this method ever sends data. - """ - while self._user_connected and not self._reconnecting: - if self._pending_ack: - ack = RequestState(MsgsAck(list(self._pending_ack))) - self._send_queue.append(ack) - self._last_acks.append(ack) - self._pending_ack.clear() - - self._log.debug('Waiting for messages to send...') - # TODO Wait for the connection send queue to be empty? - # This means that while it's not empty we can wait for - # more messages to be added to the send queue. - batch, data = await self._send_queue.get() - - if not data: - continue - - self._log.debug('Encrypting %d message(s) in %d bytes for sending', - len(batch), len(data)) - - data = self._state.encrypt_message_data(data) - - # Whether sending succeeds or not, the popped requests are now - # pending because they're removed from the queue. If a reconnect - # occurs, they will be removed from pending state and re-enqueued - # so even if the network fails they won't be lost. If they were - # never re-enqueued, the future waiting for a response "locks". - for state in batch: - if not isinstance(state, list): - if isinstance(state.request, TLRequest): - self._pending_state[state.msg_id] = state - else: - for s in state: - if isinstance(s.request, TLRequest): - self._pending_state[s.msg_id] = s - - try: - await self._connection.send(data) - except IOError as e: - self._log.info('Connection closed while sending data') - self._start_reconnect(e) - return - - self._log.debug('Encrypted messages put in a queue to be sent') - - async def _recv_loop(self): - """ - This loop is responsible for reading all incoming responses - from the network, decrypting and handling or dispatching them. - - Besides `connect`, only this method ever receives data. - """ - while self._user_connected and not self._reconnecting: - self._log.debug('Receiving items from the network...') - try: - body = await self._connection.recv() - except asyncio.CancelledError: - raise # bypass except Exception - except IOError as e: - self._log.info('Connection closed while receiving data') - self._start_reconnect(e) - return - except InvalidBufferError as e: - if e.code == 429: - self._log.warning('Server indicated flood error at transport level: %s', e) - await self._disconnect(error=e) - else: - self._log.exception('Server sent invalid buffer') - self._start_reconnect(e) - return - except Exception as e: - self._log.exception('Unhandled error while receiving data') - self._start_reconnect(e) - return - - try: - message = self._state.decrypt_message_data(body) - if message is None: - continue # this message is to be ignored - except TypeNotFoundError as e: - # Received object which we don't know how to deserialize - self._log.info('Type %08x not found, remaining data %r', - e.invalid_constructor_id, e.remaining) - continue - except SecurityError as e: - # A step while decoding had the incorrect data. This message - # should not be considered safe and it should be ignored. - self._log.warning('Security error while unpacking a ' - 'received message: %s', e) - continue - except BufferError as e: - if isinstance(e, InvalidBufferError) and e.code == 404: - self._log.info('Server does not know about the current auth key; the session may need to be recreated') - await self._disconnect(error=AuthKeyNotFound()) - else: - self._log.warning('Invalid buffer %s', e) - self._start_reconnect(e) - return - except Exception as e: - self._log.exception('Unhandled error while decrypting data') - self._start_reconnect(e) - return - - try: - await self._process_message(message) - except Exception: - self._log.exception('Unhandled error while processing msgs') - - # Response Handlers - - async def _process_message(self, message): - """ - Adds the given message to the list of messages that must be - acknowledged and dispatches control to different ``_handle_*`` - method based on its type. - """ - self._pending_ack.add(message.msg_id) - handler = self._handlers.get(message.obj.CONSTRUCTOR_ID, - self._handle_update) - await handler(message) - - def _pop_states(self, msg_id): - """ - Pops the states known to match the given ID from pending messages. - - This method should be used when the response isn't specific. - """ - state = self._pending_state.pop(msg_id, None) - if state: - return [state] - - to_pop = [] - for state in self._pending_state.values(): - if state.container_id == msg_id: - to_pop.append(state.msg_id) - - if to_pop: - return [self._pending_state.pop(x) for x in to_pop] - - for ack in self._last_acks: - if ack.msg_id == msg_id: - return [ack] - - return [] - - async def _handle_rpc_result(self, message): - """ - Handles the result for Remote Procedure Calls: - - rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult; - - This is where the future results for sent requests are set. - """ - rpc_result = message.obj - state = self._pending_state.pop(rpc_result.req_msg_id, None) - self._log.debug('Handling RPC result for message %d', - rpc_result.req_msg_id) - - if not state: - # TODO We should not get responses to things we never sent - # However receiving a File() with empty bytes is "common". - # See #658, #759 and #958. They seem to happen in a container - # which contain the real response right after. - # - # But, it might also happen that we get an *error* for no parent request. - # If that's the case attempting to read from body which is None would fail with: - # "BufferError: No more data left to read (need 4, got 0: b''); last read None". - # This seems to be particularly common for "RpcError(error_code=-500, error_message='No workers running')". - if rpc_result.error: - self._log.info('Received error without parent request: %s', rpc_result.error) - else: - try: - with BinaryReader(rpc_result.body) as reader: - if not isinstance(reader.tgread_object(), upload.File): - raise ValueError('Not an upload.File') - except (TypeNotFoundError, ValueError): - self._log.info('Received response without parent request: %s', rpc_result.body) - return - - if rpc_result.error: - error = rpc_message_to_error(rpc_result.error, state.request) - self._send_queue.append( - RequestState(MsgsAck([state.msg_id]))) - - if not state.future.cancelled(): - state.future.set_exception(error) - else: - try: - with BinaryReader(rpc_result.body) as reader: - result = state.request.read_result(reader) - except Exception as e: - # e.g. TypeNotFoundError, should be propagated to caller - if not state.future.cancelled(): - state.future.set_exception(e) - else: - self._store_own_updates(result) - if not state.future.cancelled(): - state.future.set_result(result) - - async def _handle_container(self, message): - """ - Processes the inner messages of a container with many of them: - - msg_container#73f1f8dc messages:vector<%Message> = MessageContainer; - """ - self._log.debug('Handling container') - for inner_message in message.obj.messages: - await self._process_message(inner_message) - - async def _handle_gzip_packed(self, message): - """ - Unpacks the data from a gzipped object and processes it: - - gzip_packed#3072cfa1 packed_data:bytes = Object; - """ - self._log.debug('Handling gzipped data') - with BinaryReader(message.obj.data) as reader: - message.obj = reader.tgread_object() - await self._process_message(message) - - async def _handle_update(self, message): - try: - assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates') - except AssertionError: - self._log.warning( - 'Note: %s is not an update, not dispatching it %s', - message.obj.__class__.__name__, - message.obj - ) - return - - self._log.debug('Handling update %s', message.obj.__class__.__name__) - self._updates_queue.put_nowait(message.obj) - - def _store_own_updates(self, obj, *, _update_ids=frozenset(( - _tl.UpdateShortMessage.CONSTRUCTOR_ID, - _tl.UpdateShortChatMessage.CONSTRUCTOR_ID, - _tl.UpdateShort.CONSTRUCTOR_ID, - _tl.UpdatesCombined.CONSTRUCTOR_ID, - _tl.Updates.CONSTRUCTOR_ID, - _tl.UpdateShortSentMessage.CONSTRUCTOR_ID, - )), _update_like_ids=frozenset(( - _tl.messages.AffectedHistory.CONSTRUCTOR_ID, - _tl.messages.AffectedMessages.CONSTRUCTOR_ID, - _tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID, - ))): - try: - if obj.CONSTRUCTOR_ID in _update_ids: - obj._self_outgoing = True # flag to only process, but not dispatch these - self._updates_queue.put_nowait(obj) - elif obj.CONSTRUCTOR_ID in _update_like_ids: - # Ugly "hack" (?) - otherwise bots reliably detect gaps when deleting messages. - # - # Note: the `date` being `None` is used to check for `updatesTooLong`, so `0` is - # used instead. It is still not read, because `updateShort` has no `seq`. - # - # Some requests, such as `readHistory`, also return these types. But the `pts_count` - # seems to be zero, so while this will produce some bogus `updateDeleteMessages`, - # it's still one of the "cleaner" approaches to handling the new `pts`. - # `updateDeleteMessages` is probably the "least-invasive" update that can be used. - upd = _tl.UpdateShort(_tl.UpdateDeleteMessages([], obj.pts, obj.pts_count), 0) - upd._self_outgoing = True - self._updates_queue.put_nowait(upd) - except AttributeError: - pass - - async def _handle_pong(self, message): - """ - Handles pong results, which don't come inside a ``rpc_result`` - but are still sent through a request: - - pong#347773c5 msg_id:long ping_id:long = Pong; - """ - pong = message.obj - self._log.debug('Handling pong for message %d', pong.msg_id) - if self._ping == pong.ping_id: - self._ping = None - - state = self._pending_state.pop(pong.msg_id, None) - if state: - state.future.set_result(pong) - - async def _handle_bad_server_salt(self, message): - """ - Corrects the currently used server salt to use the right value - before enqueuing the rejected message to be re-sent: - - bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int - error_code:int new_server_salt:long = BadMsgNotification; - """ - bad_salt = message.obj - self._log.debug('Handling bad salt for message %d', bad_salt.bad_msg_id) - self._state.salt = bad_salt.new_server_salt - states = self._pop_states(bad_salt.bad_msg_id) - self._send_queue.extend(states) - - self._log.debug('%d message(s) will be resent', len(states)) - - async def _handle_bad_notification(self, message): - """ - Adjusts the current state to be correct based on the - received bad message notification whenever possible: - - bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int - error_code:int = BadMsgNotification; - """ - bad_msg = message.obj - states = self._pop_states(bad_msg.bad_msg_id) - - self._log.debug('Handling bad msg %s', bad_msg) - if bad_msg.error_code in (16, 17): - # Sent msg_id too low or too high (respectively). - # Use the current msg_id to determine the right time offset. - to = self._state.update_time_offset( - correct_msg_id=message.msg_id) - self._log.info('System clock is wrong, set time offset to %ds', to) - elif bad_msg.error_code == 32: - # msg_seqno too low, so just pump it up by some "large" amount - # TODO A better fix would be to start with a new fresh session ID - self._state._sequence += 64 - elif bad_msg.error_code == 33: - # msg_seqno too high never seems to happen but just in case - self._state._sequence -= 16 - else: - for state in states: - state.future.set_exception( - BadMessageError(state.request, bad_msg.error_code)) - return - - # Messages are to be re-sent once we've corrected the issue - self._send_queue.extend(states) - self._log.debug('%d messages will be resent due to bad msg', - len(states)) - - async def _handle_detailed_info(self, message): - """ - Updates the current status with the received detailed information: - - msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long - bytes:int status:int = MsgDetailedInfo; - """ - # TODO https://goo.gl/VvpCC6 - msg_id = message.obj.answer_msg_id - self._log.debug('Handling detailed info for message %d', msg_id) - self._pending_ack.add(msg_id) - - async def _handle_new_detailed_info(self, message): - """ - Updates the current status with the received detailed information: - - msg_new_detailed_info#809db6df answer_msg_id:long - bytes:int status:int = MsgDetailedInfo; - """ - # TODO https://goo.gl/G7DPsR - msg_id = message.obj.answer_msg_id - self._log.debug('Handling new detailed info for message %d', msg_id) - self._pending_ack.add(msg_id) - - async def _handle_new_session_created(self, message): - """ - Updates the current status with the received session information: - - new_session_created#9ec20908 first_msg_id:long unique_id:long - server_salt:long = NewSession; - """ - # TODO https://goo.gl/LMyN7A - self._log.debug('Handling new session created') - self._state.salt = message.obj.server_salt - - async def _handle_ack(self, message): - """ - Handles a server acknowledge about our messages. Normally - these can be ignored except in the case of ``auth.logOut``: - - auth.logOut#5717da40 = Bool; - - Telegram doesn't seem to send its result so we need to confirm - it manually. No other request is known to have this behaviour. - - Since the ID of sent messages consisting of a container is - never returned (unless on a bad notification), this method - also removes containers messages when any of their inner - messages are acknowledged. - """ - ack = message.obj - self._log.debug('Handling acknowledge for %s', str(ack.msg_ids)) - for msg_id in ack.msg_ids: - state = self._pending_state.get(msg_id) - if state and isinstance(state.request, LogOutRequest): - del self._pending_state[msg_id] - if not state.future.cancelled(): - state.future.set_result(True) - - async def _handle_future_salts(self, message): - """ - Handles future salt results, which don't come inside a - ``rpc_result`` but are still sent through a request: - - future_salts#ae500895 req_msg_id:long now:int - salts:vector = FutureSalts; - """ - # TODO save these salts and automatically adjust to the - # correct one whenever the salt in use expires. - self._log.debug('Handling future salts for message %d', message.msg_id) - state = self._pending_state.pop(message.msg_id, None) - if state: - state.future.set_result(message.obj) - - async def _handle_state_forgotten(self, message): - """ - Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by - enqueuing a :tl:`MsgsStateInfo` to be sent at a later point. - """ - self._send_queue.append(RequestState(MsgsStateInfo( - req_msg_id=message.msg_id, info=chr(1) * len(message.obj.msg_ids) - ))) - - async def _handle_msg_all(self, message): - """ - Handles :tl:`MsgsAllInfo` by doing nothing (yet). - """ - - async def _handle_destroy_session(self, message): - """ - Handles both :tl:`DestroySessionOk` and :tl:`DestroySessionNone`. - It behaves pretty much like handling an RPC result. - """ - for msg_id, state in self._pending_state.items(): - if isinstance(state.request, DestroySessionRequest)\ - and state.request.session_id == message.obj.session_id: - break - else: - return - - del self._pending_state[msg_id] - if not state.future.cancelled(): - state.future.set_result(message.obj) - - async def _handle_destroy_auth_key(self, message): - """ - Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`. - - :tl:`DestroyAuthKey` is not intended for users to use, but they still - might, and the response won't come in `rpc_result`, so thhat's worked - around here. - """ - self._log.debug('Handling destroy auth key %s', message.obj) - for msg_id, state in list(self._pending_state.items()): - if isinstance(state.request, DestroyAuthKeyRequest): - del self._pending_state[msg_id] - if not state.future.cancelled(): - state.future.set_result(message.obj) - - # If the auth key has been destroyed, that pretty much means the - # library can't continue as our auth key will no longer be found - # on the server. - # Even if the library didn't disconnect, the server would (and then - # the library would reconnect and learn about auth key being invalid). - if isinstance(message.obj, DestroyAuthKeyOk): - await self._disconnect(error=AuthKeyNotFound()) diff --git a/telethon/network/mtprotostate.py b/telethon/network/mtprotostate.py deleted file mode 100644 index 53b66306..00000000 --- a/telethon/network/mtprotostate.py +++ /dev/null @@ -1,279 +0,0 @@ -import os -import struct -import time -from hashlib import sha256 -from collections import deque - -from ..crypto import AES -from ..errors import SecurityError, InvalidBufferError -from ..extensions import BinaryReader -from ..tl.core import TLMessage -from ..tl.tlobject import TLRequest -from ..tl.functions import InvokeAfterMsgRequest -from ..tl.core.gzippacked import GzipPacked -from ..tl.types import BadServerSalt, BadMsgNotification - - -# N is not specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable -MAX_RECENT_MSG_IDS = 500 - -MSG_TOO_NEW_DELTA = 30 -MSG_TOO_OLD_DELTA = 300 - -# Something must be wrong if we ignore too many messages at the same time -MAX_CONSECUTIVE_IGNORED = 10 - - -class _OpaqueRequest(TLRequest): - """ - Wraps a serialized request into a type that can be serialized again. - """ - def __init__(self, data: bytes): - self.data = data - - def _bytes(self): - return self.data - - - -class MTProtoState: - """ - `telethon.network.mtprotosender.MTProtoSender` needs to hold a state - in order to be able to encrypt and decrypt incoming/outgoing messages, - as well as generating the message IDs. Instances of this class hold - together all the required information. - - It doesn't make sense to use `telethon.sessions.abstract.Session` for - the sender because the sender should *not* be concerned about storing - this information to disk, as one may create as many senders as they - desire to any other data center, or some CDN. Using the same session - for all these is not a good idea as each need their own authkey, and - the concept of "copying" sessions with the unnecessary entities or - updates state for these connections doesn't make sense. - - While it would be possible to have a `MTProtoPlainState` that does no - encryption so that it was usable through the `MTProtoLayer` and thus - avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more - focused to efficiency and this state is also more advanced (since it - supports gzipping and invoking after other message IDs). There are too - many methods that would be needed to make it convenient to use for the - authentication process, at which point the `MTProtoPlainSender` is better. - """ - def __init__(self, auth_key, loggers): - self.auth_key = auth_key - self._log = loggers[__name__] - self.time_offset = 0 - self.salt = 0 - - self.id = self._sequence = self._last_msg_id = None - self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS) - self._highest_remote_id = 0 - self._ignore_count = 0 - self.reset() - - def reset(self): - """ - Resets the state. - """ - # Session IDs can be random on every connection - self.id = struct.unpack('q', os.urandom(8))[0] - self._sequence = 0 - self._last_msg_id = 0 - self._recent_remote_ids.clear() - self._highest_remote_id = 0 - self._ignore_count = 0 - - def update_message_id(self, message): - """ - Updates the message ID to a new one, - used when the time offset changed. - """ - message.msg_id = self._get_new_msg_id() - - @staticmethod - def _calc_key(auth_key, msg_key, client): - """ - Calculate the key based on Telegram guidelines for MTProto 2, - specifying whether it's the client or not. See - https://core.telegram.org/mtproto/description#defining-aes-key-and-initialization-vector - """ - x = 0 if client else 8 - sha256a = sha256(msg_key + auth_key[x: x + 36]).digest() - sha256b = sha256(auth_key[x + 40:x + 76] + msg_key).digest() - - aes_key = sha256a[:8] + sha256b[8:24] + sha256a[24:32] - aes_iv = sha256b[:8] + sha256a[8:24] + sha256b[24:32] - - return aes_key, aes_iv - - def write_data_as_message(self, buffer, data, content_related, - *, after_id=None): - """ - Writes a message containing the given data into buffer. - - Returns the message id. - """ - msg_id = self._get_new_msg_id() - seq_no = self._get_seq_no(content_related) - if after_id is None: - body = GzipPacked.gzip_if_smaller(content_related, data) - else: - # The `RequestState` stores `bytes(request)`, not the request itself. - # `invokeAfterMsg` wants a `TLRequest` though, hence the wrapping. - body = GzipPacked.gzip_if_smaller(content_related, - bytes(InvokeAfterMsgRequest(after_id, _OpaqueRequest(data)))) - - buffer.write(struct.pack('> 32 - time_delta = now - remote_msg_time - - if time_delta > MSG_TOO_OLD_DELTA: - self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id) - self._count_ignored() - return None - - if -time_delta > MSG_TOO_NEW_DELTA: - self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id) - self._count_ignored() - return None - - self._recent_remote_ids.append(remote_msg_id) - self._highest_remote_id = remote_msg_id - self._ignore_count = 0 - - return TLMessage(remote_msg_id, remote_sequence, obj) - - def _count_ignored(self): - # It's possible that ignoring a message "bricks" the connection, - # but this should not happen unless there's something else wrong. - self._ignore_count += 1 - if self._ignore_count >= MAX_CONSECUTIVE_IGNORED: - raise SecurityError('Too many messages had to be ignored consecutively') - - def _get_new_msg_id(self): - """ - Generates a new unique message ID based on the current - time (in ms) since epoch, applying a known time offset. - """ - now = time.time() + self.time_offset - nanoseconds = int((now - int(now)) * 1e+9) - new_msg_id = (int(now) << 32) | (nanoseconds << 2) - - if self._last_msg_id >= new_msg_id: - new_msg_id = self._last_msg_id + 4 - - self._last_msg_id = new_msg_id - return new_msg_id - - def update_time_offset(self, correct_msg_id): - """ - Updates the time offset to the correct - one given a known valid message ID. - """ - bad = self._get_new_msg_id() - old = self.time_offset - - now = int(time.time()) - correct = correct_msg_id >> 32 - self.time_offset = correct - now - - if self.time_offset != old: - self._last_msg_id = 0 - self._log.debug( - 'Updated time offset (old offset %d, bad %d, good %d, new %d)', - old, bad, correct_msg_id, self.time_offset - ) - - return self.time_offset - - def _get_seq_no(self, content_related): - """ - Generates the next sequence number depending on whether - it should be for a content-related query or not. - """ - if content_related: - result = self._sequence * 2 + 1 - self._sequence += 1 - return result - else: - return self._sequence * 2 diff --git a/telethon/network/requeststate.py b/telethon/network/requeststate.py deleted file mode 100644 index 21b5efd9..00000000 --- a/telethon/network/requeststate.py +++ /dev/null @@ -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, after=None): - self.container_id = None - self.msg_id = None - self.request = request - self.data = bytes(request) - self.future = asyncio.Future() - self.after = after diff --git a/telethon/password.py b/telethon/password.py deleted file mode 100644 index 0f950254..00000000 --- a/telethon/password.py +++ /dev/null @@ -1,194 +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 - while True: - random = os.urandom(random_size) - a = int.from_bytes(random, 'big') - A = pow(g, a, p) - if is_good_mod_exp_first(A, p): - a_for_hash = big_num_for_hash(A) - u = int.from_bytes(sha256(a_for_hash, b_for_hash), 'big') - if u > 0: - return (a, a_for_hash, u) - - a, a_for_hash, u = generate_and_check_random() - g_b = (B - kg_x) % p - if not is_good_mod_exp_first(g_b, p): - raise ValueError('bad g_b') - - ux = u * x - a_ux = a + ux - S = pow(g_b, a_ux, p) - K = sha256(big_num_for_hash(S)) - M1 = sha256( - xor(sha256(p_for_hash), sha256(g_for_hash)), - sha256(algo.salt1), - sha256(algo.salt2), - a_for_hash, - b_for_hash, - K - ) - - return types.InputCheckPasswordSRP( - request.srp_id, bytes(a_for_hash), bytes(M1)) diff --git a/telethon/requestiter.py b/telethon/requestiter.py deleted file mode 100644 index fd28419d..00000000 --- a/telethon/requestiter.py +++ /dev/null @@ -1,134 +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) - ) - 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 deleted file mode 100644 index d10c76e6..00000000 --- a/telethon/sessions/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index 11cc63a1..00000000 --- a/telethon/sessions/abstract.py +++ /dev/null @@ -1,172 +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 get_update_states(self): - """ - Returns an iterable over all known pairs of ``(entity ID, update state)``. - """ - - def close(self): - """ - Called on client disconnection. Should be used to - free any used resources. Can be left empty if none. - """ - - @abstractmethod - def save(self): - """ - Called whenever important properties change. It should - make persist the relevant session information to disk. - """ - raise NotImplementedError - - @abstractmethod - def delete(self): - """ - Called upon client.log_out(). Should delete the stored - information from disk since it's not valid anymore. - """ - raise NotImplementedError - - @classmethod - def list_sessions(cls): - """ - Lists available sessions. Not used by the library itself. - """ - return [] - - @abstractmethod - def process_entities(self, tlo): - """ - Processes the input ``TLObject`` or ``list`` and saves - whatever information is relevant (e.g., ID or access hash). - """ - raise NotImplementedError - - @abstractmethod - def get_input_entity(self, key): - """ - Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``). - The library uses this method whenever an ``InputPeer`` is needed - to suit several purposes (e.g. user only provided its ID or wishes - to use a cached username to avoid extra RPC). - """ - raise NotImplementedError - - @abstractmethod - def cache_file(self, md5_digest, file_size, instance): - """ - Caches the given file information persistently, so that it - doesn't need to be re-uploaded in case the file is used again. - - The ``instance`` will be either an ``InputPhoto`` or ``InputDocument``, - both with an ``.id`` and ``.access_hash`` attributes. - """ - raise NotImplementedError - - @abstractmethod - def get_file(self, md5_digest, file_size, cls): - """ - Returns an instance of ``cls`` if the ``md5_digest`` and ``file_size`` - match an existing saved record. The class will either be an - ``InputPhoto`` or ``InputDocument``, both with two parameters - ``id`` and ``access_hash`` in that order. - """ - raise NotImplementedError diff --git a/telethon/sessions/memory.py b/telethon/sessions/memory.py deleted file mode 100644 index 721b6d9a..00000000 --- a/telethon/sessions/memory.py +++ /dev/null @@ -1,251 +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 get_update_states(self): - return self._update_states.items() - - def close(self): - pass - - def save(self): - pass - - def delete(self): - pass - - @staticmethod - def _entity_values_to_row(id, hash, username, phone, name): - # While this is a simple implementation it might be overrode by, - # other classes so they don't need to implement the plural form - # of the method. Don't remove. - return id, hash, username, phone, name - - def _entity_to_row(self, e): - if not isinstance(e, TLObject): - return - try: - p = utils.get_input_peer(e, allow_self=False) - marked_id = utils.get_peer_id(p) - except TypeError: - # Note: `get_input_peer` already checks for non-zero `access_hash`. - # See issues #354 and #392. It also checks that the entity - # is not `min`, because its `access_hash` cannot be used - # anywhere (since layer 102, there are two access hashes). - return - - if isinstance(p, (InputPeerUser, InputPeerChannel)): - p_hash = p.access_hash - elif isinstance(p, InputPeerChat): - p_hash = 0 - else: - return - - username = getattr(e, 'username', None) or None - if username is not None: - username = username.lower() - phone = getattr(e, 'phone', None) - name = utils.get_display_name(e) or None - return self._entity_values_to_row( - marked_id, p_hash, username, phone, name - ) - - def _entities_to_rows(self, tlo): - if not isinstance(tlo, TLObject) and utils.is_list_like(tlo): - # This may be a list of users already for instance - entities = tlo - else: - entities = [] - if hasattr(tlo, 'user'): - entities.append(tlo.user) - if hasattr(tlo, 'chat'): - entities.append(tlo.chat) - if hasattr(tlo, 'chats') and utils.is_list_like(tlo.chats): - entities.extend(tlo.chats) - if hasattr(tlo, 'users') and utils.is_list_like(tlo.users): - entities.extend(tlo.users) - - rows = [] # Rows to add (id, hash, username, phone, name) - for e in entities: - row = self._entity_to_row(e) - if row: - rows.append(row) - return rows - - def process_entities(self, tlo): - self._entities |= set(self._entities_to_rows(tlo)) - - def get_entity_rows_by_phone(self, phone): - try: - return next((id, hash) for id, hash, _, found_phone, _ - in self._entities if found_phone == phone) - except StopIteration: - pass - - def get_entity_rows_by_username(self, username): - try: - return next((id, hash) for id, hash, found_username, _, _ - in self._entities if found_username == username) - except StopIteration: - pass - - def get_entity_rows_by_name(self, name): - try: - return next((id, hash) for id, hash, _, _, found_name - in self._entities if found_name == name) - except StopIteration: - pass - - def get_entity_rows_by_id(self, id, exact=True): - try: - if exact: - return next((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 deleted file mode 100644 index 63962502..00000000 --- a/telethon/sessions/sqlite.py +++ /dev/null @@ -1,368 +0,0 @@ -import datetime -import os -import time - -from ..tl import types -from .memory import MemorySession, _SentFileType -from .. import utils -from ..crypto import AuthKey -from ..tl.types import ( - InputPhoto, InputDocument, PeerUser, PeerChat, PeerChannel -) - -try: - import sqlite3 - sqlite3_err = None -except ImportError as e: - sqlite3 = None - sqlite3_err = type(e) - -EXTENSION = '.session' -CURRENT_VERSION = 7 # database version - - -class SQLiteSession(MemorySession): - """This session contains the required information to login into your - Telegram account. NEVER give the saved session file to anyone, since - they would gain instant access to all your messages and contacts. - - If you think the session has been compromised, close all the sessions - through an official Telegram client to revoke the authorization. - """ - - def __init__(self, session_id=None): - if sqlite3 is None: - raise sqlite3_err - - super().__init__() - self.filename = ':memory:' - self.save_entities = True - - if session_id: - self.filename = session_id - if not self.filename.endswith(EXTENSION): - self.filename += EXTENSION - - self._conn = None - c = self._cursor() - c.execute("select name from sqlite_master " - "where type='table' and name='version'") - if c.fetchone(): - # Tables already exist, check for the version - c.execute("select version from version") - version = c.fetchone()[0] - if version < CURRENT_VERSION: - self._upgrade_database(old=version) - c.execute("delete from version") - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self.save() - - # These values will be saved - c.execute('select * from sessions') - tuple_ = c.fetchone() - if tuple_: - self._dc_id, self._server_address, self._port, key, \ - self._takeout_id = tuple_ - self._auth_key = AuthKey(data=key) - - c.close() - else: - # Tables don't exist, create new ones - self._create_table( - c, - "version (version integer primary key)" - , - """sessions ( - dc_id integer primary key, - server_address text, - port integer, - auth_key blob, - takeout_id integer - )""" - , - """entities ( - id integer primary key, - hash integer not null, - username text, - phone integer, - name text, - date integer - )""" - , - """sent_files ( - md5_digest blob, - file_size integer, - type integer, - id integer, - hash integer, - primary key(md5_digest, file_size, type) - )""" - , - """update_state ( - id integer primary key, - pts integer, - qts integer, - date integer, - seq integer - )""" - ) - c.execute("insert into version values (?)", (CURRENT_VERSION,)) - self._update_session_table() - c.close() - self.save() - - def clone(self, to_instance=None): - cloned = super().clone(to_instance) - cloned.save_entities = self.save_entities - return cloned - - def _upgrade_database(self, old): - c = self._cursor() - if old == 1: - old += 1 - # old == 1 doesn't have the old sent_files so no need to drop - if old == 2: - old += 1 - # Old cache from old sent_files lasts then a day anyway, drop - c.execute('drop table sent_files') - self._create_table(c, """sent_files ( - md5_digest blob, - file_size integer, - type integer, - id integer, - hash integer, - primary key(md5_digest, file_size, type) - )""") - if old == 3: - old += 1 - self._create_table(c, """update_state ( - id integer primary key, - pts integer, - qts integer, - date integer, - seq integer - )""") - if old == 4: - old += 1 - c.execute("alter table sessions add column takeout_id integer") - if old == 5: - # Not really any schema upgrade, but potentially all access - # hashes for User and Channel are wrong, so drop them off. - old += 1 - c.execute('delete from entities') - if old == 6: - old += 1 - c.execute("alter table entities add column date integer") - - c.close() - - @staticmethod - def _create_table(c, *definitions): - for definition in definitions: - c.execute('create table {}'.format(definition)) - - # Data from sessions should be kept as properties - # not to fetch the database every time we need it - def set_dc(self, dc_id, server_address, port): - super().set_dc(dc_id, server_address, port) - self._update_session_table() - - # Fetch the auth_key corresponding to this data center - row = self._execute('select auth_key from sessions') - if row and row[0]: - self._auth_key = AuthKey(data=row[0]) - else: - self._auth_key = None - - @MemorySession.auth_key.setter - def auth_key(self, value): - self._auth_key = value - self._update_session_table() - - @MemorySession.takeout_id.setter - def takeout_id(self, value): - self._takeout_id = value - self._update_session_table() - - def _update_session_table(self): - c = self._cursor() - # While we can save multiple rows into the sessions table - # currently we only want to keep ONE as the tables don't - # tell us which auth_key's are usable and will work. Needs - # some more work before being able to save auth_key's for - # multiple DCs. Probably done differently. - c.execute('delete from sessions') - c.execute('insert or replace into sessions values (?,?,?,?,?)', ( - self._dc_id, - self._server_address, - self._port, - self._auth_key.key if self._auth_key else b'', - self._takeout_id - )) - c.close() - - def get_update_state(self, entity_id): - row = self._execute('select pts, qts, date, seq from update_state ' - 'where id = ?', entity_id) - if row: - pts, qts, date, seq = row - date = datetime.datetime.fromtimestamp( - date, tz=datetime.timezone.utc) - return types.updates.State(pts, qts, date, seq, unread_count=0) - - def set_update_state(self, entity_id, state): - self._execute('insert or replace into update_state values (?,?,?,?,?)', - entity_id, state.pts, state.qts, - state.date.timestamp(), state.seq) - - def get_update_states(self): - c = self._cursor() - try: - rows = c.execute('select id, pts, qts, date, seq from update_state').fetchall() - return ((row[0], types.updates.State( - pts=row[1], - qts=row[2], - date=datetime.datetime.fromtimestamp(row[3], tz=datetime.timezone.utc), - seq=row[4], - unread_count=0) - ) for row in rows) - finally: - c.close() - - def save(self): - """Saves the current session object as session_user_id.session""" - # This is a no-op if there are no changes to commit, so there's - # no need for us to keep track of an "unsaved changes" variable. - if self._conn is not None: - self._conn.commit() - - def _cursor(self): - """Asserts that the connection is open and returns a cursor""" - if self._conn is None: - self._conn = sqlite3.connect(self.filename, - check_same_thread=False) - return self._conn.cursor() - - def _execute(self, stmt, *values): - """ - Gets a cursor, executes `stmt` and closes the cursor, - fetching one row afterwards and returning its result. - """ - c = self._cursor() - try: - return c.execute(stmt, values).fetchone() - finally: - c.close() - - def close(self): - """Closes the connection unless we're working in-memory""" - if self.filename != ':memory:': - if self._conn is not None: - self._conn.commit() - self._conn.close() - self._conn = None - - def delete(self): - """Deletes the current session file""" - if self.filename == ':memory:': - return True - try: - os.remove(self.filename) - return True - except OSError: - return False - - @classmethod - def list_sessions(cls): - """Lists all the sessions of the users who have ever connected - using this client and never logged out - """ - return [os.path.splitext(os.path.basename(f))[0] - for f in os.listdir('.') if f.endswith(EXTENSION)] - - # Entity processing - - def process_entities(self, tlo): - """ - Processes all the found entities on the given TLObject, - unless .save_entities is False. - """ - if not self.save_entities: - return - - rows = self._entities_to_rows(tlo) - if not rows: - return - - c = self._cursor() - try: - now_tup = (int(time.time()),) - rows = [row + now_tup for row in rows] - c.executemany( - 'insert or replace into entities values (?,?,?,?,?,?)', rows) - finally: - c.close() - - def get_entity_rows_by_phone(self, phone): - return self._execute( - 'select id, hash from entities where phone = ?', phone) - - def get_entity_rows_by_username(self, username): - c = self._cursor() - try: - results = c.execute( - 'select id, hash, date from entities where username = ?', - (username,) - ).fetchall() - - if not results: - return None - - # If there is more than one result for the same username, evict the oldest one - if len(results) > 1: - results.sort(key=lambda t: t[2] or 0) - c.executemany('update entities set username = null where id = ?', - [(t[0],) for t in results[:-1]]) - - return results[-1][0], results[-1][1] - finally: - c.close() - - def get_entity_rows_by_name(self, name): - return self._execute( - 'select id, hash from entities where name = ?', name) - - def get_entity_rows_by_id(self, id, exact=True): - if exact: - return self._execute( - 'select id, hash from entities where id = ?', id) - else: - return self._execute( - 'select id, hash from entities where id in (?,?,?)', - utils.get_peer_id(PeerUser(id)), - utils.get_peer_id(PeerChat(id)), - utils.get_peer_id(PeerChannel(id)) - ) - - # File processing - - def get_file(self, md5_digest, file_size, cls): - row = self._execute( - 'select id, hash from sent_files ' - 'where md5_digest = ? and file_size = ? and type = ?', - md5_digest, file_size, _SentFileType.from_type(cls).value - ) - if row: - # Both allowed classes have (id, access_hash) as parameters - return cls(row[0], row[1]) - - def cache_file(self, md5_digest, file_size, instance): - if not isinstance(instance, (InputDocument, InputPhoto)): - raise TypeError('Cannot cache %s instance' % type(instance)) - - self._execute( - 'insert or replace into sent_files values (?,?,?,?,?)', - md5_digest, file_size, - _SentFileType.from_type(type(instance)).value, - instance.id, instance.access_hash - ) diff --git a/telethon/sessions/string.py b/telethon/sessions/string.py deleted file mode 100644 index fb971d82..00000000 --- a/telethon/sessions/string.py +++ /dev/null @@ -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/sync.py b/telethon/sync.py deleted file mode 100644 index f647670a..00000000 --- a/telethon/sync.py +++ /dev/null @@ -1,74 +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 events, errors, utils, connection, helpers -from .client.account import _TakeoutClient -from .client.telegramclient import TelegramClient -from .tl import types, functions, custom -from .tl.custom import ( - Draft, Dialog, MessageButton, Forward, Button, - Message, InlineResult, Conversation -) -from .tl.custom.chatgetter import ChatGetter -from .tl.custom.sendergetter import SenderGetter - - -def _syncify_wrap(t, method_name): - method = getattr(t, method_name) - - @functools.wraps(method) - def syncified(*args, **kwargs): - coro = method(*args, **kwargs) - loop = helpers.get_running_loop() - if loop.is_running(): - return coro - else: - return loop.run_until_complete(coro) - - # Save an accessible reference to the original method - setattr(syncified, '__tl.sync', method) - setattr(t, method_name, syncified) - - -def syncify(*types): - """ - Converts all the methods in the given types (class definitions) - into synchronous, which return either the coroutine or the result - based on whether ``asyncio's`` event loop is running. - """ - # Our asynchronous generators all are `RequestIter`, which already - # provide a synchronous iterator variant, so we don't need to worry - # about asyncgenfunction's here. - for t in types: - for name in dir(t): - if not name.startswith('_') or name == '__call__': - if inspect.iscoroutinefunction(getattr(t, name)): - _syncify_wrap(t, name) - - -syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, - ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) - - -# Private special case, since a conversation's methods return -# futures (but the public function themselves are synchronous). -_syncify_wrap(Conversation, '_get_result') - -__all__ = [ - 'TelegramClient', 'Button', - 'types', 'functions', 'custom', 'errors', - 'events', 'utils', 'connection' -] diff --git a/telethon/tl/__init__.py b/telethon/tl/__init__.py deleted file mode 100644 index e187537f..00000000 --- a/telethon/tl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .tlobject import TLObject, TLRequest diff --git a/telethon/tl/core/__init__.py b/telethon/tl/core/__init__.py deleted file mode 100644 index 3113196a..00000000 --- a/telethon/tl/core/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index fb4094e4..00000000 --- a/telethon/tl/core/gzippacked.py +++ /dev/null @@ -1,45 +0,0 @@ -import gzip -import struct - -from .. import TLObject - - -class GzipPacked(TLObject): - CONSTRUCTOR_ID = 0x3072cfa1 - - def __init__(self, data): - self.data = data - - @staticmethod - def gzip_if_smaller(content_related, data): - """Calls bytes(request), and based on a certain threshold, - optionally gzips the resulting data. If the gzipped data is - smaller than the original byte array, this is returned instead. - - Note that this only applies to content related requests. - """ - if content_related and len(data) > 512: - gzipped = bytes(GzipPacked(data)) - return gzipped if len(gzipped) < len(data) else data - else: - return data - - def __bytes__(self): - return struct.pack('`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionEditMessage) - - @property - def deleted_message(self): - """ - Whether a message in this channel was deleted or not. - - If `True`, `old` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionDeleteMessage) - - @property - def changed_admin(self): - """ - Whether the permissions for an admin in this channel - changed or not. - - If `True`, `old` and `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance( - self.original.action, - types.ChannelAdminLogEventActionParticipantToggleAdmin) - - @property - def changed_restrictions(self): - """ - Whether a message in this channel was edited or not. - - If `True`, `old` and `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance( - self.original.action, - types.ChannelAdminLogEventActionParticipantToggleBan) - - @property - def changed_invites(self): - """ - Whether the invites in the channel were toggled or not. - - If `True`, `old` and `new` will be present as `bool`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleInvites) - - @property - def changed_location(self): - """ - Whether the location setting of the channel has changed or not. - - If `True`, `old` and `new` will be present as :tl:`ChannelLocation`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeLocation) - - @property - def joined(self): - """ - Whether `user` joined through the channel's - public username or not. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantJoin) - - @property - def joined_invite(self): - """ - Whether a new user joined through an invite - link to the channel or not. - - If `True`, `new` will be present as - :tl:`ChannelParticipant`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantInvite) - - @property - def left(self): - """ - Whether `user` left the channel or not. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantLeave) - - @property - def changed_hide_history(self): - """ - Whether hiding the previous message history for new members - in the channel was toggled or not. - - If `True`, `old` and `new` will be present as `bool`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionTogglePreHistoryHidden) - - @property - def changed_signatures(self): - """ - Whether the message signatures in the channel were toggled - or not. - - If `True`, `old` and `new` will be present as `bool`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleSignatures) - - @property - def changed_pin(self): - """ - Whether a new message in this channel was pinned or not. - - If `True`, `new` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionUpdatePinned) - - @property - def changed_default_banned_rights(self): - """ - Whether the default banned rights were changed or not. - - If `True`, `old` and `new` will - be present as :tl:`ChatBannedRights`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionDefaultBannedRights) - - @property - def stopped_poll(self): - """ - Whether a poll was stopped or not. - - If `True`, `new` will be present as - `Message `. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionStopPoll) - - @property - def started_group_call(self): - """ - Whether a group call was started or not. - - If `True`, `new` will be present as :tl:`InputGroupCall`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionStartGroupCall) - - @property - def discarded_group_call(self): - """ - Whether a group call was started or not. - - If `True`, `old` will be present as :tl:`InputGroupCall`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionDiscardGroupCall) - - @property - def user_muted(self): - """ - Whether a participant was muted in the ongoing group call or not. - - If `True`, `new` will be present as :tl:`GroupCallParticipant`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantMute) - - @property - def user_unmutted(self): - """ - Whether a participant was unmuted from the ongoing group call or not. - - If `True`, `new` will be present as :tl:`GroupCallParticipant`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantUnmute) - - @property - def changed_call_settings(self): - """ - Whether the group call settings were changed or not. - - If `True`, `new` will be `True` if new users are muted on join. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionToggleGroupCallSetting) - - @property - def changed_history_ttl(self): - """ - Whether the Time To Live of the message history has changed. - - Messages sent after this change will have a ``ttl_period`` in seconds - indicating how long they should live for before being auto-deleted. - - If `True`, `old` will be the old TTL, and `new` the new TTL, in seconds. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionChangeHistoryTTL) - - @property - def deleted_exported_invite(self): - """ - Whether the exported chat invite has been deleted. - - If `True`, `old` will be the deleted :tl:`ExportedChatInvite`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteDelete) - - @property - def edited_exported_invite(self): - """ - Whether the exported chat invite has been edited. - - If `True`, `old` and `new` will be the old and new - :tl:`ExportedChatInvite`, respectively. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteEdit) - - @property - def revoked_exported_invite(self): - """ - Whether the exported chat invite has been revoked. - - If `True`, `old` will be the revoked :tl:`ExportedChatInvite`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionExportedInviteRevoke) - - @property - def joined_by_invite(self): - """ - Whether a new participant has joined with the use of an invite link. - - If `True`, `old` will be pre-existing (old) :tl:`ExportedChatInvite` - used to join. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantJoinByInvite) - - @property - def changed_user_volume(self): - """ - Whether a participant's volume in a call has been changed. - - If `True`, `new` will be the updated :tl:`GroupCallParticipant`. - """ - return isinstance(self.original.action, - types.ChannelAdminLogEventActionParticipantVolume) - - def __str__(self): - return str(self.original) - - def stringify(self): - return self.original.stringify() diff --git a/telethon/tl/custom/button.py b/telethon/tl/custom/button.py deleted file mode 100644 index 0ed9a43d..00000000 --- a/telethon/tl/custom/button.py +++ /dev/null @@ -1,308 +0,0 @@ -from .. import types -from ... import utils - - -class Button: - """ - .. note:: - - This class is used to **define** reply markups, e.g. when - sending a message or replying to events. When you access - `Message.buttons ` - they are actually `MessageButton - `, - so you might want to refer to that class instead. - - Helper class to allow defining ``reply_markup`` when - sending a message with inline or keyboard buttons. - - You should make use of the defined class methods to create button - instances instead making them yourself (i.e. don't do ``Button(...)`` - but instead use methods line `Button.inline(...) ` etc. - - You can use `inline`, `switch_inline`, `url`, `auth`, `buy` and `game` - together to create inline buttons (under the message). - - You can use `text`, `request_location`, `request_phone` and `request_poll` - together to create a reply markup (replaces the user keyboard). - You can also configure the aspect of the reply with these. - The latest message with a reply markup will be the one shown to the user - (messages contain the buttons, not the chat itself). - - You **cannot** mix the two type of buttons together, - and it will error if you try to do so. - - The text for all buttons may be at most 142 characters. - If more characters are given, Telegram will cut the text - to 128 characters and add the ellipsis (…) character as - the 129. - """ - def __init__(self, button, *, resize, single_use, selective): - self.button = button - self.resize = resize - self.single_use = single_use - self.selective = selective - - @staticmethod - def _is_inline(button): - """ - Returns `True` if the button belongs to an inline keyboard. - """ - return isinstance(button, ( - types.KeyboardButtonBuy, - types.KeyboardButtonCallback, - types.KeyboardButtonGame, - types.KeyboardButtonSwitchInline, - types.KeyboardButtonUrl, - types.InputKeyboardButtonUrlAuth - )) - - @staticmethod - def inline(text, data=None): - """ - Creates a new inline button with some payload data in it. - - If `data` is omitted, the given `text` will be used as `data`. - In any case `data` should be either `bytes` or `str`. - - Note that the given `data` must be less or equal to 64 bytes. - If more than 64 bytes are passed as data, ``ValueError`` is raised. - If you need to store more than 64 bytes, consider saving the real - data in a database and a reference to that data inside the button. - - When the user clicks this button, `events.CallbackQuery - ` will trigger with the - same data that the button contained, so that you can determine which - button was pressed. - """ - if not data: - data = text.encode('utf-8') - elif not isinstance(data, (bytes, bytearray, memoryview)): - data = str(data).encode('utf-8') - - if len(data) > 64: - raise ValueError('Too many bytes for the data') - - return types.KeyboardButtonCallback(text, data) - - @staticmethod - def switch_inline(text, query='', same_peer=False): - """ - Creates a new inline button to switch to inline query. - - If `query` is given, it will be the default text to be used - when making the inline query. - - If ``same_peer is True`` the inline query will directly be - set under the currently opened chat. Otherwise, the user will - have to select a different dialog to make the query. - - When the user clicks this button, after a chat is selected, their - input field will be filled with the username of your bot followed - by the query text, ready to make inline queries. - """ - return types.KeyboardButtonSwitchInline(text, query, same_peer) - - @staticmethod - def url(text, url=None): - """ - Creates a new inline button to open the desired URL on click. - - If no `url` is given, the `text` will be used as said URL instead. - - You cannot detect that the user clicked this button directly. - - When the user clicks this button, a confirmation box will be shown - to the user asking whether they want to open the displayed URL unless - the domain is trusted, and once confirmed the URL will open in their - device. - """ - return types.KeyboardButtonUrl(text, url or text) - - @staticmethod - def auth(text, url=None, *, bot=None, write_access=False, fwd_text=None): - """ - Creates a new inline button to authorize the user at the given URL. - - You should set the `url` to be on the same domain as the one configured - for the desired `bot` via `@BotFather `_ using - the ``/setdomain`` command. - - For more information about letting the user login via Telegram to - a certain domain, see https://core.telegram.org/widgets/login. - - If no `url` is specified, it will default to `text`. - - Args: - bot (`hints.EntityLike`): - The bot that requires this authorization. By default, this - is the bot that is currently logged in (itself), although - you may pass a different input peer. - - .. note:: - - For now, you cannot use ID or username for this argument. - If you want to use a different bot than the one currently - logged in, you must manually use `client.get_input_entity() - `. - - write_access (`bool`): - Whether write access is required or not. - This is `False` by default (read-only access). - - fwd_text (`str`): - The new text to show in the button if the message is - forwarded. By default, the button text will be the same. - - When the user clicks this button, a confirmation box will be shown - to the user asking whether they want to login to the specified domain. - """ - return types.InputKeyboardButtonUrlAuth( - text=text, - url=url or text, - bot=utils.get_input_user(bot or types.InputUserSelf()), - request_write_access=write_access, - fwd_text=fwd_text - ) - - @classmethod - def text(cls, text, *, resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button with the given text. - - Args: - resize (`bool`): - If present, the entire keyboard will be reconfigured to - be resized and be smaller if there are not many buttons. - - single_use (`bool`): - If present, the entire keyboard will be reconfigured to - be usable only once before it hides itself. - - selective (`bool`): - If present, the entire keyboard will be reconfigured to - be "selective". The keyboard will be shown only to specific - users. It will target users that are @mentioned in the text - of the message or to the sender of the message you reply to. - - When the user clicks this button, a text message with the same text - as the button will be sent, and can be handled with `events.NewMessage - `. You cannot distinguish - between a button press and the user typing and sending exactly the - same text on their own. - """ - return cls(types.KeyboardButton(text), - resize=resize, single_use=single_use, selective=selective) - - @classmethod - def request_location(cls, text, *, - resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button to request the user's location on click. - - ``resize``, ``single_use`` and ``selective`` are documented in `text`. - - When the user clicks this button, a confirmation box will be shown - to the user asking whether they want to share their location with the - bot, and if confirmed a message with geo media will be sent. - """ - return cls(types.KeyboardButtonRequestGeoLocation(text), - resize=resize, single_use=single_use, selective=selective) - - @classmethod - def request_phone(cls, text, *, - resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button to request the user's phone on click. - - ``resize``, ``single_use`` and ``selective`` are documented in `text`. - - When the user clicks this button, a confirmation box will be shown - to the user asking whether they want to share their phone with the - bot, and if confirmed a message with contact media will be sent. - """ - return cls(types.KeyboardButtonRequestPhone(text), - resize=resize, single_use=single_use, selective=selective) - - @classmethod - def request_poll(cls, text, *, force_quiz=False, - resize=None, single_use=None, selective=None): - """ - Creates a new keyboard button to request the user to create a poll. - - If `force_quiz` is `False`, the user will be allowed to choose whether - they want their poll to be a quiz or not. Otherwise, the user will be - forced to create a quiz when creating the poll. - - If a poll is a quiz, there will be only one answer that is valid, and - the votes cannot be retracted. Otherwise, users can vote and retract - the vote, and the pol might be multiple choice. - - ``resize``, ``single_use`` and ``selective`` are documented in `text`. - - When the user clicks this button, a screen letting the user create a - poll will be shown, and if they do create one, the poll will be sent. - """ - return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz), - resize=resize, single_use=single_use, selective=selective) - - @staticmethod - def clear(selective=None): - """ - Clears all keyboard buttons after sending a message with this markup. - When used, no other button should be present or it will be ignored. - - ``selective`` is as documented in `text`. - - """ - return types.ReplyKeyboardHide(selective=selective) - - @staticmethod - def force_reply(single_use=None, selective=None, placeholder=None): - """ - Forces a reply to the message with this markup. If used, - no other button should be present or it will be ignored. - - ``single_use`` and ``selective`` are as documented in `text`. - - Args: - placeholder (str): - text to show the user at typing place of message. - - If the placeholder is too long, Telegram applications will - crop the text (for example, to 64 characters and adding an - ellipsis (…) character as the 65th). - """ - return types.ReplyKeyboardForceReply( - single_use=single_use, - selective=selective, - placeholder=placeholder) - - @staticmethod - def buy(text): - """ - Creates a new inline button to buy a product. - - This can only be used when sending files of type - :tl:`InputMediaInvoice`, and must be the first button. - - If the button is not specified, Telegram will automatically - add the button to the message. See the - `Payments API `__ - documentation for more information. - """ - return types.KeyboardButtonBuy(text) - - @staticmethod - def game(text): - """ - Creates a new inline button to start playing a game. - - This should be used when sending files of type - :tl:`InputMediaGame`, and must be the first button. - - See the - `Games `__ - documentation for more information on using games. - """ - return types.KeyboardButtonGame(text) diff --git a/telethon/tl/custom/chatgetter.py b/telethon/tl/custom/chatgetter.py deleted file mode 100644 index 240d9b83..00000000 --- a/telethon/tl/custom/chatgetter.py +++ /dev/null @@ -1,150 +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._mb_entity_cache.get( - utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer() - except AttributeError: - pass - - return self._input_chat - - async def get_input_chat(self): - """ - Returns `input_chat`, but will make an API call to find the - input chat unless it's already cached. - """ - if self.input_chat is None and self.chat_id and self._client: - try: - # The chat may be recent, look in dialogs - target = self.chat_id - async for d in self._client.iter_dialogs(100): - if d.id == target: - self._chat = d.entity - self._input_chat = d.input_entity - break - except errors.RPCError: - pass - - return self._input_chat - - @property - def chat_id(self): - """ - Returns the marked chat integer ID. Note that this value **will - be different** from ``peer_id`` for incoming private messages, since - the chat *to* which the messages go is to your own person, but - the *chat* itself is with the one who sent the message. - - TL;DR; this gets the ID that you expect. - - If there is a chat in the object, `chat_id` will *always* be set, - which is why you should use it instead of `chat.id `. - """ - return utils.get_peer_id(self._chat_peer) if self._chat_peer else None - - @property - def is_private(self): - """ - `True` if the message was sent as a private message. - - Returns `None` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - return isinstance(self._chat_peer, types.PeerUser) if self._chat_peer else None - - @property - def is_group(self): - """ - True if the message was sent on a group or megagroup. - - Returns `None` if there isn't enough information - (e.g. on `events.MessageDeleted `). - """ - # TODO Cache could tell us more in the future - if self._broadcast is None and hasattr(self.chat, 'broadcast'): - self._broadcast = bool(self.chat.broadcast) - - if isinstance(self._chat_peer, types.PeerChannel): - if self._broadcast is None: - return None - else: - return not self._broadcast - - return isinstance(self._chat_peer, types.PeerChat) - - @property - def is_channel(self): - """`True` if the message was sent on a megagroup or channel.""" - # The only case where chat peer could be none is in MessageDeleted, - # however those always have the peer in channels. - return isinstance(self._chat_peer, types.PeerChannel) - - async def _refetch_chat(self): - """ - Re-fetches chat information through other means. - """ diff --git a/telethon/tl/custom/conversation.py b/telethon/tl/custom/conversation.py deleted file mode 100644 index 6cb973d4..00000000 --- a/telethon/tl/custom/conversation.py +++ /dev/null @@ -1,529 +0,0 @@ -import asyncio -import functools -import inspect -import itertools -import time - -from .chatgetter import ChatGetter -from ... import helpers, utils, errors - -# Sometimes the edits arrive very fast (within the same second). -# In that case we add a small delta so that the age is older, for -# comparision purposes. This value is enough for up to 1000 messages. -_EDIT_COLLISION_DELTA = 0.001 - - -def _checks_cancelled(f): - @functools.wraps(f) - def wrapper(self, *args, **kwargs): - if self._cancelled: - raise asyncio.CancelledError('The conversation was cancelled before') - - return f(self, *args, **kwargs) - return wrapper - - -class Conversation(ChatGetter): - """ - Represents a conversation inside an specific chat. - - A conversation keeps track of new messages since it was - created until its exit and easily lets you query the - current state. - - If you need a conversation across two or more chats, - you should use two conversations and synchronize them - as you better see fit. - """ - _id_counter = 0 - _custom_counter = 0 - - def __init__(self, client, input_chat, - *, timeout, total_timeout, max_messages, - exclusive, replies_are_responses): - # This call resets the client - ChatGetter.__init__(self, input_chat=input_chat) - - self._id = Conversation._id_counter - Conversation._id_counter += 1 - - self._client = client - self._timeout = timeout - self._total_timeout = total_timeout - self._total_due = None - - self._outgoing = set() - self._last_outgoing = 0 - self._incoming = [] - self._last_incoming = 0 - self._max_incoming = max_messages - self._last_read = None - self._custom = {} - - self._pending_responses = {} - self._pending_replies = {} - self._pending_edits = {} - self._pending_reads = {} - - self._exclusive = exclusive - self._cancelled = False - - # The user is able to expect two responses for the same message. - # {desired message ID: next incoming index} - self._response_indices = {} - if replies_are_responses: - self._reply_indices = self._response_indices - else: - self._reply_indices = {} - - self._edit_dates = {} - - @_checks_cancelled - async def send_message(self, *args, **kwargs): - """ - Sends a message in the context of this conversation. Shorthand - for `telethon.client.messages.MessageMethods.send_message` with - ``entity`` already set. - """ - sent = await self._client.send_message( - self._input_chat, *args, **kwargs) - - # Albums will be lists, so handle that - ms = sent if isinstance(sent, list) else (sent,) - self._outgoing.update(m.id for m in ms) - self._last_outgoing = ms[-1].id - return sent - - @_checks_cancelled - async def send_file(self, *args, **kwargs): - """ - Sends a file in the context of this conversation. Shorthand - for `telethon.client.uploads.UploadMethods.send_file` with - ``entity`` already set. - """ - sent = await self._client.send_file( - self._input_chat, *args, **kwargs) - - # Albums will be lists, so handle that - ms = sent if isinstance(sent, list) else (sent,) - self._outgoing.update(m.id for m in ms) - self._last_outgoing = ms[-1].id - return sent - - @_checks_cancelled - def mark_read(self, message=None): - """ - Marks as read the latest received message if ``message is None``. - Otherwise, marks as read until the given message (or message ID). - - This is equivalent to calling `client.send_read_acknowledge - `. - """ - if message is None: - if self._incoming: - message = self._incoming[-1].id - else: - message = 0 - elif not isinstance(message, int): - message = message.id - - return self._client.send_read_acknowledge( - self._input_chat, max_id=message) - - def get_response(self, message=None, *, timeout=None): - """ - Gets the next message that responds to a previous one. This is - the method you need most of the time, along with `get_edit`. - - Args: - message (`Message ` | `int`, optional): - The message (or the message ID) for which a response - is expected. By default this is the last sent message. - - timeout (`int` | `float`, optional): - If present, this `timeout` (in seconds) will override the - per-action timeout defined for the conversation. - - .. code-block:: python - - async with client.conversation(...) as conv: - await conv.send_message('Hey, what is your name?') - - response = await conv.get_response() - name = response.text - - await conv.send_message('Nice to meet you, {}!'.format(name)) - """ - return self._get_message( - message, self._response_indices, self._pending_responses, timeout, - lambda x, y: True - ) - - def get_reply(self, message=None, *, timeout=None): - """ - Gets the next message that explicitly replies to a previous one. - """ - return self._get_message( - message, self._reply_indices, self._pending_replies, timeout, - lambda x, y: x.reply_to and x.reply_to.reply_to_msg_id == y - ) - - def _get_message( - self, target_message, indices, pending, timeout, condition): - """ - Gets the next desired message under the desired condition. - - Args: - target_message (`object`): - The target message for which we want to find another - response that applies based on `condition`. - - indices (`dict`): - This dictionary remembers the last ID chosen for the - input `target_message`. - - pending (`dict`): - This dictionary remembers {msg_id: Future} to be set - once `condition` is met. - - timeout (`int`): - The timeout (in seconds) override to use for this operation. - - condition (`callable`): - The condition callable that checks if an incoming - message is a valid response. - """ - start_time = time.time() - target_id = self._get_message_id(target_message) - - # If there is no last-chosen ID, make sure to pick one *after* - # the input message, since we don't want responses back in time - if target_id not in indices: - for i, incoming in enumerate(self._incoming): - if incoming.id > target_id: - indices[target_id] = i - break - else: - indices[target_id] = len(self._incoming) - - # We will always return a future from here, even if the result - # can be set immediately. Otherwise, needing to await only - # sometimes is an annoying edge case (i.e. we would return - # a `Message` but `get_response()` always `await`'s). - future = self._client.loop.create_future() - - # If there are enough responses saved return the next one - last_idx = indices[target_id] - if last_idx < len(self._incoming): - incoming = self._incoming[last_idx] - if condition(incoming, target_id): - indices[target_id] += 1 - future.set_result(incoming) - return future - - # Otherwise the next incoming response will be the one to use - # - # Note how we fill "pending" before giving control back to the - # event loop through "await". We want to register it as soon as - # possible, since any other task switch may arrive with the result. - pending[target_id] = future - return self._get_result(future, start_time, timeout, pending, target_id) - - def get_edit(self, message=None, *, timeout=None): - """ - Awaits for an edit after the last message to arrive. - The arguments are the same as those for `get_response`. - """ - start_time = time.time() - target_id = self._get_message_id(message) - - target_date = self._edit_dates.get(target_id, 0) - earliest_edit = min( - (x for x in self._incoming - if x.edit_date - and x.id > target_id - and x.edit_date.timestamp() > target_date - ), - key=lambda x: x.edit_date.timestamp(), - default=None - ) - - future = self._client.loop.create_future() - if earliest_edit and earliest_edit.edit_date.timestamp() > target_date: - self._edit_dates[target_id] = earliest_edit.edit_date.timestamp() - future.set_result(earliest_edit) - return future # we should always return something we can await - - # Otherwise the next incoming response will be the one to use - self._pending_edits[target_id] = future - return self._get_result(future, start_time, timeout, self._pending_edits, target_id) - - def wait_read(self, message=None, *, timeout=None): - """ - Awaits for the sent message to be marked as read. Note that - receiving a response doesn't imply the message was read, and - this action will also trigger even without a response. - """ - start_time = time.time() - future = self._client.loop.create_future() - target_id = self._get_message_id(message) - - if self._last_read is None: - self._last_read = target_id - 1 - - if self._last_read >= target_id: - return - - self._pending_reads[target_id] = future - return self._get_result(future, start_time, timeout, self._pending_reads, target_id) - - async def wait_event(self, event, *, timeout=None): - """ - Waits for a custom event to occur. Timeouts still apply. - - .. note:: - - **Only use this if there isn't another method available!** - For example, don't use `wait_event` for new messages, - since `get_response` already exists, etc. - - Unless you're certain that your code will run fast enough, - generally you should get a "handle" of this special coroutine - before acting. In this example you will see how to wait for a user - to join a group with proper use of `wait_event`: - - .. code-block:: python - - from telethon import TelegramClient, events - - client = TelegramClient(...) - group_id = ... - - async def main(): - # Could also get the user id from an event; this is just an example - user_id = ... - - async with client.conversation(user_id) as conv: - # Get a handle to the future event we'll wait for - handle = conv.wait_event(events.ChatAction( - group_id, - func=lambda e: e.user_joined and e.user_id == user_id - )) - - # Perform whatever action in between - await conv.send_message('Please join this group before speaking to me!') - - # Wait for the event we registered above to fire - event = await handle - - # Continue with the conversation - await conv.send_message('Thanks!') - - This way your event can be registered before acting, - since the response may arrive before your event was - registered. It depends on your use case since this - also means the event can arrive before you send - a previous action. - """ - start_time = time.time() - if isinstance(event, type): - event = event() - - await event.resolve(self._client) - - counter = Conversation._custom_counter - Conversation._custom_counter += 1 - - future = self._client.loop.create_future() - self._custom[counter] = (event, future) - try: - return await self._get_result(future, start_time, timeout, self._custom, counter) - finally: - # Need to remove it from the dict if it times out, else we may - # try and fail to set the result later (#1618). - self._custom.pop(counter, None) - - async def _check_custom(self, built): - for key, (ev, fut) in list(self._custom.items()): - ev_type = type(ev) - inst = built[ev_type] - - if inst: - filter = ev.filter(inst) - if inspect.isawaitable(filter): - filter = await filter - - if filter: - fut.set_result(inst) - del self._custom[key] - - def _on_new_message(self, response): - response = response.message - if response.chat_id != self.chat_id or response.out: - return - - if len(self._incoming) == self._max_incoming: - self._cancel_all(ValueError('Too many incoming messages')) - return - - self._incoming.append(response) - - # Most of the time, these dictionaries will contain just one item - # TODO In fact, why not make it be that way? Force one item only. - # How often will people want to wait for two responses at - # the same time? It's impossible, first one will arrive - # and then another, so they can do that. - for msg_id, future in list(self._pending_responses.items()): - self._response_indices[msg_id] = len(self._incoming) - future.set_result(response) - del self._pending_responses[msg_id] - - for msg_id, future in list(self._pending_replies.items()): - if response.reply_to and msg_id == response.reply_to.reply_to_msg_id: - self._reply_indices[msg_id] = len(self._incoming) - future.set_result(response) - del self._pending_replies[msg_id] - - def _on_edit(self, message): - message = message.message - if message.chat_id != self.chat_id or message.out: - return - - # We have to update our incoming messages with the new edit date - for i, m in enumerate(self._incoming): - if m.id == message.id: - self._incoming[i] = message - break - - for msg_id, future in list(self._pending_edits.items()): - if msg_id < message.id: - edit_ts = message.edit_date.timestamp() - - # We compare <= because edit_ts resolution is always to - # seconds, but we may have increased _edit_dates before. - # Since the dates are ever growing this is not a problem. - if edit_ts <= self._edit_dates.get(msg_id, 0): - self._edit_dates[msg_id] += _EDIT_COLLISION_DELTA - else: - self._edit_dates[msg_id] = message.edit_date.timestamp() - - future.set_result(message) - del self._pending_edits[msg_id] - - def _on_read(self, event): - if event.chat_id != self.chat_id or event.inbox: - return - - self._last_read = event.max_id - - for msg_id, pending in list(self._pending_reads.items()): - if msg_id >= self._last_read: - pending.set_result(True) - del self._pending_reads[msg_id] - - def _get_message_id(self, message): - if message is not None: # 0 is valid but false-y, check for None - return message if isinstance(message, int) else message.id - elif self._last_outgoing: - return self._last_outgoing - else: - raise ValueError('No message was sent previously') - - @_checks_cancelled - def _get_result(self, future, start_time, timeout, pending, target_id): - due = self._total_due - if timeout is None: - timeout = self._timeout - - if timeout is not None: - due = min(due, start_time + timeout) - - # NOTE: We can't try/finally to pop from pending here because - # the event loop needs to get back to us, but it might - # dispatch another update before, and in that case a - # response could be set twice. So responses must be - # cleared when their futures are set to a result. - return asyncio.wait_for( - future, - timeout=None if due == float('inf') else due - time.time() - ) - - def _cancel_all(self, exception=None): - self._cancelled = True - for pending in itertools.chain( - self._pending_responses.values(), - self._pending_replies.values(), - self._pending_edits.values()): - if exception: - pending.set_exception(exception) - else: - pending.cancel() - - for _, fut in self._custom.values(): - if exception: - fut.set_exception(exception) - else: - fut.cancel() - - async def __aenter__(self): - self._input_chat = \ - await self._client.get_input_entity(self._input_chat) - - self._chat_peer = utils.get_peer(self._input_chat) - - # Make sure we're the only conversation in this chat if it's exclusive - chat_id = utils.get_peer_id(self._chat_peer) - conv_set = self._client._conversations[chat_id] - if self._exclusive and conv_set: - raise errors.AlreadyInConversationError() - - conv_set.add(self) - self._cancelled = False - - self._last_outgoing = 0 - self._last_incoming = 0 - for d in ( - self._outgoing, self._incoming, - self._pending_responses, self._pending_replies, - self._pending_edits, self._response_indices, - self._reply_indices, self._edit_dates, self._custom): - d.clear() - - if self._total_timeout: - self._total_due = time.time() + self._total_timeout - else: - self._total_due = float('inf') - - return self - - def cancel(self): - """ - Cancels the current conversation. Pending responses and subsequent - calls to get a response will raise ``asyncio.CancelledError``. - - This method is synchronous and should not be awaited. - """ - self._cancel_all() - - async def cancel_all(self): - """ - Calls `cancel` on *all* conversations in this chat. - - Note that you should ``await`` this method, since it's meant to be - used outside of a context manager, and it needs to resolve the chat. - """ - chat_id = await self._client.get_peer_id(self._input_chat) - for conv in self._client._conversations[chat_id]: - conv.cancel() - - async def __aexit__(self, exc_type, exc_val, exc_tb): - chat_id = utils.get_peer_id(self._chat_peer) - conv_set = self._client._conversations[chat_id] - conv_set.discard(self) - if not conv_set: - del self._client._conversations[chat_id] - - self._cancel_all() - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit diff --git a/telethon/tl/custom/dialog.py b/telethon/tl/custom/dialog.py deleted file mode 100644 index 79ef1131..00000000 --- a/telethon/tl/custom/dialog.py +++ /dev/null @@ -1,161 +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, message): - # Both entities and messages being dicts {ID: item} - self._client = client - self.dialog = dialog - self.pinned = bool(dialog.pinned) - self.folder_id = dialog.folder_id - self.archived = dialog.folder_id is not None - self.message = message - self.date = getattr(self.message, 'date', None) - - self.entity = entities[utils.get_peer_id(dialog.peer)] - self.input_entity = utils.get_input_peer(self.entity) - self.id = utils.get_peer_id(self.entity) # ^ May be InputPeerSelf() - self.name = self.title = utils.get_display_name(self.entity) - - self.unread_count = dialog.unread_count - self.unread_mentions_count = dialog.unread_mentions_count - - self.draft = Draft(client, self.entity, self.dialog.draft) - - self.is_user = isinstance(self.entity, types.User) - self.is_group = ( - isinstance(self.entity, (types.Chat, types.ChatForbidden)) or - (isinstance(self.entity, types.Channel) and self.entity.megagroup) - ) - self.is_channel = isinstance(self.entity, types.Channel) - - async def send_message(self, *args, **kwargs): - """ - Sends a message to this dialog. This is just a wrapper around - ``client.send_message(dialog.input_entity, *args, **kwargs)``. - """ - return await self._client.send_message( - self.input_entity, *args, **kwargs) - - async def delete(self, revoke=False): - """ - Deletes the dialog from your dialog list. If you own the - channel this won't destroy it, only delete it from the list. - - Shorthand for `telethon.client.dialogs.DialogMethods.delete_dialog` - with ``entity`` already set. - """ - # Pass the entire entity so the method can determine whether - # the `Chat` is deactivated (in which case we don't kick ourselves, - # or it would raise `PEER_ID_INVALID`). - await self._client.delete_dialog(self.entity, revoke=revoke) - - async def archive(self, folder=1): - """ - Archives (or un-archives) this dialog. - - Args: - folder (`int`, optional): - The folder to which the dialog should be archived to. - - If you want to "un-archive" it, use ``folder=0``. - - Returns: - The :tl:`Updates` object that the request produces. - - Example: - - .. code-block:: python - - # Archiving - dialog.archive() - - # Un-archiving - dialog.archive(0) - """ - return await self._client(functions.folders.EditPeerFoldersRequest([ - types.InputFolderPeer(self.input_entity, folder_id=folder) - ])) - - def to_dict(self): - return { - '_': 'Dialog', - 'name': self.name, - 'date': self.date, - 'draft': self.draft, - 'message': self.message, - 'entity': self.entity, - } - - def __str__(self): - return TLObject.pretty_format(self.to_dict()) - - def stringify(self): - return TLObject.pretty_format(self.to_dict(), indent=0) diff --git a/telethon/tl/custom/draft.py b/telethon/tl/custom/draft.py deleted file mode 100644 index c482b64f..00000000 --- a/telethon/tl/custom/draft.py +++ /dev/null @@ -1,191 +0,0 @@ -import datetime - -from .. import TLObject -from ..functions.messages import SaveDraftRequest -from ..types import DraftMessage -from ...errors import RPCError -from ...extensions import markdown -from ...utils import get_input_peer, get_peer, get_peer_id - - -class Draft: - """ - Custom class that encapsulates a draft on the Telegram servers, providing - an abstraction to change the message conveniently. The library will return - instances of this class when calling :meth:`get_drafts()`. - - Args: - date (`datetime`): - The date of the draft. - - link_preview (`bool`): - Whether the link preview is enabled or not. - - reply_to_msg_id (`int`): - The message ID that the draft will reply to. - """ - def __init__(self, client, entity, draft): - self._client = client - self._peer = get_peer(entity) - self._entity = entity - self._input_entity = get_input_peer(entity) if entity else None - - if not draft or not isinstance(draft, DraftMessage): - draft = DraftMessage('', None, None, None, None) - - self._text = markdown.unparse(draft.message, draft.entities) - self._raw_text = draft.message - self.date = draft.date - self.link_preview = not draft.no_webpage - self.reply_to_msg_id = draft.reply_to_msg_id - - @property - def entity(self): - """ - The entity that belongs to this dialog (user, chat or channel). - """ - return self._entity - - @property - def input_entity(self): - """ - Input version of the entity. - """ - if not self._input_entity: - try: - self._input_entity = self._client._mb_entity_cache.get( - get_peer_id(self._peer, add_mark=False))._as_input_peer() - except AttributeError: - pass - - return self._input_entity - - async def get_entity(self): - """ - Returns `entity` but will make an API call if necessary. - """ - if not self.entity and await self.get_input_entity(): - try: - self._entity =\ - await self._client.get_entity(self._input_entity) - except ValueError: - pass - - return self._entity - - async def get_input_entity(self): - """ - Returns `input_entity` but will make an API call if necessary. - """ - # We don't actually have an API call we can make yet - # to get more info, but keep this method for consistency. - return self.input_entity - - @property - def text(self): - """ - The markdown text contained in the draft. It will be - empty if there is no text (and hence no draft is set). - """ - return self._text - - @property - def raw_text(self): - """ - The raw (text without formatting) contained in the draft. - It will be empty if there is no text (thus draft not set). - """ - return self._raw_text - - @property - def is_empty(self): - """ - Convenience bool to determine if the draft is empty or not. - """ - return not self._text - - async def set_message( - self, text=None, reply_to=0, parse_mode=(), - link_preview=None): - """ - Changes the draft message on the Telegram servers. The changes are - reflected in this object. - - :param str text: New text of the draft. - Preserved if left as None. - - :param int reply_to: Message ID to reply to. - Preserved if left as 0, erased if set to None. - - :param bool link_preview: Whether to attach a web page preview. - Preserved if left as None. - - :param str parse_mode: The parse mode to be used for the text. - :return bool: `True` on success. - """ - if text is None: - text = self._text - - if reply_to == 0: - reply_to = self.reply_to_msg_id - - if link_preview is None: - link_preview = self.link_preview - - raw_text, entities =\ - await self._client._parse_message_text(text, parse_mode) - - result = await self._client(SaveDraftRequest( - peer=self._peer, - message=raw_text, - no_webpage=not link_preview, - reply_to_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 deleted file mode 100644 index aa714828..00000000 --- a/telethon/tl/custom/file.py +++ /dev/null @@ -1,146 +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 old bot-API style ``file_id`` representing this file. - - .. warning:: - - This feature has not been maintained for a long time and - may not work. It will be removed in future versions. - - .. note:: - - This file ID may not work under user accounts, - but should still be usable by bot accounts. - - You can, however, still use it to identify - a file in for example a database. - """ - return utils.pack_bot_file_id(self.media) - - @property - def name(self): - """ - The file name of this document. - """ - return self._from_attr(types.DocumentAttributeFilename, 'file_name') - - @property - def ext(self): - """ - The extension from the mime type of this file. - - If the mime type is unknown, the extension - from the file name (if any) will be used. - """ - return ( - mimetypes.guess_extension(self.mime_type) - or os.path.splitext(self.name or '')[-1] - or None - ) - - @property - def mime_type(self): - """ - The mime-type of this file. - """ - if isinstance(self.media, types.Photo): - return 'image/jpeg' - elif isinstance(self.media, types.Document): - return self.media.mime_type - - @property - def width(self): - """ - The width in pixels of this media if it's a photo or a video. - """ - if isinstance(self.media, types.Photo): - return max(getattr(s, 'w', 0) for s in self.media.sizes) - - return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') - - @property - def height(self): - """ - The height in pixels of this media if it's a photo or a video. - """ - if isinstance(self.media, types.Photo): - return max(getattr(s, 'h', 0) for s in self.media.sizes) - - return self._from_attr(( - types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') - - @property - def duration(self): - """ - The duration in seconds of the audio or video. - """ - return self._from_attr(( - types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') - - @property - def title(self): - """ - The title of the song. - """ - return self._from_attr(types.DocumentAttributeAudio, 'title') - - @property - def performer(self): - """ - The performer of the song. - """ - return self._from_attr(types.DocumentAttributeAudio, 'performer') - - @property - def emoji(self): - """ - A string with all emoji that represent the current sticker. - """ - return self._from_attr(types.DocumentAttributeSticker, 'alt') - - @property - def sticker_set(self): - """ - The :tl:`InputStickerSet` to which the sticker file belongs. - """ - return self._from_attr(types.DocumentAttributeSticker, 'stickerset') - - @property - def size(self): - """ - The size in bytes of this file. - - For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions. - """ - if isinstance(self.media, types.Photo): - return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None) - elif isinstance(self.media, types.Document): - return self.media.size - - def _from_attr(self, cls, field): - if isinstance(self.media, types.Document): - for attr in self.media.attributes: - if isinstance(attr, cls): - return getattr(attr, field, None) diff --git a/telethon/tl/custom/forward.py b/telethon/tl/custom/forward.py deleted file mode 100644 index afe6b3a0..00000000 --- a/telethon/tl/custom/forward.py +++ /dev/null @@ -1,51 +0,0 @@ -from .chatgetter import ChatGetter -from .sendergetter import SenderGetter -from ... import utils, helpers -from ...tl import types - - -class Forward(ChatGetter, SenderGetter): - """ - Custom class that encapsulates a :tl:`MessageFwdHeader` providing an - abstraction to easily access information like the original sender. - - Remember that this class implements `ChatGetter - ` and `SenderGetter - ` which means you - have access to all their sender and chat properties and methods. - - Attributes: - - original_fwd (:tl:`MessageFwdHeader`): - The original :tl:`MessageFwdHeader` instance. - - Any other attribute: - Attributes not described here are the same as those available - in the original :tl:`MessageFwdHeader`. - """ - def __init__(self, client, original, entities): - # Copy all the fields, not reference! It would cause memory cycles: - # self.original_fwd.original_fwd.original_fwd.original_fwd - # ...would be valid if we referenced. - self.__dict__.update(original.__dict__) - self.original_fwd = original - - sender_id = sender = input_sender = peer = chat = input_chat = None - if original.from_id: - ty = helpers._entity_type(original.from_id) - if ty == helpers._EntityType.USER: - sender_id = utils.get_peer_id(original.from_id) - sender, input_sender = utils._get_entity_pair( - sender_id, entities, client._mb_entity_cache) - - elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): - peer = original.from_id - chat, input_chat = utils._get_entity_pair( - utils.get_peer_id(peer), entities, client._mb_entity_cache) - - # This call resets the client - ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) - SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender) - self._client = client - - # TODO We could reload the message diff --git a/telethon/tl/custom/inlinebuilder.py b/telethon/tl/custom/inlinebuilder.py deleted file mode 100644 index f82b7e67..00000000 --- a/telethon/tl/custom/inlinebuilder.py +++ /dev/null @@ -1,450 +0,0 @@ -import hashlib - -from .. import functions, types -from ... import utils - -_TYPE_TO_MIMES = { - 'gif': ['image/gif'], # 'video/mp4' too, but that's used for video - 'article': ['text/html'], - 'audio': ['audio/mpeg'], - 'contact': [], - 'file': ['application/pdf', 'application/zip'], # actually any - 'geo': [], - 'photo': ['image/jpeg'], - 'sticker': ['image/webp', 'application/x-tgsticker'], - 'venue': [], - 'video': ['video/mp4'], # tdlib includes text/html for some reason - 'voice': ['audio/ogg'], -} - - -class InlineBuilder: - """ - Helper class to allow defining `InlineQuery - ` ``results``. - - Common arguments to all methods are - explained here to avoid repetition: - - text (`str`, optional): - If present, the user will send a text - message with this text upon being clicked. - - link_preview (`bool`, optional): - Whether to show a link preview in the sent - text message or not. - - geo (:tl:`InputGeoPoint`, :tl:`GeoPoint`, :tl:`InputMediaVenue`, :tl:`MessageMediaVenue`, optional): - If present, it may either be a geo point or a venue. - - period (int, optional): - The period in seconds to be used for geo points. - - contact (:tl:`InputMediaContact`, :tl:`MessageMediaContact`, optional): - If present, it must be the contact information to send. - - game (`bool`, optional): - May be `True` to indicate that the game will be sent. - - buttons (`list`, `custom.Button `, :tl:`KeyboardButton`, optional): - Same as ``buttons`` for `client.send_message() - `. - - parse_mode (`str`, optional): - Same as ``parse_mode`` for `client.send_message() - `. - - id (`str`, optional): - The string ID to use for this result. If not present, it - will be the SHA256 hexadecimal digest of converting the - created :tl:`InputBotInlineResult` with empty ID to ``bytes()``, - so that the ID will be deterministic for the same input. - - .. note:: - - If two inputs are exactly the same, their IDs will be the same - too. If you send two articles with the same ID, it will raise - ``ResultIdDuplicateError``. Consider giving them an explicit - ID if you need to send two results that are the same. - """ - def __init__(self, client): - self._client = client - - # noinspection PyIncorrectDocstring - async def article( - self, title, description=None, - *, url=None, thumb=None, content=None, - id=None, text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates new inline result of article type. - - Args: - title (`str`): - The title to be shown for this result. - - description (`str`, optional): - Further explanation of what this result means. - - url (`str`, optional): - The URL to be shown for this result. - - thumb (:tl:`InputWebDocument`, optional): - The thumbnail to be shown for this result. - For now it has to be a :tl:`InputWebDocument` if present. - - content (:tl:`InputWebDocument`, optional): - The content to be shown for this result. - For now it has to be a :tl:`InputWebDocument` if present. - - Example: - .. code-block:: python - - results = [ - # Option with title and description sending a message. - builder.article( - title='First option', - description='This is the first option', - text='Text sent after clicking this option', - ), - # Option with title URL to be opened when clicked. - builder.article( - title='Second option', - url='https://example.com', - text='Text sent if the user clicks the option and not the URL', - ), - # Sending a message with buttons. - # You can use a list or a list of lists to include more buttons. - builder.article( - title='Third option', - text='Text sent with buttons below', - buttons=Button.url('https://example.com'), - ), - ] - """ - # TODO Does 'article' work always? - # article, photo, gif, mpeg4_gif, video, audio, - # voice, document, location, venue, contact, game - result = types.InputBotInlineResult( - id=id or '', - type='article', - send_message=await self._message( - text=text, parse_mode=parse_mode, link_preview=link_preview, - geo=geo, period=period, - contact=contact, - game=game, - buttons=buttons - ), - title=title, - description=description, - url=url, - thumb=thumb, - content=content - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def photo( - self, file, *, id=None, include_media=True, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates a new inline result of photo type. - - Args: - include_media (`bool`, optional): - Whether the photo file used to display the result should be - included in the message itself or not. By default, the photo - is included, and the text parameter alters the caption. - - file (`obj`, optional): - Same as ``file`` for `client.send_file() - `. - - Example: - .. code-block:: python - - results = [ - # Sending just the photo when the user selects it. - builder.photo('/path/to/photo.jpg'), - - # Including a caption with some in-memory photo. - photo_bytesio = ... - builder.photo( - photo_bytesio, - text='This will be the caption of the sent photo', - ), - - # Sending just the message without including the photo. - builder.photo( - photo, - text='This will be a normal text message', - include_media=False, - ), - ] - """ - try: - fh = utils.get_input_photo(file) - except TypeError: - _, media, _ = await self._client._file_to_media( - file, allow_cache=True, as_image=True - ) - if isinstance(media, types.InputPhoto): - fh = media - else: - r = await self._client(functions.messages.UploadMediaRequest( - types.InputPeerSelf(), media=media - )) - fh = utils.get_input_photo(r.photo) - - result = types.InputBotInlineResultPhoto( - id=id or '', - type='photo', - photo=fh, - send_message=await self._message( - text=text or '', - parse_mode=parse_mode, - link_preview=link_preview, - media=include_media, - geo=geo, - period=period, - contact=contact, - game=game, - buttons=buttons - ) - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def document( - self, file, title=None, *, description=None, type=None, - mime_type=None, attributes=None, force_document=False, - voice_note=False, video_note=False, use_cache=True, id=None, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None, - include_media=True - ): - """ - Creates a new inline result of document type. - - `use_cache`, `mime_type`, `attributes`, `force_document`, - `voice_note` and `video_note` are described in `client.send_file - `. - - Args: - file (`obj`): - Same as ``file`` for `client.send_file() - `. - - title (`str`, optional): - The title to be shown for this result. - - description (`str`, optional): - Further explanation of what this result means. - - type (`str`, optional): - The type of the document. May be one of: article, audio, - contact, file, geo, gif, photo, sticker, venue, video, voice. - It will be automatically set if ``mime_type`` is specified, - and default to ``'file'`` if no matching mime type is found. - you may need to pass ``attributes`` in order to use ``type`` - effectively. - - attributes (`list`, optional): - Optional attributes that override the inferred ones, like - :tl:`DocumentAttributeFilename` and so on. - - include_media (`bool`, optional): - Whether the document file used to display the result should be - included in the message itself or not. By default, the document - is included, and the text parameter alters the caption. - - Example: - .. code-block:: python - - results = [ - # Sending just the file when the user selects it. - builder.document('/path/to/file.pdf'), - - # Including a caption with some in-memory file. - file_bytesio = ... - builder.document( - file_bytesio, - text='This will be the caption of the sent file', - ), - - # Sending just the message without including the file. - builder.document( - photo, - text='This will be a normal text message', - include_media=False, - ), - ] - """ - if type is None: - if voice_note: - type = 'voice' - elif mime_type: - for ty, mimes in _TYPE_TO_MIMES.items(): - for mime in mimes: - if mime_type == mime: - type = ty - break - - if type is None: - type = 'file' - - try: - fh = utils.get_input_document(file) - except TypeError: - _, media, _ = await self._client._file_to_media( - file, - mime_type=mime_type, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - allow_cache=use_cache - ) - if isinstance(media, types.InputDocument): - fh = media - else: - r = await self._client(functions.messages.UploadMediaRequest( - types.InputPeerSelf(), media=media - )) - fh = utils.get_input_document(r.document) - - result = types.InputBotInlineResultDocument( - id=id or '', - type=type, - document=fh, - send_message=await self._message( - # Empty string for text if there's media but text is None. - # We may want to display a document but send text; however - # default to sending the media (without text, i.e. stickers). - text=text or '', - parse_mode=parse_mode, - link_preview=link_preview, - media=include_media, - geo=geo, - period=period, - contact=contact, - game=game, - buttons=buttons - ), - title=title, - description=description - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - # noinspection PyIncorrectDocstring - async def game( - self, short_name, *, id=None, - text=None, parse_mode=(), link_preview=True, - geo=None, period=60, contact=None, game=False, buttons=None - ): - """ - Creates a new inline result of game type. - - Args: - short_name (`str`): - The short name of the game to use. - """ - result = types.InputBotInlineResultGame( - id=id or '', - short_name=short_name, - send_message=await self._message( - text=text, parse_mode=parse_mode, link_preview=link_preview, - geo=geo, period=period, - contact=contact, - game=game, - buttons=buttons - ) - ) - if id is None: - result.id = hashlib.sha256(bytes(result)).hexdigest() - - return result - - async def _message( - self, *, - text=None, parse_mode=(), link_preview=True, media=False, - geo=None, period=60, contact=None, game=False, buttons=None - ): - # Empty strings are valid but false-y; if they're empty use dummy '\0' - args = ('\0' if text == '' else text, geo, contact, game) - if sum(1 for x in args if x is not None and x is not False) != 1: - raise ValueError( - 'Must set exactly one of text, geo, contact or game (set {})' - .format(', '.join(x[0] for x in zip( - 'text geo contact game'.split(), args) if x[1]) or 'none') - ) - - markup = self._client.build_reply_markup(buttons, inline_only=True) - if text is not None: - text, msg_entities = await self._client._parse_message_text( - text, parse_mode - ) - if media: - # "MediaAuto" means it will use whatever media the inline - # result itself has (stickers, photos, or documents), while - # respecting the user's text (caption) and formatting. - return types.InputBotInlineMessageMediaAuto( - message=text, - entities=msg_entities, - reply_markup=markup - ) - else: - return types.InputBotInlineMessageText( - message=text, - no_webpage=not link_preview, - entities=msg_entities, - reply_markup=markup - ) - elif isinstance(geo, (types.InputGeoPoint, types.GeoPoint)): - return types.InputBotInlineMessageMediaGeo( - geo_point=utils.get_input_geo(geo), - period=period, - reply_markup=markup - ) - elif isinstance(geo, (types.InputMediaVenue, types.MessageMediaVenue)): - if isinstance(geo, types.InputMediaVenue): - geo_point = geo.geo_point - else: - geo_point = geo.geo - - return types.InputBotInlineMessageMediaVenue( - geo_point=geo_point, - title=geo.title, - address=geo.address, - provider=geo.provider, - venue_id=geo.venue_id, - venue_type=geo.venue_type, - reply_markup=markup - ) - elif isinstance(contact, ( - types.InputMediaContact, types.MessageMediaContact)): - return types.InputBotInlineMessageMediaContact( - phone_number=contact.phone_number, - first_name=contact.first_name, - last_name=contact.last_name, - vcard=contact.vcard, - reply_markup=markup - ) - elif game: - return types.InputBotInlineMessageGame( - reply_markup=markup - ) - else: - raise ValueError('No text, game or valid geo or contact given') diff --git a/telethon/tl/custom/inlineresult.py b/telethon/tl/custom/inlineresult.py deleted file mode 100644 index 15639aa5..00000000 --- a/telethon/tl/custom/inlineresult.py +++ /dev/null @@ -1,176 +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. - """ - # tdlib types are the following (InlineQueriesManager::answer_inline_query @ 1a4a834): - # gif, article, audio, contact, file, geo, photo, sticker, venue, video, voice - # - # However, those documented in https://core.telegram.org/bots/api#inline-mode are different. - ARTICLE = 'article' - PHOTO = 'photo' - GIF = 'gif' - VIDEO = 'video' - VIDEO_GIF = 'mpeg4_gif' - AUDIO = 'audio' - DOCUMENT = 'document' - LOCATION = 'location' - VENUE = 'venue' - CONTACT = 'contact' - GAME = 'game' - - def __init__(self, client, original, query_id=None, *, entity=None): - self._client = client - self.result = original - self._query_id = query_id - self._entity = entity - - @property - def type(self): - """ - The always-present type of this result. It will be one of: - ``'article'``, ``'photo'``, ``'gif'``, ``'mpeg4_gif'``, ``'video'``, - ``'audio'``, ``'voice'``, ``'document'``, ``'location'``, ``'venue'``, - ``'contact'``, ``'game'``. - - You can access all of these constants through `InlineResult`, - such as `InlineResult.ARTICLE`, `InlineResult.VIDEO_GIF`, etc. - """ - return self.result.type - - @property - def message(self): - """ - The always-present :tl:`BotInlineMessage` that - will be sent if `click` is called on this result. - """ - return self.result.send_message - - @property - def title(self): - """ - The title for this inline result. It may be `None`. - """ - return self.result.title - - @property - def description(self): - """ - The description for this inline result. It may be `None`. - """ - return self.result.description - - @property - def url(self): - """ - The URL present in this inline results. If you want to "click" - this URL to open it in your browser, you should use Python's - `webbrowser.open(url)` for such task. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.url - - @property - def photo(self): - """ - Returns either the :tl:`WebDocument` thumbnail for - normal results or the :tl:`Photo` for media results. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.thumb - elif isinstance(self.result, types.BotInlineMediaResult): - return self.result.photo - - @property - def document(self): - """ - Returns either the :tl:`WebDocument` content for - normal results or the :tl:`Document` for media results. - """ - if isinstance(self.result, types.BotInlineResult): - return self.result.content - elif isinstance(self.result, types.BotInlineMediaResult): - return self.result.document - - async def click(self, entity=None, reply_to=None, comment_to=None, - silent=False, clear_draft=False, hide_via=False, - background=None): - """ - Clicks this result and sends the associated `message`. - - Args: - entity (`entity`): - The entity to which the message of this result should be sent. - - reply_to (`int` | `Message `, optional): - If present, the sent message will reply to this ID or message. - - comment_to (`int` | `Message `, optional): - Similar to ``reply_to``, but replies in the linked group of a - broadcast channel instead (effectively leaving a "comment to" - the specified message). - - silent (`bool`, optional): - Whether the message should notify people with sound or not. - Defaults to `False` (send with a notification sound unless - the person has the chat muted). Set it to `True` to alter - this behaviour. - - clear_draft (`bool`, optional): - Whether the draft should be removed after sending the - message from this result or not. Defaults to `False`. - - hide_via (`bool`, optional): - Whether the "via @bot" should be hidden or not. - Only works with certain bots (like @bing or @gif). - - background (`bool`, optional): - Whether the message should be send in background. - - """ - if entity: - entity = await self._client.get_input_entity(entity) - elif self._entity: - entity = self._entity - else: - raise ValueError('You must provide the entity where the result should be sent to') - - if comment_to: - entity, reply_id = await self._client._get_comment_data(entity, comment_to) - else: - reply_id = None if reply_to is None else utils.get_message_id(reply_to) - - req = functions.messages.SendInlineBotResultRequest( - peer=entity, - query_id=self._query_id, - id=self.result.id, - silent=silent, - background=background, - clear_draft=clear_draft, - hide_via=hide_via, - reply_to_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 deleted file mode 100644 index 9ab0c0a8..00000000 --- a/telethon/tl/custom/inlineresults.py +++ /dev/null @@ -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, *, entity=None): - super().__init__(InlineResult(client, x, original.query_id, entity=entity) - for x in original.results) - - self.result = original - self.query_id = original.query_id - self.cache_time = original.cache_time - self._valid_until = time.time() + self.cache_time - self.users = original.users - self.gallery = bool(original.gallery) - self.next_offset = original.next_offset - self.switch_pm = original.switch_pm - - def results_valid(self): - """ - Returns `True` if the cache time has not expired - yet and the results can still be considered valid. - """ - return time.time() < self._valid_until - - def _to_str(self, item_function): - return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, ' - 'next_offset={}, switch_pm={}]'.format( - ', '.join(item_function(x) for x in self), - self.query_id, - self.cache_time, - self.users, - self.gallery, - self.next_offset, - self.switch_pm - )) - - def __str__(self): - return self._to_str(str) - - def __repr__(self): - return self._to_str(repr) diff --git a/telethon/tl/custom/inputsizedfile.py b/telethon/tl/custom/inputsizedfile.py deleted file mode 100644 index fcb743f6..00000000 --- a/telethon/tl/custom/inputsizedfile.py +++ /dev/null @@ -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 deleted file mode 100644 index 4959ed0f..00000000 --- a/telethon/tl/custom/message.py +++ /dev/null @@ -1,1159 +0,0 @@ -from typing import Optional, List, TYPE_CHECKING -from datetime import datetime -from .chatgetter import ChatGetter -from .sendergetter import SenderGetter -from .messagebutton import MessageButton -from .forward import Forward -from .file import File -from .. import TLObject, types, functions, alltlobjects -from ... import utils, errors - - -# TODO Figure out a way to have the code generator error on missing fields -# Maybe parsing the init function alone if that's possible. -class Message(ChatGetter, SenderGetter, TLObject): - """ - This custom class aggregates both :tl:`Message` and - :tl:`MessageService` to ease accessing their members. - - Remember that this class implements `ChatGetter - ` and `SenderGetter - ` which means you - have access to all their sender and chat properties and methods. - - Members: - out (`bool`): - Whether the message is outgoing (i.e. you sent it from - another session) or incoming (i.e. someone else sent it). - - Note that messages in your own chat are always incoming, - but this member will be `True` if you send a message - to your own chat. Messages you forward to your chat are - *not* considered outgoing, just like official clients - display them. - - mentioned (`bool`): - Whether you were mentioned in this message or not. - Note that replies to your own messages also count - as mentions. - - media_unread (`bool`): - Whether you have read the media in this message - or not, e.g. listened to the voice note media. - - silent (`bool`): - Whether the message should notify people with sound or not. - Previously used in channels, but since 9 August 2019, it can - also be `used in private chats - `_. - - post (`bool`): - Whether this message is a post in a broadcast - channel or not. - - from_scheduled (`bool`): - Whether this message was originated from a previously-scheduled - message or not. - - legacy (`bool`): - Whether this is a legacy message or not. - - edit_hide (`bool`): - Whether the edited mark of this message is edited - should be hidden (e.g. in GUI clients) or shown. - - pinned (`bool`): - Whether this message is currently pinned or not. - - noforwards (`bool`): - Whether this message can be forwarded or not. - - id (`int`): - The ID of this message. This field is *always* present. - Any other member is optional and may be `None`. - - from_id (:tl:`Peer`): - The peer who sent this message, which is either - :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. - This value will be `None` for anonymous messages. - - peer_id (:tl:`Peer`): - The peer to which this message was sent, which is either - :tl:`PeerUser`, :tl:`PeerChat` or :tl:`PeerChannel`. This - will always be present except for empty messages. - - fwd_from (:tl:`MessageFwdHeader`): - The original forward header if this message is a forward. - You should probably use the `forward` property instead. - - via_bot_id (`int`): - The ID of the bot used to send this message - through its inline mode (e.g. "via @like"). - - reply_to (:tl:`MessageReplyHeader`): - The original reply header if this message is replying to another. - - date (`datetime`): - The UTC+0 `datetime` object indicating when this message - was sent. This will always be present except for empty - messages. - - message (`str`): - The string text of the message for `Message - ` instances, - which will be `None` for other types of messages. - - media (:tl:`MessageMedia`): - The media sent with this message if any (such as - photos, videos, documents, gifs, stickers, etc.). - - You may want to access the `photo`, `document` - etc. properties instead. - - If the media was not present or it was :tl:`MessageMediaEmpty`, - this member will instead be `None` for convenience. - - reply_markup (:tl:`ReplyMarkup`): - The reply markup for this message (which was sent - either via a bot or by a bot). You probably want - to access `buttons` instead. - - entities (List[:tl:`MessageEntity`]): - The list of markup entities in this message, - such as bold, italics, code, hyperlinks, etc. - - views (`int`): - The number of views this message from a broadcast - channel has. This is also present in forwards. - - forwards (`int`): - The number of times this message has been forwarded. - - replies (`int`): - The number of times another message has replied to this message. - - edit_date (`datetime`): - The date when this message was last edited. - - post_author (`str`): - The display name of the message sender to - show in messages sent to broadcast channels. - - grouped_id (`int`): - If this message belongs to a group of messages - (photo albums or video albums), all of them will - have the same value here. - - reactions (:tl:`MessageReactions`) - Reactions to this message. - - restriction_reason (List[:tl:`RestrictionReason`]) - An optional list of reasons why this message was restricted. - If the list is `None`, this message has not been restricted. - - ttl_period (`int`): - The Time To Live period configured for this message. - The message should be erased from wherever it's stored (memory, a - local database, etc.) when - ``datetime.now() > message.date + timedelta(seconds=message.ttl_period)``. - - action (:tl:`MessageAction`): - The message action object of the message for :tl:`MessageService` - instances, which will be `None` for other types of messages. - """ - - # region Initialization - - def __init__( - # Common to all - self, id: int, - - # Common to Message and MessageService (mandatory) - peer_id: types.TypePeer = None, - date: Optional[datetime] = None, - - # Common to Message and MessageService (flags) - out: Optional[bool] = None, - mentioned: Optional[bool] = None, - media_unread: Optional[bool] = None, - silent: Optional[bool] = None, - post: Optional[bool] = None, - from_id: Optional[types.TypePeer] = None, - reply_to: Optional[types.TypeMessageReplyHeader] = None, - ttl_period: Optional[int] = None, - - # For Message (mandatory) - message: Optional[str] = None, - - # For Message (flags) - fwd_from: Optional[types.TypeMessageFwdHeader] = None, - via_bot_id: Optional[int] = None, - media: Optional[types.TypeMessageMedia] = None, - reply_markup: Optional[types.TypeReplyMarkup] = None, - entities: Optional[List[types.TypeMessageEntity]] = None, - views: Optional[int] = None, - edit_date: Optional[datetime] = None, - post_author: Optional[str] = None, - grouped_id: Optional[int] = None, - from_scheduled: Optional[bool] = None, - legacy: Optional[bool] = None, - edit_hide: Optional[bool] = None, - pinned: Optional[bool] = None, - noforwards: Optional[bool] = None, - reactions: Optional[types.TypeMessageReactions] = None, - restriction_reason: Optional[types.TypeRestrictionReason] = None, - forwards: Optional[int] = None, - replies: Optional[types.TypeMessageReplies] = None, - - # For MessageAction (mandatory) - action: Optional[types.TypeMessageAction] = None - ): - # Common properties to messages, then to service (in the order they're defined in the `.tl`) - self.out = bool(out) - self.mentioned = mentioned - self.media_unread = media_unread - self.silent = silent - self.post = post - self.from_scheduled = from_scheduled - self.legacy = legacy - self.edit_hide = edit_hide - self.id = id - self.from_id = from_id - self.peer_id = peer_id - self.fwd_from = fwd_from - self.via_bot_id = via_bot_id - self.reply_to = reply_to - self.date = date - self.message = message - self.media = None if isinstance(media, types.MessageMediaEmpty) else media - self.reply_markup = reply_markup - self.entities = entities - self.views = views - self.forwards = forwards - self.replies = replies - self.edit_date = edit_date - self.pinned = pinned - self.noforwards = noforwards - self.post_author = post_author - self.grouped_id = grouped_id - self.reactions = reactions - self.restriction_reason = restriction_reason - self.ttl_period = ttl_period - 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 - self._linked_chat = None - - sender_id = None - if from_id is not None: - sender_id = utils.get_peer_id(from_id) - elif peer_id: - # If the message comes from a Channel, let the sender be it - # ...or... - # incoming messages in private conversations no longer have from_id - # (layer 119+), but the sender can only be the chat we're in. - if post or (not out and isinstance(peer_id, types.PeerUser)): - sender_id = utils.get_peer_id(peer_id) - - # Note that these calls would reset the client - ChatGetter.__init__(self, peer_id, broadcast=post) - SenderGetter.__init__(self, sender_id) - - self._forward = None - - def _finish_init(self, client, entities, input_chat): - """ - Finishes the initialization of this message by setting - the client that sent the message and making use of the - known entities. - """ - self._client = client - - # Make messages sent to ourselves outgoing unless they're forwarded. - # This makes it consistent with official client's appearance. - if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from: - self.out = True - - cache = client._mb_entity_cache - - self._sender, self._input_sender = utils._get_entity_pair( - self.sender_id, entities, cache) - - self._chat, self._input_chat = utils._get_entity_pair( - self.chat_id, entities, cache) - - if input_chat: # This has priority - self._input_chat = input_chat - - if self.via_bot_id: - self._via_bot, self._via_input_bot = utils._get_entity_pair( - self.via_bot_id, entities, cache) - - if self.fwd_from: - self._forward = Forward(self._client, self.fwd_from, entities) - - if self.action: - if isinstance(self.action, (types.MessageActionChatAddUser, - types.MessageActionChatCreate)): - self._action_entities = [entities.get(i) - for i in self.action.users] - elif isinstance(self.action, types.MessageActionChatDeleteUser): - self._action_entities = [entities.get(self.action.user_id)] - elif isinstance(self.action, types.MessageActionChatJoinedByLink): - self._action_entities = [entities.get(self.action.inviter_id)] - elif isinstance(self.action, types.MessageActionChatMigrateTo): - self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChannel(self.action.channel_id)))] - elif isinstance( - self.action, types.MessageActionChannelMigrateFrom): - self._action_entities = [entities.get(utils.get_peer_id( - types.PeerChat(self.action.chat_id)))] - - if self.replies and self.replies.channel_id: - self._linked_chat = entities.get(utils.get_peer_id( - types.PeerChannel(self.replies.channel_id))) - - - # endregion Initialization - - # region Public Properties - - @property - def client(self): - """ - Returns the `TelegramClient ` - that *patched* this message. This will only be present if you - **use the friendly methods**, it won't be there if you invoke - raw API methods manually, in which case you should only access - members, not properties. - """ - return self._client - - @property - def text(self): - """ - The message text, formatted using the client's default - parse mode. Will be `None` for :tl:`MessageService`. - """ - if self._text is None and self._client: - if not self._client.parse_mode: - self._text = self.message - else: - self._text = self._client.parse_mode.unparse( - self.message, self.entities) - - return self._text - - @text.setter - def text(self, value): - self._text = value - if self._client and self._client.parse_mode: - self.message, self.entities = self._client.parse_mode.parse(value) - else: - self.message, self.entities = value, [] - - @property - def raw_text(self): - """ - The raw message text, ignoring any formatting. - Will be `None` for :tl:`MessageService`. - - Setting a value to this field will erase the - `entities`, unlike changing the `message` member. - """ - return self.message - - @raw_text.setter - def raw_text(self, value): - self.message = value - self.entities = [] - self._text = None - - @property - def is_reply(self): - """ - `True` if the message is a reply to some other message. - - Remember that you can access the ID of the message - this one is replying to through `reply_to.reply_to_msg_id`, - and the `Message` object with `get_reply_message()`. - """ - return self.reply_to is not None - - @property - def forward(self): - """ - The `Forward ` - information if this message is a forwarded message. - """ - return self._forward - - @property - def buttons(self): - """ - Returns a list of lists of `MessageButton - `, - if any. - - Otherwise, it returns `None`. - """ - if self._buttons is None and self.reply_markup: - if not self.input_chat: - return - try: - bot = self._needed_markup_bot() - except ValueError: - return - else: - self._set_buttons(self._input_chat, bot) - - return self._buttons - - async def get_buttons(self): - """ - Returns `buttons` when that property fails (this is rarely needed). - """ - if not self.buttons and self.reply_markup: - chat = await self.get_input_chat() - if not chat: - return - try: - bot = self._needed_markup_bot() - except ValueError: - await self._reload_message() - bot = self._needed_markup_bot() # TODO use via_input_bot - - self._set_buttons(chat, bot) - - return self._buttons - - @property - def button_count(self): - """ - Returns the total button count (sum of all `buttons` rows). - """ - if self._buttons_count is None: - if isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - self._buttons_count = sum( - len(row.buttons) for row in self.reply_markup.rows) - else: - self._buttons_count = 0 - - return self._buttons_count - - @property - def file(self): - """ - Returns a `File ` wrapping the - `photo` or `document` in this message. If the media type is different - (polls, games, none, etc.), this property will be `None`. - - This instance lets you easily access other properties, such as - `file.id `, - `file.name `, - etc., without having to manually inspect the ``document.attributes``. - """ - if not self._file: - media = self.photo or self.document - if media: - self._file = File(media) - - return self._file - - @property - def photo(self): - """ - The :tl:`Photo` media in this message, if any. - - This will also return the photo for :tl:`MessageService` if its - action is :tl:`MessageActionChatEditPhoto`, or if the message has - a web preview with a photo. - """ - if isinstance(self.media, types.MessageMediaPhoto): - if isinstance(self.media.photo, types.Photo): - return self.media.photo - elif isinstance(self.action, types.MessageActionChatEditPhoto): - return self.action.photo - else: - web = self.web_preview - if web and isinstance(web.photo, types.Photo): - return web.photo - - @property - def document(self): - """ - The :tl:`Document` media in this message, if any. - """ - if isinstance(self.media, types.MessageMediaDocument): - if isinstance(self.media.document, types.Document): - return self.media.document - else: - web = self.web_preview - if web and isinstance(web.document, types.Document): - return web.document - - @property - def web_preview(self): - """ - The :tl:`WebPage` media in this message, if any. - """ - if isinstance(self.media, types.MessageMediaWebPage): - if isinstance(self.media.webpage, types.WebPage): - return self.media.webpage - - @property - def audio(self): - """ - The :tl:`Document` media in this message, if it's an audio file. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: not attr.voice) - - @property - def voice(self): - """ - The :tl:`Document` media in this message, if it's a voice note. - """ - return self._document_by_attribute(types.DocumentAttributeAudio, - lambda attr: attr.voice) - - @property - def video(self): - """ - The :tl:`Document` media in this message, if it's a video. - """ - return self._document_by_attribute(types.DocumentAttributeVideo) - - @property - def video_note(self): - """ - The :tl:`Document` media in this message, if it's a video note. - """ - return self._document_by_attribute(types.DocumentAttributeVideo, - lambda attr: attr.round_message) - - @property - def gif(self): - """ - The :tl:`Document` media in this message, if it's a "gif". - - "Gif" files by Telegram are normally ``.mp4`` video files without - sound, the so called "animated" media. However, it may be the actual - gif format if the file is too large. - """ - return self._document_by_attribute(types.DocumentAttributeAnimated) - - @property - def sticker(self): - """ - The :tl:`Document` media in this message, if it's a sticker. - """ - return self._document_by_attribute(types.DocumentAttributeSticker) - - @property - def contact(self): - """ - The :tl:`MessageMediaContact` in this message, if it's a contact. - """ - if isinstance(self.media, types.MessageMediaContact): - return self.media - - @property - def game(self): - """ - The :tl:`Game` media in this message, if it's a game. - """ - if isinstance(self.media, types.MessageMediaGame): - return self.media.game - - @property - def geo(self): - """ - The :tl:`GeoPoint` media in this message, if it has a location. - """ - if isinstance(self.media, (types.MessageMediaGeo, - types.MessageMediaGeoLive, - types.MessageMediaVenue)): - return self.media.geo - - @property - def invoice(self): - """ - The :tl:`MessageMediaInvoice` in this message, if it's an invoice. - """ - if isinstance(self.media, types.MessageMediaInvoice): - return self.media - - @property - def poll(self): - """ - The :tl:`MessageMediaPoll` in this message, if it's a poll. - """ - if isinstance(self.media, types.MessageMediaPoll): - return self.media - - @property - def venue(self): - """ - The :tl:`MessageMediaVenue` in this message, if it's a venue. - """ - if isinstance(self.media, types.MessageMediaVenue): - return self.media - - @property - def dice(self): - """ - The :tl:`MessageMediaDice` in this message, if it's a dice roll. - """ - if isinstance(self.media, types.MessageMediaDice): - return self.media - - @property - def action_entities(self): - """ - Returns a list of entities that took part in this action. - - Possible cases for this are :tl:`MessageActionChatAddUser`, - :tl:`types.MessageActionChatCreate`, :tl:`MessageActionChatDeleteUser`, - :tl:`MessageActionChatJoinedByLink` :tl:`MessageActionChatMigrateTo` - and :tl:`MessageActionChannelMigrateFrom`. - - If the action is neither of those, the result will be `None`. - If some entities could not be retrieved, the list may contain - some `None` items in it. - """ - return self._action_entities - - @property - def via_bot(self): - """ - The bot :tl:`User` if the message was sent via said bot. - - This will only be present if `via_bot_id` is not `None` and - the entity is known. - """ - return self._via_bot - - @property - def via_input_bot(self): - """ - Returns the input variant of `via_bot`. - """ - return self._via_input_bot - - @property - def reply_to_msg_id(self): - """ - Returns the message ID this message is replying to, if any. - This is equivalent to accessing ``.reply_to.reply_to_msg_id``. - """ - return self.reply_to.reply_to_msg_id if self.reply_to else None - - @property - def to_id(self): - """ - Returns the peer to which this message was sent to. This used to exist - to infer the ``.peer_id``. - """ - # If the client wasn't set we can't emulate the behaviour correctly, - # so as a best-effort simply return the chat peer. - if self._client and not self.out and self.is_private: - return types.PeerUser(self._client._self_id) - - return self.peer_id - - # endregion Public Properties - - # region Public Methods - - def get_entities_text(self, cls=None): - """ - Returns a list of ``(markup entity, inner text)`` - (like bold or italics). - - The markup entity is a :tl:`MessageEntity` that represents bold, - italics, etc., and the inner text is the `str` inside that markup - entity. - - For example: - - .. code-block:: python - - print(repr(message.text)) # shows: 'Hello **world**!' - - for ent, txt in message.get_entities_text(): - print(ent) # shows: MessageEntityBold(offset=6, length=5) - print(txt) # shows: world - - Args: - cls (`type`): - Returns entities matching this type only. For example, - the following will print the text for all ``code`` entities: - - >>> from telethon.tl.types import MessageEntityCode - >>> - >>> m = ... # get the message - >>> for _, inner_text in m.get_entities_text(MessageEntityCode): - >>> print(inner_text) - """ - ent = self.entities - if not ent: - return [] - - if cls: - ent = [c for c in ent if isinstance(c, cls)] - - texts = utils.get_inner_text(self.message, ent) - return list(zip(ent, texts)) - - async def get_reply_message(self): - """ - The `Message` that this message is replying to, or `None`. - - The result will be cached after its first use. - """ - if self._reply_message is None and self._client: - if not self.reply_to: - return None - - # Bots cannot access other bots' messages by their ID. - # However they can access them through replies... - self._reply_message = await self._client.get_messages( - await self.get_input_chat() if self.is_channel else None, - ids=types.InputMessageReplyTo(self.id) - ) - if not self._reply_message: - # ...unless the current message got deleted. - # - # If that's the case, give it a second chance accessing - # directly by its ID. - self._reply_message = await self._client.get_messages( - self._input_chat if self.is_channel else None, - ids=self.reply_to.reply_to_msg_id - ) - - return self._reply_message - - async def respond(self, *args, **kwargs): - """ - Responds to the message (not as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with ``entity`` already set. - """ - if self._client: - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def reply(self, *args, **kwargs): - """ - Replies to the message (as a reply). Shorthand for - `telethon.client.messages.MessageMethods.send_message` - with both ``entity`` and ``reply_to`` already set. - """ - if self._client: - kwargs['reply_to'] = self.id - return await self._client.send_message( - await self.get_input_chat(), *args, **kwargs) - - async def forward_to(self, *args, **kwargs): - """ - Forwards the message. Shorthand for - `telethon.client.messages.MessageMethods.forward_messages` - with both ``messages`` and ``from_peer`` already set. - - If you need to forward more than one message at once, don't use - this `forward_to` method. Use a - `telethon.client.telegramclient.TelegramClient` instance directly. - """ - if self._client: - kwargs['messages'] = self.id - kwargs['from_peer'] = await self.get_input_chat() - return await self._client.forward_messages(*args, **kwargs) - - async def edit(self, *args, **kwargs): - """ - Edits the message 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: - # Passing the entire message is important, in case it has to be - # refetched for a fresh file reference. - return await self._client.download_media(self, *args, **kwargs) - - async def click(self, i=None, j=None, - *, text=None, filter=None, data=None, share_phone=None, - share_geo=None, password=None): - """ - Calls :tl:`SendVote` with the specified poll option - or `button.click ` - on the specified button. - - Does nothing if the message is not a poll or has no buttons. - - Args: - i (`int` | `list`): - Clicks the i'th button or poll option (starting from the index 0). - For multiple-choice polls, a list with the indices should be used. - Will ``raise IndexError`` if out of bounds. Example: - - >>> message = ... # get the message somehow - >>> # Clicking the 3rd button - >>> # [button1] [button2] - >>> # [ button3 ] - >>> # [button4] [button5] - >>> await message.click(2) # index - - j (`int`): - Clicks the button at position (i, j), these being the - indices for the (row, column) respectively. Example: - - >>> # Clicking the 2nd button on the 1st row. - >>> # [button1] [button2] - >>> # [ button3 ] - >>> # [button4] [button5] - >>> await message.click(0, 1) # (row, column) - - This is equivalent to ``message.buttons[0][1].click()``. - - text (`str` | `callable`): - Clicks the first button or poll option with the text "text". This may - also be a callable, like a ``re.compile(...).match``, - and the text will be passed to it. - - If you need to select multiple options in a poll, - pass a list of indices to the ``i`` parameter. - - filter (`callable`): - Clicks the first button or poll option for which the callable - returns `True`. The callable should accept a single - `MessageButton ` - or `PollAnswer ` argument. - - If you need to select multiple options in a poll, - pass a list of indices to the ``i`` parameter. - - data (`bytes`): - This argument overrides the rest and will not search any - buttons. Instead, it will directly send the request to - behave as if it clicked a button with said data. Note - that if the message does not have this data, it will - ``raise DataInvalidError``. - - share_phone (`bool` | `str` | tl:`InputMediaContact`): - When clicking on a keyboard button requesting a phone number - (:tl:`KeyboardButtonRequestPhone`), this argument must be - explicitly set to avoid accidentally sharing the number. - - It can be `True` to automatically share the current user's - phone, a string to share a specific phone number, or a contact - media to specify all details. - - If the button is pressed without this, `ValueError` is raised. - - share_geo (`tuple` | `list` | tl:`InputMediaGeoPoint`): - When clicking on a keyboard button requesting a geo location - (:tl:`KeyboardButtonRequestGeoLocation`), this argument must - be explicitly set to avoid accidentally sharing the location. - - It must be a `tuple` of `float` as ``(longitude, latitude)``, - or a :tl:`InputGeoPoint` instance to avoid accidentally using - the wrong roder. - - If the button is pressed without this, `ValueError` is raised. - - password (`str`): - When clicking certain buttons (such as BotFather's confirmation - button to transfer ownership), if your account has 2FA enabled, - you need to provide your account's password. Otherwise, - `teltehon.errors.PasswordHashInvalidError` is raised. - - Example: - - .. code-block:: python - - # Click the first button - await message.click(0) - - # Click some row/column - await message.click(row, column) - - # Click by text - await message.click(text='👍') - - # Click by data - await message.click(data=b'payload') - - # Click on a button requesting a phone - await message.click(0, share_phone=True) - """ - if not self._client: - return - - if data: - chat = await self.get_input_chat() - if not chat: - return None - - but = types.KeyboardButtonCallback('', data) - return await MessageButton(self._client, but, chat, None, self.id).click( - share_phone=share_phone, share_geo=share_geo, password=password) - - if sum(int(x is not None) for x in (i, text, filter)) >= 2: - raise ValueError('You can only set either of i, text or filter') - - # Finding the desired poll options and sending them - if self.poll is not None: - def find_options(): - answers = self.poll.poll.answers - if i is not None: - if utils.is_list_like(i): - return [answers[idx].option for idx in i] - return [answers[i].option] - if text is not None: - if callable(text): - for answer in answers: - if text(answer.text): - return [answer.option] - else: - for answer in answers: - if answer.text == text: - return [answer.option] - return - - if filter is not None: - for answer in answers: - if filter(answer): - return [answer.option] - return - - options = find_options() - if options is None: - options = [] - return await self._client( - functions.messages.SendVoteRequest( - peer=self._input_chat, - msg_id=self.id, - options=options - ) - ) - - if not await self.get_buttons(): - return # Accessing the property sets self._buttons[_flat] - - def find_button(): - nonlocal i - if text is not None: - if callable(text): - for button in self._buttons_flat: - if text(button.text): - return button - else: - for button in self._buttons_flat: - if button.text == text: - return button - return - - if filter is not None: - for button in self._buttons_flat: - if filter(button): - return button - return - - if i is None: - i = 0 - if j is None: - return self._buttons_flat[i] - else: - return self._buttons[i][j] - - button = find_button() - if button: - return await button.click( - share_phone=share_phone, share_geo=share_geo, password=password) - - async def mark_read(self): - """ - Marks the message as read. Shorthand for - `client.send_read_acknowledge() - ` - with both ``entity`` and ``message`` already set. - """ - if self._client: - await self._client.send_read_acknowledge( - await self.get_input_chat(), max_id=self.id) - - async def pin(self, *, notify=False, pm_oneside=False): - """ - Pins the message. Shorthand for - `telethon.client.messages.MessageMethods.pin_message` - with both ``entity`` and ``message`` already set. - """ - # TODO Constantly checking if client is a bit annoying, - # maybe just make it illegal to call messages from raw API? - # That or figure out a way to always set it directly. - if self._client: - return await self._client.pin_message( - await self.get_input_chat(), self.id, notify=notify, pm_oneside=pm_oneside) - - async def unpin(self): - """ - Unpins the message. Shorthand for - `telethon.client.messages.MessageMethods.unpin_message` - with both ``entity`` and ``message`` already set. - """ - if self._client: - return await self._client.unpin_message( - await self.get_input_chat(), self.id) - - # endregion Public Methods - - # region Private Methods - - async def _reload_message(self): - """ - Re-fetches this message to reload the sender and chat entities, - along with their input versions. - """ - if not self._client: - return - - try: - chat = await self.get_input_chat() if self.is_channel else None - msg = await self._client.get_messages(chat, ids=self.id) - except ValueError: - return # We may not have the input chat/get message failed - if not msg: - return # The message may be deleted and it will be None - - self._sender = msg._sender - self._input_sender = msg._input_sender - self._chat = msg._chat - self._input_chat = msg._input_chat - self._via_bot = msg._via_bot - self._via_input_bot = msg._via_input_bot - self._forward = msg._forward - self._action_entities = msg._action_entities - - async def _refetch_sender(self): - await self._reload_message() - - def _set_buttons(self, chat, bot): - """ - Helper methods to set the buttons given the input sender and chat. - """ - if self._client and isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - self._buttons = [[ - MessageButton(self._client, button, chat, bot, self.id) - for button in row.buttons - ] for row in self.reply_markup.rows] - self._buttons_flat = [x for row in self._buttons for x in row] - - def _needed_markup_bot(self): - """ - Returns the input peer of the bot that's needed for the reply markup. - - This is necessary for :tl:`KeyboardButtonSwitchInline` since we need - to know what bot we want to start. Raises ``ValueError`` if the bot - cannot be found but is needed. Returns `None` if it's not needed. - """ - if self._client and not isinstance(self.reply_markup, ( - types.ReplyInlineMarkup, types.ReplyKeyboardMarkup)): - return None - - for row in self.reply_markup.rows: - for button in row.buttons: - if isinstance(button, types.KeyboardButtonSwitchInline): - # no via_bot_id means the bot sent the message itself (#1619) - if button.same_peer or not self.via_bot_id: - bot = self.input_sender - if not bot: - raise ValueError('No input sender') - return bot - else: - try: - return self._client._mb_entity_cache.get( - utils.resolve_id(self.via_bot_id)[0])._as_input_peer() - except AttributeError: - raise ValueError('No input sender') from None - - def _document_by_attribute(self, kind, condition=None): - """ - Helper method to return the document only if it has an attribute - that's an instance of the given kind, and passes the condition. - """ - doc = self.document - if doc: - for attr in doc.attributes: - if isinstance(attr, kind): - if not condition or condition(attr): - return doc - return None - - # endregion Private Methods diff --git a/telethon/tl/custom/messagebutton.py b/telethon/tl/custom/messagebutton.py deleted file mode 100644 index 7f6490b2..00000000 --- a/telethon/tl/custom/messagebutton.py +++ /dev/null @@ -1,146 +0,0 @@ -from .. import types, functions -from ... import password as pwd_mod -from ...errors import BotResponseTimeoutError -import webbrowser -import os - - -class MessageButton: - """ - .. note:: - - `Message.buttons ` - are instances of this type. If you want to **define** a reply - markup for e.g. sending messages, refer to `Button - ` instead. - - Custom class that encapsulates a message button providing - an abstraction to easily access some commonly needed features - (such as clicking the button itself). - - Attributes: - - button (:tl:`KeyboardButton`): - The original :tl:`KeyboardButton` object. - """ - def __init__(self, client, original, chat, bot, msg_id): - self.button = original - self._bot = bot - self._chat = chat - self._msg_id = msg_id - self._client = client - - @property - def client(self): - """ - Returns the `telethon.client.telegramclient.TelegramClient` - instance that created this instance. - """ - return self._client - - @property - def text(self): - """The text string of the button.""" - return self.button.text - - @property - def data(self): - """The `bytes` data for :tl:`KeyboardButtonCallback` objects.""" - if isinstance(self.button, types.KeyboardButtonCallback): - return self.button.data - - @property - def inline_query(self): - """The query `str` for :tl:`KeyboardButtonSwitchInline` objects.""" - if isinstance(self.button, types.KeyboardButtonSwitchInline): - return self.button.query - - @property - def url(self): - """The url `str` for :tl:`KeyboardButtonUrl` objects.""" - if isinstance(self.button, types.KeyboardButtonUrl): - return self.button.url - - async def click(self, share_phone=None, share_geo=None, *, password=None): - """ - Emulates the behaviour of clicking this button. - - If it's a normal :tl:`KeyboardButton` with text, a message will be - sent, and the sent `Message ` returned. - - If it's an inline :tl:`KeyboardButtonCallback` with text and data, - it will be "clicked" and the :tl:`BotCallbackAnswer` returned. - - If it's an inline :tl:`KeyboardButtonSwitchInline` button, the - :tl:`StartBotRequest` will be invoked and the resulting updates - returned. - - If it's a :tl:`KeyboardButtonUrl`, the URL of the button will - be passed to ``webbrowser.open`` and return `True` on success. - - If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you - want to ``share_phone=True`` in order to share it. Sharing it is not a - default because it is a privacy concern and could happen accidentally. - - You may also use ``share_phone=phone`` to share a specific number, in - which case either `str` or :tl:`InputMediaContact` should be used. - - If it's a :tl:`KeyboardButtonRequestGeoLocation`, you must pass a - tuple in ``share_geo=(longitude, latitude)``. Note that Telegram seems - to have some heuristics to determine impossible locations, so changing - this value a lot quickly may not work as expected. You may also pass a - :tl:`InputGeoPoint` if you find the order confusing. - """ - if isinstance(self.button, types.KeyboardButton): - return await self._client.send_message( - self._chat, self.button.text, parse_mode=None) - elif isinstance(self.button, types.KeyboardButtonCallback): - if password is not None: - pwd = await self._client(functions.account.GetPasswordRequest()) - password = pwd_mod.compute_check(pwd, password) - - req = functions.messages.GetBotCallbackAnswerRequest( - peer=self._chat, msg_id=self._msg_id, data=self.button.data, - password=password - ) - try: - return await self._client(req) - except BotResponseTimeoutError: - return None - elif isinstance(self.button, types.KeyboardButtonSwitchInline): - return await self._client(functions.messages.StartBotRequest( - bot=self._bot, peer=self._chat, start_param=self.button.query - )) - elif isinstance(self.button, types.KeyboardButtonUrl): - return webbrowser.open(self.button.url) - elif isinstance(self.button, types.KeyboardButtonGame): - req = functions.messages.GetBotCallbackAnswerRequest( - peer=self._chat, msg_id=self._msg_id, game=True - ) - try: - return await self._client(req) - except BotResponseTimeoutError: - return None - elif isinstance(self.button, types.KeyboardButtonRequestPhone): - if not share_phone: - raise ValueError('cannot click on phone buttons unless share_phone=True') - - if share_phone == True or isinstance(share_phone, str): - me = await self._client.get_me() - share_phone = types.InputMediaContact( - phone_number=me.phone if share_phone == True else share_phone, - first_name=me.first_name or '', - last_name=me.last_name or '', - vcard='' - ) - - return await self._client.send_file(self._chat, share_phone) - elif isinstance(self.button, types.KeyboardButtonRequestGeoLocation): - if not share_geo: - raise ValueError('cannot click on geo buttons unless share_geo=(longitude, latitude)') - - if isinstance(share_geo, (tuple, list)): - long, lat = share_geo - share_geo = types.InputMediaGeoPoint(types.InputGeoPoint(lat=lat, long=long)) - - return await self._client.send_file(self._chat, share_geo) diff --git a/telethon/tl/custom/participantpermissions.py b/telethon/tl/custom/participantpermissions.py deleted file mode 100644 index d3719778..00000000 --- a/telethon/tl/custom/participantpermissions.py +++ /dev/null @@ -1,138 +0,0 @@ -from .. import types - - -def _admin_prop(field_name, doc): - """ - Helper method to build properties that return `True` if the user is an - administrator of a normal chat, or otherwise return `True` if the user - has a specific permission being an admin of a channel. - """ - def fget(self): - if not self.is_admin: - return False - if self.is_chat: - return True - - return getattr(self.participant.admin_rights, field_name) - - return {'fget': fget, 'doc': doc} - - -class ParticipantPermissions: - """ - Participant permissions information. - - The properties in this objects are boolean values indicating whether the - user has the permission or not. - - Example - .. code-block:: python - - permissions = ... - - if permissions.is_banned: - "this user is banned" - elif permissions.is_admin: - "this user is an administrator" - """ - def __init__(self, participant, chat: bool): - self.participant = participant - self.is_chat = chat - - @property - def is_admin(self): - """ - Whether the user is an administrator of the chat or not. The creator - also counts as begin an administrator, since they have all permissions. - """ - return self.is_creator or isinstance(self.participant, ( - types.ChannelParticipantAdmin, - types.ChatParticipantAdmin - )) - - @property - def is_creator(self): - """ - Whether the user is the creator of the chat or not. - """ - return isinstance(self.participant, ( - types.ChannelParticipantCreator, - types.ChatParticipantCreator - )) - - @property - def has_default_permissions(self): - """ - Whether the user is a normal user of the chat (not administrator, but - not banned either, and has no restrictions applied). - """ - return isinstance(self.participant, ( - types.ChannelParticipant, - types.ChatParticipant, - types.ChannelParticipantSelf - )) - - @property - def is_banned(self): - """ - Whether the user is banned in the chat. - """ - return isinstance(self.participant, types.ChannelParticipantBanned) - - @property - def has_left(self): - """ - Whether the user left the chat. - """ - return isinstance(self.participant, types.ChannelParticipantLeft) - - @property - def add_admins(self): - """ - Whether the administrator can add new administrators with the same or - less permissions than them. - """ - if not self.is_admin: - return False - - if self.is_chat: - return self.is_creator - - return self.participant.admin_rights.add_admins - - ban_users = property(**_admin_prop('ban_users', """ - Whether the administrator can ban other users or not. - """)) - - pin_messages = property(**_admin_prop('pin_messages', """ - Whether the administrator can pin messages or not. - """)) - - invite_users = property(**_admin_prop('invite_users', """ - Whether the administrator can add new users to the chat. - """)) - - delete_messages = property(**_admin_prop('delete_messages', """ - Whether the administrator can delete messages from other participants. - """)) - - edit_messages = property(**_admin_prop('edit_messages', """ - Whether the administrator can edit messages. - """)) - - post_messages = property(**_admin_prop('post_messages', """ - Whether the administrator can post messages in the broadcast channel. - """)) - - change_info = property(**_admin_prop('change_info', """ - Whether the administrator can change the information about the chat, - such as title or description. - """)) - - anonymous = property(**_admin_prop('anonymous', """ - Whether the administrator will remain anonymous when sending messages. - """)) - - manage_call = property(**_admin_prop('manage_call', """ - Whether the user will be able to manage group calls. - """)) diff --git a/telethon/tl/custom/qrlogin.py b/telethon/tl/custom/qrlogin.py deleted file mode 100644 index cd540274..00000000 --- a/telethon/tl/custom/qrlogin.py +++ /dev/null @@ -1,119 +0,0 @@ -import asyncio -import base64 -import datetime - -from .. import types, functions -from ... import events - - -class QRLogin: - """ - QR login information. - - Most of the time, you will present the `url` as a QR code to the user, - and while it's being shown, call `wait`. - """ - def __init__(self, client, ignored_ids): - self._client = client - self._request = functions.auth.ExportLoginTokenRequest( - self._client.api_id, self._client.api_hash, ignored_ids) - self._resp = None - - async def recreate(self): - """ - Generates a new token and URL for a new QR code, useful if the code - has expired before it was imported. - """ - self._resp = await self._client(self._request) - - @property - def token(self) -> bytes: - """ - The binary data representing the token. - - It can be used by a previously-authorized client in a call to - :tl:`auth.importLoginToken` to log the client that originally - requested the QR login. - """ - return self._resp.token - - @property - def url(self) -> str: - """ - The ``tg://login`` URI with the token. When opened by a Telegram - application where the user is logged in, it will import the login - token. - - If you want to display a QR code to the user, this is the URL that - should be launched when the QR code is scanned (the URL that should - be contained in the QR code image you generate). - - Whether you generate the QR code image or not is up to you, and the - library can't do this for you due to the vast ways of generating and - displaying the QR code that exist. - - The URL simply consists of `token` base64-encoded. - """ - return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('=')) - - @property - def expires(self) -> datetime.datetime: - """ - The `datetime` at which the QR code will expire. - - If you want to try again, you will need to call `recreate`. - """ - return self._resp.expires - - async def wait(self, timeout: float = None): - """ - Waits for the token to be imported by a previously-authorized client, - either by scanning the QR, launching the URL directly, or calling the - import method. - - This method **must** be called before the QR code is scanned, and - must be executing while the QR code is being scanned. Otherwise, the - login will not complete. - - Will raise `asyncio.TimeoutError` if the login doesn't complete on - time. - - Arguments - timeout (float): - The timeout, in seconds, to wait before giving up. By default - the library will wait until the token expires, which is often - what you want. - - Returns - On success, an instance of :tl:`User`. On failure it will raise. - """ - if timeout is None: - timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds() - - event = asyncio.Event() - - async def handler(_update): - event.set() - - self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken)) - - try: - # Will raise timeout error if it doesn't complete quick enough, - # which we want to let propagate - await asyncio.wait_for(event.wait(), timeout=timeout) - finally: - self._client.remove_event_handler(handler) - - # We got here without it raising timeout error, so we can proceed - resp = await self._client(self._request) - if isinstance(resp, types.auth.LoginTokenMigrateTo): - await self._client._switch_dc(resp.dc_id) - resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token)) - # resp should now be auth.loginTokenSuccess - - if isinstance(resp, types.auth.LoginTokenSuccess): - user = resp.authorization.user - await self._client._on_login(user) - return user - - raise TypeError('Login token response was unexpected: {}'.format(resp)) diff --git a/telethon/tl/custom/sendergetter.py b/telethon/tl/custom/sendergetter.py deleted file mode 100644 index 7d389ec7..00000000 --- a/telethon/tl/custom/sendergetter.py +++ /dev/null @@ -1,102 +0,0 @@ -import abc - -from ... import utils - - -class SenderGetter(abc.ABC): - """ - Helper base class that introduces the `sender`, `input_sender` - and `sender_id` properties and `get_sender` and `get_input_sender` - methods. - """ - def __init__(self, sender_id=None, *, sender=None, input_sender=None): - self._sender_id = sender_id - self._sender = sender - self._input_sender = input_sender - self._client = None - - @property - def sender(self): - """ - Returns the :tl:`User` or :tl:`Channel` that sent this object. - It may be `None` if Telegram didn't send the sender. - - If you only need the ID, use `sender_id` instead. - - If you need to call a method which needs - this chat, use `input_sender` instead. - - If you're using `telethon.events`, use `get_sender()` instead. - """ - return self._sender - - async def get_sender(self): - """ - Returns `sender`, but will make an API call to find the - sender unless it's already cached. - - If you only need the ID, use `sender_id` instead. - - If you need to call a method which needs - this sender, use `get_input_sender()` instead. - """ - # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. - # It's a flag that will be set if only minimal information is - # available (such as display name, but username may be missing), - # in which case we want to force fetch the entire thing because - # the user explicitly called a method. If the user is okay with - # cached information, they may use the property instead. - if (self._sender is None or getattr(self._sender, 'min', None)) \ - and await self.get_input_sender(): - # self.get_input_sender may refresh in which case the sender may no longer be min - # However it could still incur a cost so the cheap check is done twice instead. - if self._sender is None or getattr(self._sender, 'min', None): - try: - self._sender =\ - await self._client.get_entity(self._input_sender) - except ValueError: - await self._refetch_sender() - return self._sender - - @property - def input_sender(self): - """ - This :tl:`InputPeer` is the input version of the user/channel who - sent the message. Similarly to `input_chat - `, this doesn't - have things like username or similar, but still useful in some cases. - - Note that this might not be available if the library can't - find the input chat, or if the message a broadcast on a channel. - """ - if self._input_sender is None and self._sender_id and self._client: - try: - self._input_sender = self._client._mb_entity_cache.get( - utils.resolve_id(self._sender_id)[0])._as_input_peer() - except AttributeError: - pass - return self._input_sender - - async def get_input_sender(self): - """ - Returns `input_sender`, but will make an API call to find the - input sender unless it's already cached. - """ - if self.input_sender is None and self._sender_id and self._client: - await self._refetch_sender() - return self._input_sender - - @property - def sender_id(self): - """ - Returns the marked sender integer ID, if present. - - If there is a sender in the object, `sender_id` will *always* be set, - which is why you should use it instead of `sender.id `. - """ - return self._sender_id - - async def _refetch_sender(self): - """ - Re-fetches sender information through other means. - """ diff --git a/telethon/tl/patched/__init__.py b/telethon/tl/patched/__init__.py deleted file mode 100644 index 2951f2af..00000000 --- a/telethon/tl/patched/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from .. import types, alltlobjects -from ..custom.message import Message as _Message - -class MessageEmpty(_Message, types.MessageEmpty): - pass - -types.MessageEmpty = MessageEmpty -alltlobjects.tlobjects[MessageEmpty.CONSTRUCTOR_ID] = MessageEmpty - -class MessageService(_Message, types.MessageService): - pass - -types.MessageService = MessageService -alltlobjects.tlobjects[MessageService.CONSTRUCTOR_ID] = MessageService - -class Message(_Message, types.Message): - pass - -types.Message = Message -alltlobjects.tlobjects[Message.CONSTRUCTOR_ID] = Message diff --git a/telethon/tl/tlobject.py b/telethon/tl/tlobject.py deleted file mode 100644 index 4b94e00f..00000000 --- a/telethon/tl/tlobject.py +++ /dev/null @@ -1,222 +0,0 @@ -import base64 -import json -import struct -from datetime import datetime, date, timedelta, timezone -import time - -_EPOCH_NAIVE = datetime(*time.gmtime(0)[:6]) -_EPOCH_NAIVE_LOCAL = datetime(*time.localtime(0)[:6]) -_EPOCH = _EPOCH_NAIVE.replace(tzinfo=timezone.utc) - - -def _datetime_to_timestamp(dt): - # If no timezone is specified, it is assumed to be in utc zone - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - # We use .total_seconds() method instead of simply dt.timestamp(), - # because on Windows the latter raises OSError on datetimes ~< datetime(1970,1,1) - secs = int((dt - _EPOCH).total_seconds()) - # Make sure it's a valid signed 32 bit integer, as used by Telegram. - # This does make very large dates wrap around, but it's the best we - # can do with Telegram's limitations. - return struct.unpack('i', struct.pack('I', secs & 0xffffffff))[0] - - -def _json_default(value): - if isinstance(value, bytes): - return base64.b64encode(value).decode('ascii') - elif isinstance(value, datetime): - return value.isoformat() - else: - return repr(value) - - -class TLObject: - CONSTRUCTOR_ID = None - SUBCLASS_OF_ID = None - - @staticmethod - def pretty_format(obj, indent=None): - """ - Pretty formats the given object as a string which is returned. - If indent is None, a single line will be returned. - """ - if indent is None: - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - return '{}({})'.format(obj.get('_', 'dict'), ', '.join( - '{}={}'.format(k, TLObject.pretty_format(v)) - for k, v in obj.items() if k != '_' - )) - elif isinstance(obj, str) or isinstance(obj, bytes): - return repr(obj) - elif hasattr(obj, '__iter__'): - return '[{}]'.format( - ', '.join(TLObject.pretty_format(x) for x in obj) - ) - else: - return repr(obj) - else: - result = [] - if isinstance(obj, TLObject): - obj = obj.to_dict() - - if isinstance(obj, dict): - result.append(obj.get('_', 'dict')) - result.append('(') - if obj: - result.append('\n') - indent += 1 - for k, v in obj.items(): - if k == '_': - continue - result.append('\t' * indent) - result.append(k) - result.append('=') - result.append(TLObject.pretty_format(v, indent)) - result.append(',\n') - result.pop() # last ',\n' - indent -= 1 - result.append('\n') - result.append('\t' * indent) - result.append(')') - - elif isinstance(obj, str) or isinstance(obj, bytes): - result.append(repr(obj)) - - elif hasattr(obj, '__iter__'): - result.append('[\n') - indent += 1 - for x in obj: - result.append('\t' * indent) - result.append(TLObject.pretty_format(x, indent)) - result.append(',\n') - indent -= 1 - result.append('\t' * indent) - result.append(']') - - else: - result.append(repr(obj)) - - return ''.join(result) - - @staticmethod - def serialize_bytes(data): - """Write bytes by using Telegram guidelines""" - if not isinstance(data, bytes): - if isinstance(data, str): - data = data.encode('utf-8') - else: - raise TypeError( - 'bytes or str expected, not {}'.format(type(data))) - - r = [] - if len(data) < 254: - padding = (len(data) + 1) % 4 - if padding != 0: - padding = 4 - padding - - r.append(bytes([len(data)])) - r.append(data) - - else: - padding = len(data) % 4 - if padding != 0: - padding = 4 - padding - - r.append(bytes([ - 254, - len(data) % 256, - (len(data) >> 8) % 256, - (len(data) >> 16) % 256 - ])) - r.append(data) - - r.append(bytes(padding)) - return b''.join(r) - - @staticmethod - def serialize_datetime(dt): - if not dt and not isinstance(dt, timedelta): - return b'\0\0\0\0' - - if isinstance(dt, datetime): - dt = _datetime_to_timestamp(dt) - elif isinstance(dt, date): - dt = _datetime_to_timestamp(datetime(dt.year, dt.month, dt.day)) - elif isinstance(dt, float): - dt = int(dt) - elif isinstance(dt, timedelta): - # Timezones are tricky. datetime.utcnow() + ... timestamp() works - dt = _datetime_to_timestamp(datetime.utcnow() + dt) - - if isinstance(dt, int): - return struct.pack(' We send new access_hash for Channel with min flag since layer 102. - # > Previously, we omitted it. - # > That one works just to download the profile picture. - # - # < So, min hashes only work for getting files, - # < but the non-min hash is required for any other operation? - # - # > Yes. - # - # More information: https://core.telegram.org/api/min - try: - if entity.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return entity - except AttributeError: - # e.g. custom.Dialog (can't cyclic import). - if allow_self and hasattr(entity, 'input_entity'): - return entity.input_entity - elif hasattr(entity, 'entity'): - return get_input_peer(entity.entity) - else: - _raise_cast_fail(entity, 'InputPeer') - - if isinstance(entity, types.User): - if entity.is_self and allow_self: - return types.InputPeerSelf() - elif (entity.access_hash is not None and not entity.min) or not check_hash: - return types.InputPeerUser(entity.id, entity.access_hash) - else: - raise TypeError('User without access_hash or min info cannot be input') - - if isinstance(entity, (types.Chat, types.ChatEmpty, types.ChatForbidden)): - return types.InputPeerChat(entity.id) - - if isinstance(entity, types.Channel): - if (entity.access_hash is not None and not entity.min) or not check_hash: - return types.InputPeerChannel(entity.id, entity.access_hash) - else: - raise TypeError('Channel without access_hash or min info cannot be input') - if isinstance(entity, types.ChannelForbidden): - # "channelForbidden are never min", and since their hash is - # also not optional, we assume that this truly is the case. - return types.InputPeerChannel(entity.id, entity.access_hash) - - if isinstance(entity, types.InputUser): - return types.InputPeerUser(entity.user_id, entity.access_hash) - - if isinstance(entity, types.InputChannel): - return types.InputPeerChannel(entity.channel_id, entity.access_hash) - - if isinstance(entity, types.InputUserSelf): - return types.InputPeerSelf() - - if isinstance(entity, types.InputUserFromMessage): - return types.InputPeerUserFromMessage(entity.peer, entity.msg_id, entity.user_id) - - if isinstance(entity, types.InputChannelFromMessage): - return types.InputPeerChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) - - if isinstance(entity, types.UserEmpty): - return types.InputPeerEmpty() - - if isinstance(entity, types.UserFull): - return get_input_peer(entity.user) - - if isinstance(entity, types.ChatFull): - return types.InputPeerChat(entity.id) - - if isinstance(entity, types.PeerChat): - return types.InputPeerChat(entity.chat_id) - - _raise_cast_fail(entity, 'InputPeer') - - -def get_input_channel(entity): - """ - Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone. - - .. important:: - - This method does not validate for invalid general-purpose access - hashes, unlike `get_input_peer`. Consider using instead: - ``get_input_channel(get_input_peer(channel))``. - """ - try: - if entity.SUBCLASS_OF_ID == 0x40f202fd: # crc32(b'InputChannel') - return entity - except AttributeError: - _raise_cast_fail(entity, 'InputChannel') - - if isinstance(entity, (types.Channel, types.ChannelForbidden)): - return types.InputChannel(entity.id, entity.access_hash or 0) - - if isinstance(entity, types.InputPeerChannel): - return types.InputChannel(entity.channel_id, entity.access_hash) - - if isinstance(entity, types.InputPeerChannelFromMessage): - return types.InputChannelFromMessage(entity.peer, entity.msg_id, entity.channel_id) - - _raise_cast_fail(entity, 'InputChannel') - - -def get_input_user(entity): - """ - Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone. - - .. important:: - - This method does not validate for invalid general-purpose access - hashes, unlike `get_input_peer`. Consider using instead: - ``get_input_channel(get_input_peer(channel))``. - """ - try: - if entity.SUBCLASS_OF_ID == 0xe669bf46: # crc32(b'InputUser'): - return entity - except AttributeError: - _raise_cast_fail(entity, 'InputUser') - - if isinstance(entity, types.User): - if entity.is_self: - return types.InputUserSelf() - else: - return types.InputUser(entity.id, entity.access_hash or 0) - - if isinstance(entity, types.InputPeerSelf): - return types.InputUserSelf() - - if isinstance(entity, (types.UserEmpty, types.InputPeerEmpty)): - return types.InputUserEmpty() - - if isinstance(entity, types.UserFull): - return get_input_user(entity.user) - - if isinstance(entity, types.InputPeerUser): - return types.InputUser(entity.user_id, entity.access_hash) - - if isinstance(entity, types.InputPeerUserFromMessage): - return types.InputUserFromMessage(entity.peer, entity.msg_id, entity.user_id) - - _raise_cast_fail(entity, 'InputUser') - - -def get_input_dialog(dialog): - """Similar to :meth:`get_input_peer`, but for dialogs""" - try: - if dialog.SUBCLASS_OF_ID == 0xa21c9795: # crc32(b'InputDialogPeer') - return dialog - if dialog.SUBCLASS_OF_ID == 0xc91c90b6: # crc32(b'InputPeer') - return types.InputDialogPeer(dialog) - except AttributeError: - _raise_cast_fail(dialog, 'InputDialogPeer') - - try: - return types.InputDialogPeer(get_input_peer(dialog)) - except TypeError: - pass - - _raise_cast_fail(dialog, 'InputDialogPeer') - - -def get_input_document(document): - """Similar to :meth:`get_input_peer`, but for documents""" - try: - if document.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument'): - return document - except AttributeError: - _raise_cast_fail(document, 'InputDocument') - - if isinstance(document, types.Document): - return types.InputDocument( - id=document.id, access_hash=document.access_hash, - file_reference=document.file_reference) - - if isinstance(document, types.DocumentEmpty): - return types.InputDocumentEmpty() - - if isinstance(document, types.MessageMediaDocument): - return get_input_document(document.document) - - if isinstance(document, types.Message): - return get_input_document(document.media) - - _raise_cast_fail(document, 'InputDocument') - - -def get_input_photo(photo): - """Similar to :meth:`get_input_peer`, but for photos""" - try: - if photo.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto'): - return photo - except AttributeError: - _raise_cast_fail(photo, 'InputPhoto') - - if isinstance(photo, types.Message): - photo = photo.media - - if isinstance(photo, (types.photos.Photo, types.MessageMediaPhoto)): - photo = photo.photo - - if isinstance(photo, types.Photo): - return types.InputPhoto(id=photo.id, access_hash=photo.access_hash, - file_reference=photo.file_reference) - - if isinstance(photo, types.PhotoEmpty): - return types.InputPhotoEmpty() - - if isinstance(photo, types.messages.ChatFull): - photo = photo.full_chat - - if isinstance(photo, types.ChannelFull): - return get_input_photo(photo.chat_photo) - elif isinstance(photo, types.UserFull): - return get_input_photo(photo.profile_photo) - elif isinstance(photo, (types.Channel, types.Chat, types.User)): - return get_input_photo(photo.photo) - - if isinstance(photo, (types.UserEmpty, types.ChatEmpty, - types.ChatForbidden, types.ChannelForbidden)): - return types.InputPhotoEmpty() - - _raise_cast_fail(photo, 'InputPhoto') - - -def get_input_chat_photo(photo): - """Similar to :meth:`get_input_peer`, but for chat photos""" - try: - if photo.SUBCLASS_OF_ID == 0xd4eb2d74: # crc32(b'InputChatPhoto') - return photo - elif photo.SUBCLASS_OF_ID == 0xe7655f1f: # crc32(b'InputFile'): - return types.InputChatUploadedPhoto(photo) - except AttributeError: - _raise_cast_fail(photo, 'InputChatPhoto') - - photo = get_input_photo(photo) - if isinstance(photo, types.InputPhoto): - return types.InputChatPhoto(photo) - elif isinstance(photo, types.InputPhotoEmpty): - return types.InputChatPhotoEmpty() - - _raise_cast_fail(photo, 'InputChatPhoto') - - -def get_input_geo(geo): - """Similar to :meth:`get_input_peer`, but for geo points""" - try: - if geo.SUBCLASS_OF_ID == 0x430d225: # crc32(b'InputGeoPoint'): - return geo - except AttributeError: - _raise_cast_fail(geo, 'InputGeoPoint') - - if isinstance(geo, types.GeoPoint): - return types.InputGeoPoint(lat=geo.lat, long=geo.long) - - if isinstance(geo, types.GeoPointEmpty): - return types.InputGeoPointEmpty() - - if isinstance(geo, types.MessageMediaGeo): - return get_input_geo(geo.geo) - - if isinstance(geo, types.Message): - return get_input_geo(geo.media) - - _raise_cast_fail(geo, 'InputGeoPoint') - - -def get_input_media( - media, *, - is_photo=False, attributes=None, force_document=False, - voice_note=False, video_note=False, supports_streaming=False, - ttl=None -): - """ - Similar to :meth:`get_input_peer`, but for media. - - If the media is :tl:`InputFile` and ``is_photo`` is known to be `True`, - it will be treated as an :tl:`InputMediaUploadedPhoto`. Else, the rest - of parameters will indicate how to treat it. - """ - try: - if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia') - return media - elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto') - return types.InputMediaPhoto(media, ttl_seconds=ttl) - elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument') - return types.InputMediaDocument(media, ttl_seconds=ttl) - except AttributeError: - _raise_cast_fail(media, 'InputMedia') - - if isinstance(media, types.MessageMediaPhoto): - return types.InputMediaPhoto( - id=get_input_photo(media.photo), - ttl_seconds=ttl or media.ttl_seconds - ) - - if isinstance(media, (types.Photo, types.photos.Photo, types.PhotoEmpty)): - return types.InputMediaPhoto( - id=get_input_photo(media), - ttl_seconds=ttl - ) - - if isinstance(media, types.MessageMediaDocument): - return types.InputMediaDocument( - id=get_input_document(media.document), - ttl_seconds=ttl or media.ttl_seconds - ) - - if isinstance(media, (types.Document, types.DocumentEmpty)): - return types.InputMediaDocument( - id=get_input_document(media), - ttl_seconds=ttl - ) - - if isinstance(media, (types.InputFile, types.InputFileBig)): - if is_photo: - return types.InputMediaUploadedPhoto(file=media, ttl_seconds=ttl) - else: - attrs, mime = get_attributes( - media, - attributes=attributes, - force_document=force_document, - voice_note=voice_note, - video_note=video_note, - supports_streaming=supports_streaming - ) - return types.InputMediaUploadedDocument( - file=media, mime_type=mime, attributes=attrs, force_file=force_document, - ttl_seconds=ttl) - - if isinstance(media, types.MessageMediaGame): - return types.InputMediaGame(id=types.InputGameID( - id=media.game.id, - access_hash=media.game.access_hash - )) - - if isinstance(media, types.MessageMediaContact): - return types.InputMediaContact( - phone_number=media.phone_number, - first_name=media.first_name, - last_name=media.last_name, - vcard='' - ) - - if isinstance(media, types.MessageMediaGeo): - return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo)) - - if isinstance(media, types.MessageMediaVenue): - return types.InputMediaVenue( - geo_point=get_input_geo(media.geo), - title=media.title, - address=media.address, - provider=media.provider, - venue_id=media.venue_id, - venue_type='' - ) - - if isinstance(media, types.MessageMediaDice): - return types.InputMediaDice(media.emoticon) - - if isinstance(media, ( - types.MessageMediaEmpty, types.MessageMediaUnsupported, - types.ChatPhotoEmpty, types.UserProfilePhotoEmpty, - types.ChatPhoto, types.UserProfilePhoto)): - return types.InputMediaEmpty() - - if isinstance(media, types.Message): - return get_input_media(media.media, is_photo=is_photo, ttl=ttl) - - if isinstance(media, types.MessageMediaPoll): - if media.poll.quiz: - if not media.results.results: - # A quiz has correct answers, which we don't know until answered. - # If the quiz hasn't been answered we can't reconstruct it properly. - raise TypeError('Cannot cast unanswered quiz to any kind of InputMedia.') - - correct_answers = [r.option for r in media.results.results if r.correct] - else: - correct_answers = None - - return types.InputMediaPoll( - poll=media.poll, - correct_answers=correct_answers, - solution=media.results.solution, - solution_entities=media.results.solution_entities, - ) - - if isinstance(media, types.Poll): - return types.InputMediaPoll(media) - - _raise_cast_fail(media, 'InputMedia') - - -def get_input_message(message): - """Similar to :meth:`get_input_peer`, but for input messages.""" - try: - if isinstance(message, int): # This case is really common too - return types.InputMessageID(message) - elif message.SUBCLASS_OF_ID == 0x54b6bcc5: # crc32(b'InputMessage'): - return message - elif message.SUBCLASS_OF_ID == 0x790009e3: # crc32(b'Message'): - return types.InputMessageID(message.id) - except AttributeError: - pass - - _raise_cast_fail(message, 'InputMedia') - - -def get_input_group_call(call): - """Similar to :meth:`get_input_peer`, but for input calls.""" - try: - if call.SUBCLASS_OF_ID == 0x58611ab1: # crc32(b'InputGroupCall') - return call - elif call.SUBCLASS_OF_ID == 0x20b4f320: # crc32(b'GroupCall') - return types.InputGroupCall(id=call.id, access_hash=call.access_hash) - except AttributeError: - _raise_cast_fail(call, 'InputGroupCall') - - -def _get_entity_pair(entity_id, entities, cache, - get_input_peer=get_input_peer): - """ - Returns ``(entity, input_entity)`` for the given entity ID. - """ - if not entity_id: - return None, None - - entity = entities.get(entity_id) - try: - input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer() - except AttributeError: - # AttributeError is unlikely, so another TypeError won't hurt - try: - input_entity = get_input_peer(entity) - except TypeError: - input_entity = None - - return entity, input_entity - - -def get_message_id(message): - """Similar to :meth:`get_input_peer`, but for message IDs.""" - if message is None: - return None - - if isinstance(message, int): - return message - - try: - if message.SUBCLASS_OF_ID == 0x790009e3: - # hex(crc32(b'Message')) = 0x790009e3 - return message.id - except AttributeError: - pass - - raise TypeError('Invalid message type: {}'.format(type(message))) - - -def _get_metadata(file): - if not hachoir: - return - - stream = None - close_stream = True - seekable = True - - # The parser may fail and we don't want to crash if - # the extraction process fails. - try: - # Note: aiofiles are intentionally left out for simplicity. - # `helpers._FileStream` is async only for simplicity too, so can't - # reuse it here. - if isinstance(file, str): - stream = open(file, 'rb') - elif isinstance(file, bytes): - stream = io.BytesIO(file) - else: - stream = file - close_stream = False - if getattr(file, 'seekable', None): - seekable = file.seekable() - else: - seekable = False - - if not seekable: - return None - - pos = stream.tell() - filename = getattr(file, 'name', '') - - parser = hachoir.parser.guess.guessParser(hachoir.stream.InputIOStream( - stream, - source='file:' + filename, - tags=[], - filename=filename - )) - - return hachoir.metadata.extractMetadata(parser) - - except Exception as e: - _log.warning('Failed to analyze %s: %s %s', file, e.__class__, e) - - finally: - if stream and close_stream: - stream.close() - elif stream and seekable: - stream.seek(pos) - - -def get_attributes(file, *, attributes=None, mime_type=None, - force_document=False, voice_note=False, video_note=False, - supports_streaming=False, thumb=None): - """ - Get a list of attributes for the given file and - the mime type as a tuple ([attribute], mime_type). - """ - # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` streams - name = file if isinstance(file, str) else getattr(file, 'name', 'unnamed') - if mime_type is None: - mime_type = mimetypes.guess_type(name)[0] - - attr_dict = {types.DocumentAttributeFilename: - types.DocumentAttributeFilename(os.path.basename(name))} - - if is_audio(file): - m = _get_metadata(file) - if m: - if m.has('author'): - performer = m.get('author') - elif m.has('artist'): - performer = m.get('artist') - else: - performer = None - - attr_dict[types.DocumentAttributeAudio] = \ - types.DocumentAttributeAudio( - voice=voice_note, - title=m.get('title') if m.has('title') else None, - performer=performer, - duration=int(m.get('duration').seconds - if m.has('duration') else 0) - ) - - if not force_document and is_video(file): - m = _get_metadata(file) - if m: - doc = types.DocumentAttributeVideo( - round_message=video_note, - w=m.get('width') if m.has('width') else 1, - h=m.get('height') if m.has('height') else 1, - duration=int(m.get('duration').seconds - if m.has('duration') else 1), - supports_streaming=supports_streaming - ) - elif thumb: - t_m = _get_metadata(thumb) - width = 1 - height = 1 - if t_m and t_m.has("width"): - width = t_m.get("width") - if t_m and t_m.has("height"): - height = t_m.get("height") - - doc = types.DocumentAttributeVideo( - 0, width, height, round_message=video_note, - supports_streaming=supports_streaming) - else: - doc = types.DocumentAttributeVideo( - 0, 1, 1, round_message=video_note, - supports_streaming=supports_streaming) - - attr_dict[types.DocumentAttributeVideo] = doc - - if voice_note: - if types.DocumentAttributeAudio in attr_dict: - attr_dict[types.DocumentAttributeAudio].voice = True - else: - attr_dict[types.DocumentAttributeAudio] = \ - types.DocumentAttributeAudio(0, voice=True) - - # Now override the attributes if any. As we have a dict of - # {cls: instance}, we can override any class with the list - # of attributes provided by the user easily. - if attributes: - for a in attributes: - attr_dict[type(a)] = a - - # Ensure we have a mime type, any; but it cannot be None - # 'The "octet-stream" subtype is used to indicate that a body - # contains arbitrary binary data.' - if not mime_type: - mime_type = 'application/octet-stream' - - return list(attr_dict.values()), mime_type - - -def sanitize_parse_mode(mode): - """ - Converts the given parse mode into an object with - ``parse`` and ``unparse`` callable properties. - """ - if not mode: - return None - - if callable(mode): - class CustomMode: - @staticmethod - def unparse(text, entities): - raise NotImplementedError - - CustomMode.parse = mode - return CustomMode - elif (all(hasattr(mode, x) for x in ('parse', 'unparse')) - and all(callable(x) for x in (mode.parse, mode.unparse))): - return mode - elif isinstance(mode, str): - try: - return { - 'md': markdown, - 'markdown': markdown, - 'htm': html, - 'html': html - }[mode.lower()] - except KeyError: - raise ValueError('Unknown parse mode {}'.format(mode)) - else: - raise TypeError('Invalid parse mode type {}'.format(mode)) - - -def get_input_location(location): - """ - Similar to :meth:`get_input_peer`, but for input messages. - - Note that this returns a tuple ``(dc_id, location)``, the - ``dc_id`` being present if known. - """ - info = _get_file_info(location) - return info.dc_id, info.location - - -def _get_file_info(location): - try: - if location.SUBCLASS_OF_ID == 0x1523d462: - return _FileInfo(None, location, None) # crc32(b'InputFileLocation'): - except AttributeError: - _raise_cast_fail(location, 'InputFileLocation') - - if isinstance(location, types.Message): - location = location.media - - if isinstance(location, types.MessageMediaDocument): - location = location.document - elif isinstance(location, types.MessageMediaPhoto): - location = location.photo - - if isinstance(location, types.Document): - return _FileInfo(location.dc_id, types.InputDocumentFileLocation( - id=location.id, - access_hash=location.access_hash, - file_reference=location.file_reference, - thumb_size='' # Presumably to download one of its thumbnails - ), location.size) - elif isinstance(location, types.Photo): - return _FileInfo(location.dc_id, types.InputPhotoFileLocation( - id=location.id, - access_hash=location.access_hash, - file_reference=location.file_reference, - thumb_size=location.sizes[-1].type - ), _photo_size_byte_count(location.sizes[-1])) - - _raise_cast_fail(location, 'InputFileLocation') - - -def _get_extension(file): - """ - Gets the extension for the given file, which can be either a - str or an ``open()``'ed file (which has a ``.name`` attribute). - """ - if isinstance(file, str): - return os.path.splitext(file)[-1] - elif isinstance(file, pathlib.Path): - return file.suffix - elif isinstance(file, bytes): - kind = imghdr.what(io.BytesIO(file)) - return ('.' + kind) if kind else '' - elif isinstance(file, io.IOBase) and not isinstance(file, io.TextIOBase) and file.seekable(): - kind = imghdr.what(file) - return ('.' + kind) if kind is not None else '' - elif getattr(file, 'name', None): - # Note: ``file.name`` works for :tl:`InputFile` and some `IOBase` - return _get_extension(file.name) - else: - # Maybe it's a Telegram media - return get_extension(file) - - -def is_image(file): - """ - Returns `True` if the file extension looks like an image file to Telegram. - """ - match = re.match(r'\.(png|jpe?g)', _get_extension(file), re.IGNORECASE) - if match: - return True - else: - return isinstance(resolve_bot_file_id(file), types.Photo) - - -def is_gif(file): - """ - Returns `True` if the file extension looks like a gif file to Telegram. - """ - return re.match(r'\.gif', _get_extension(file), re.IGNORECASE) - - -def is_audio(file): - """Returns `True` if the file has an audio mime type.""" - ext = _get_extension(file) - if not ext: - metadata = _get_metadata(file) - if metadata and metadata.has('mime_type'): - return metadata.get('mime_type').startswith('audio/') - else: - return False - else: - file = 'a' + ext - return (mimetypes.guess_type(file)[0] or '').startswith('audio/') - - -def is_video(file): - """Returns `True` if the file has a video mime type.""" - ext = _get_extension(file) - if not ext: - metadata = _get_metadata(file) - if metadata and metadata.has('mime_type'): - return metadata.get('mime_type').startswith('video/') - else: - return False - else: - file = 'a' + ext - return (mimetypes.guess_type(file)[0] or '').startswith('video/') - - -def is_list_like(obj): - """ - Returns `True` if the given object looks like a list. - - Checking ``if hasattr(obj, '__iter__')`` and ignoring ``str/bytes`` is not - enough. Things like ``open()`` are also iterable (and probably many - other things), so just support the commonly known list-like objects. - """ - return isinstance(obj, (list, tuple, set, dict, GeneratorType)) - - -def parse_phone(phone): - """Parses the given phone, or returns `None` if it's invalid.""" - if isinstance(phone, int): - return str(phone) - else: - phone = re.sub(r'[+()\s-]', '', str(phone)) - if phone.isdigit(): - return phone - - -def parse_username(username): - """ - Parses the given username or channel access hash, given - a string, username or URL. Returns a tuple consisting of - both the stripped, lowercase username and whether it is - a joinchat/ hash (in which case is not lowercase'd). - - Returns ``(None, False)`` if the ``username`` or link is not valid. - """ - username = username.strip() - m = USERNAME_RE.match(username) or TG_JOIN_RE.match(username) - if m: - username = username[m.end():] - is_invite = bool(m.group(1)) - if is_invite: - return username, True - else: - username = username.rstrip('/') - - if VALID_USERNAME_RE.match(username): - return username.lower(), False - else: - return None, False - - -def get_inner_text(text, entities): - """ - Gets the inner text that's surrounded by the given entities. - For instance: text = 'hey!', entity = MessageEntityBold(2, 2) -> 'y!'. - - :param text: the original text. - :param entities: the entity or entities that must be matched. - :return: a single result or a list of the text surrounded by the entities. - """ - text = add_surrogate(text) - result = [] - for e in entities: - start = e.offset - end = e.offset + e.length - result.append(del_surrogate(text[start:end])) - - return result - - -def get_peer(peer): - try: - if isinstance(peer, int): - pid, cls = resolve_id(peer) - return cls(pid) - elif peer.SUBCLASS_OF_ID == 0x2d45687: - return peer - elif isinstance(peer, ( - types.contacts.ResolvedPeer, types.InputNotifyPeer, - types.TopPeer, types.Dialog, types.DialogPeer)): - return peer.peer - elif isinstance(peer, types.ChannelFull): - return types.PeerChannel(peer.id) - elif isinstance(peer, types.UserEmpty): - return types.PeerUser(peer.id) - elif isinstance(peer, types.ChatEmpty): - return types.PeerChat(peer.id) - - if peer.SUBCLASS_OF_ID in (0x7d7c6f86, 0xd9c7fc18): - # ChatParticipant, ChannelParticipant - return types.PeerUser(peer.user_id) - - peer = get_input_peer(peer, allow_self=False, check_hash=False) - if isinstance(peer, (types.InputPeerUser, types.InputPeerUserFromMessage)): - return types.PeerUser(peer.user_id) - elif isinstance(peer, types.InputPeerChat): - return types.PeerChat(peer.chat_id) - elif isinstance(peer, (types.InputPeerChannel, types.InputPeerChannelFromMessage)): - return types.PeerChannel(peer.channel_id) - except (AttributeError, TypeError): - pass - _raise_cast_fail(peer, 'Peer') - - -def get_peer_id(peer, add_mark=True): - """ - Convert the given peer into its marked ID by default. - - This "mark" comes from the "bot api" format, and with it the peer type - can be identified back. User ID is left unmodified, chat ID is negated, - and channel ID is "prefixed" with -100: - - * ``user_id`` - * ``-chat_id`` - * ``-100channel_id`` - - The original ID and the peer type class can be returned with - a call to :meth:`resolve_id(marked_id)`. - """ - # First we assert it's a Peer TLObject, or early return for integers - if isinstance(peer, int): - return peer if add_mark else resolve_id(peer)[0] - - # Tell the user to use their client to resolve InputPeerSelf if we got one - if isinstance(peer, types.InputPeerSelf): - _raise_cast_fail(peer, 'int (you might want to use client.get_peer_id)') - - try: - peer = get_peer(peer) - except TypeError: - _raise_cast_fail(peer, 'int') - - if isinstance(peer, types.PeerUser): - return peer.user_id - elif isinstance(peer, types.PeerChat): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.chat_id <= 9999999999): - peer.chat_id = resolve_id(peer.chat_id)[0] - - return -peer.chat_id if add_mark else peer.chat_id - else: # if isinstance(peer, types.PeerChannel): - # Check in case the user mixed things up to avoid blowing up - if not (0 < peer.channel_id <= 9999999999): - peer.channel_id = resolve_id(peer.channel_id)[0] - - if not add_mark: - return peer.channel_id - - # Growing backwards from -100_0000_000_000 indicates it's a channel - return -(1000000000000 + peer.channel_id) - - -def resolve_id(marked_id): - """Given a marked ID, returns the original ID and its :tl:`Peer` type.""" - if marked_id >= 0: - return marked_id, types.PeerUser - - marked_id = -marked_id - if marked_id > 1000000000000: - marked_id -= 1000000000000 - return marked_id, types.PeerChannel - else: - return marked_id, types.PeerChat - - -def _rle_decode(data): - """ - Decodes run-length-encoded `data`. - """ - if not data: - return data - - new = b'' - last = b'' - for cur in data: - if last == b'\0': - new += last * cur - last = b'' - else: - new += last - last = bytes([cur]) - - return new + last - - -def _rle_encode(string): - new = b'' - count = 0 - for cur in string: - if not cur: - count += 1 - else: - if count: - new += b'\0' + bytes([count]) - count = 0 - - new += bytes([cur]) - return new - - -def _decode_telegram_base64(string): - """ - Decodes a url-safe base64-encoded string into its bytes - by first adding the stripped necessary padding characters. - - This is the way Telegram shares binary data as strings, - such as Bot API-style file IDs or invite links. - - Returns `None` if the input string was not valid. - """ - try: - return base64.urlsafe_b64decode(string + '=' * (len(string) % 4)) - except (binascii.Error, ValueError, TypeError): - return None # not valid base64, not valid ascii, not a string - - -def _encode_telegram_base64(string): - """ - Inverse for `_decode_telegram_base64`. - """ - try: - return base64.urlsafe_b64encode(string).rstrip(b'=').decode('ascii') - except (binascii.Error, ValueError, TypeError): - return None # not valid base64, not valid ascii, not a string - - -def resolve_bot_file_id(file_id): - """ - Given a Bot API-style `file_id `, - returns the media it represents. If the `file_id ` - is not valid, `None` is returned instead. - - Note that the `file_id ` does not have information - such as image dimensions or file size, so these will be zero if present. - - For thumbnails, the photo ID and hash will always be zero. - """ - data = _rle_decode(_decode_telegram_base64(file_id)) - if not data: - return None - - # This isn't officially documented anywhere, but - # we assume the last byte is some kind of "version". - data, version = data[:-1], data[-1] - if version not in (2, 4): - return None - - if (version == 2 and len(data) == 24) or (version == 4 and len(data) == 25): - if version == 2: - file_type, dc_id, media_id, access_hash = struct.unpack('LQ', payload)) - elif len(payload) == 16: - return struct.unpack('>LLQ', payload) - else: - pass - except (struct.error, TypeError): - pass - return None, None, None - - -def resolve_inline_message_id(inline_msg_id): - """ - Resolves an inline message ID. Returns a tuple of - ``(message id, peer, dc id, access hash)`` - - The ``peer`` may either be a :tl:`PeerUser` referencing - the user who sent the message via the bot in a private - conversation or small group chat, or a :tl:`PeerChannel` - if the message was sent in a channel. - - The ``access_hash`` does not have any use yet. - """ - try: - dc_id, message_id, pid, access_hash = \ - struct.unpack('> bit_shift) & 0b00011111 - - byte_index, bit_shift = divmod(value_count - 1, 8) - if byte_index == len(waveform) - 1: - value = waveform[byte_index] - else: - value = struct.unpack('> bit_shift) & 0b00011111 - return bytes(result) - - -def split_text(text, entities, *, limit=4096, max_entities=100, split_at=(r'\n', r'\s', '.')): - """ - Split a message text and entities into multiple messages, each with their - own set of entities. This allows sending a very large message as multiple - messages while respecting the formatting. - - Arguments - text (`str`): - The message text. - - entities (List[:tl:`MessageEntity`]) - The formatting entities. - - limit (`int`): - The maximum message length of each individual message. - - max_entities (`int`): - The maximum amount of entities that will be present in each - individual message. - - split_at (Tuplel[`str`]): - The list of regular expressions that will determine where to split - the text. By default, a newline is searched. If no newline is - present, a space is searched. If no space is found, the split will - be made at any character. - - The last expression should always match a character, or else the - text will stop being splitted and the resulting text may be larger - than the limit. - - Yields - Pairs of ``(str, entities)`` with the split message. - - Example - .. code-block:: python - - from telethon import utils - from telethon.extensions import markdown - - very_long_markdown_text = "..." - text, entities = markdown.parse(very_long_markdown_text) - - for text, entities in utils.split_text(text, entities): - await client.send_message(chat, text, formatting_entities=entities) - """ - # TODO add test cases (multiple entities beyond cutoff, at cutoff, splitting at emoji) - # TODO try to optimize this a bit more? (avoid new_ent, smarter update method) - def update(ent, **updates): - kwargs = ent.to_dict() - del kwargs['_'] - kwargs.update(updates) - return ent.__class__(**kwargs) - - text = add_surrogate(text) - split_at = tuple(map(re.compile, split_at)) - - while True: - if len(entities) > max_entities: - last_ent = entities[max_entities - 1] - cur_limit = min(limit, last_ent.offset + last_ent.length) - else: - cur_limit = limit - - if len(text) <= cur_limit: - break - - for split in split_at: - for i in reversed(range(cur_limit)): - m = split.match(text, pos=i) - if m: - cur_text, new_text = text[:m.end()], text[m.end():] - cur_ent, new_ent = [], [] - for ent in entities: - if ent.offset < m.end(): - if ent.offset + ent.length > m.end(): - cur_ent.append(update(ent, length=m.end() - ent.offset)) - new_ent.append(update(ent, offset=0, length=ent.offset + ent.length - m.end())) - else: - cur_ent.append(ent) - else: - new_ent.append(update(ent, offset=ent.offset - m.end())) - - yield del_surrogate(cur_text), cur_ent - text, entities = new_text, new_ent - break - else: - continue - break - else: - # Can't find where to split, just return the remaining text and entities - break - - yield del_surrogate(text), entities - - -class AsyncClassWrapper: - def __init__(self, wrapped): - self.wrapped = wrapped - - def __getattr__(self, item): - w = getattr(self.wrapped, item) - async def wrapper(*args, **kwargs): - val = w(*args, **kwargs) - return await val if inspect.isawaitable(val) else val - - if callable(w): - return wrapper - else: - return w - - -def stripped_photo_to_jpg(stripped): - """ - Adds the JPG header and footer to a stripped image. - - Ported from https://github.com/telegramdesktop/tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225 - """ - # NOTE: Changes here should update _photo_size_byte_count - if len(stripped) < 3 or stripped[0] != 1: - return stripped - - header = bytearray(b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xdb\x00C\x00(\x1c\x1e#\x1e\x19(#!#-+(0 diff --git a/telethon_examples/README.md b/telethon_examples/README.md deleted file mode 100644 index ee066cb4..00000000 --- a/telethon_examples/README.md +++ /dev/null @@ -1,167 +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 core of the actual bot account [@TelethonianBot] running -in the [official Telethon's chat] to help people out. It showcases how to -create an extremely simple "plugins" system with Telethon, but you're free -to borrow ideas from it and make it as fancy as you like (perhaps you want -to add hot reloading?). - -The plugins are a separate Python file each which get loaded dynamically and -can be found at . To use them, -clone the repository into a `plugins` folder next to `assistant.py` and then -run `assistant.py`. - -The content of the plugins or how they work is not really relevant. You can -disable them by moving them elsewhere or deleting the file entirely. The point -is to learn how you can build fancy things with your own code and Telethon. - -### [`interactive_telegram_client.py`] - -* Usable as: **user**. -* Difficulty: **medium**. - -Interactive terminal client that you can use to list your dialogs, -send messages, delete them, and download media. The code is a bit -long which may make it harder to follow, and requires saving some -state in order for downloads to work later. - -### [`quart_login.py`] - -* Usable as: **user**. -* Difficulty: **medium**. - -Web-based application using [Quart](https://pgjones.gitlab.io/quart/index.html) -(an `asyncio` alternative to [Flask](http://flask.pocoo.org/)) and Telethon -together. - -The example should work as a base for Quart applications *with a single -global client*, and it should be easy to adapt for multiple clients by -following the comments in the code. - -It showcases how to login manually (ask for phone, code, and login), -and once the user is logged in, some messages and photos will be shown -in the page. - -There is nothing special about Quart. It was chosen because it's a -drop-in replacement for Flask, the most popular option for web-apps. -You can use any `asyncio` library with Telethon just as well, -like [Sanic](https://sanic.readthedocs.io/en/latest/index.html) or -[aiohttp](https://docs.aiohttp.org/en/stable/). You can even use Flask, -if you learn how to use `threading` and `asyncio` together. - -### [`gui.py`] - -* Usable as: **user and bot**. -* Difficulty: **high**. - -This is a simple GUI written with [`tkinter`] which becomes more complicated -when there's a need to use [`asyncio`] (although it's only a bit of additional -setup). The code to deal with the interface and the commands the GUI supports -also complicate the code further and require knowledge and careful reading. - -This example is the actual bot account [@TelethonianBot] running in the -[official Telethon's chat] to help people out. The file is a bit big and -assumes some [`asyncio`] knowledge, but otherwise is easy to follow. - -![Screenshot of the tkinter GUI][tkinter GUI] - -### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/payment.py) - -* Usable as: **bot**. -* Difficulty: **medium**. - -This example shows how to make invoices (Telegram's way of requesting payments) via a bot account. The example does not include how to add shipping information, though. - -You'll need to obtain a "provider token" to use this example, so please read [Telegram's guide on payments](https://core.telegram.org/bots/payments) before using this example. - - -It makes use of the ["raw API"](https://tl.telethon.dev) (that is, no friendly `client.` methods), which can be helpful in understanding how it works and how it can be used. - - -[Telethon]: https://github.com/LonamiWebs/Telethon -[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_examples/LICENSE -[@BotFather]: https://t.me/BotFather -[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/assistant.py -[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/quart_login.py -[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/gui.py -[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/interactive_telegram_client.py -[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_messages.py -[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_updates.py -[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/replier.py -[@TelethonianBot]: https://t.me/TelethonianBot -[official Telethon's chat]: https://t.me/TelethonChat -[`asyncio`]: https://docs.python.org/3/library/asyncio.html -[`tkinter`]: https://docs.python.org/3/library/tkinter.html -[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/screenshot-gui.jpg -[`events.NewMessage`]: https://docs.telethon.dev/en/stable/modules/events.html#telethon.events.newmessage.NewMessage diff --git a/telethon_examples/assistant.py b/telethon_examples/assistant.py deleted file mode 100644 index 459d8878..00000000 --- a/telethon_examples/assistant.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -This file is only the "core" of the bot. It is responsible for loading the -plugins module and initializing it. You may obtain the plugins by running: - - git clone https://github.com/Lonami/TelethonianBotExt plugins - -In the same folder where this file lives. As a result, the directory should -look like the following: - - assistant.py - plugins/ - ... -""" -import asyncio -import os -import sys -import time - -from telethon import TelegramClient - -try: - # Standalone script assistant.py with folder plugins/ - import plugins -except ImportError: - try: - # Running as a module with `python -m assistant` and structure: - # - # assistant/ - # __main__.py (this file) - # plugins/ (cloned) - from . import plugins - except ImportError: - print('could not load the plugins module, does the directory exist ' - 'in the correct location?', file=sys.stderr) - - exit(1) - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) -API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') -TOKEN = get_env('TG_TOKEN', 'Enter the bot token: ') -NAME = TOKEN.split(':')[0] - - -async def main(): - bot = TelegramClient(NAME, API_ID, API_HASH) - - await bot.start(bot_token=TOKEN) - - try: - await plugins.init(bot) - await bot.run_until_disconnected() - finally: - await bot.disconnect() - - -if __name__ == '__main__': - asyncio.run(main()) diff --git a/telethon_examples/gui.py b/telethon_examples/gui.py deleted file mode 100644 index 275ffcc5..00000000 --- a/telethon_examples/gui.py +++ /dev/null @@ -1,372 +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): - asyncio.create_task(result) - - return wrapped - - -def allow_copy(widget): - """ - This helper makes `widget` readonly but allows copying with ``Ctrl+C``. - """ - widget.bind('', lambda e: None) - widget.bind('', lambda e: "break") - - -class App(tkinter.Tk): - """ - Our main GUI application; we subclass `tkinter.Tk` - so the `self` instance can be the root widget. - - One must be careful when assigning members or - defining methods since those may interfer with - the root widget. - - You may prefer to have ``App.root = tkinter.Tk()`` - and create widgets with ``self.root`` as parent. - """ - def __init__(self, client, *args, **kwargs): - super().__init__(*args, **kwargs) - self.cl = client - self.me = None - - self.title(TITLE) - self.geometry(SIZE) - - # Signing in row; the entry supports phone and bot token - self.sign_in_label = tkinter.Label(self, text='Loading...') - self.sign_in_label.grid(row=0, column=0) - self.sign_in_entry = tkinter.Entry(self) - self.sign_in_entry.grid(row=0, column=1, sticky=tkinter.EW) - self.sign_in_entry.bind('', self.sign_in) - self.sign_in_button = tkinter.Button(self, text='...', - command=self.sign_in) - self.sign_in_button.grid(row=0, column=2) - self.code = None - - # The chat where to send and show messages from - tkinter.Label(self, text='Target chat:').grid(row=1, column=0) - self.chat = tkinter.Entry(self) - self.chat.grid(row=1, column=1, columnspan=2, sticky=tkinter.EW) - self.columnconfigure(1, weight=1) - self.chat.bind('', self.check_chat) - self.chat.bind('', self.check_chat) - self.chat.focus() - self.chat_id = None - - # Message log (incoming and outgoing); we configure it as readonly - self.log = tkinter.scrolledtext.ScrolledText(self) - allow_copy(self.log) - self.log.grid(row=2, column=0, columnspan=3, sticky=tkinter.NSEW) - self.rowconfigure(2, weight=1) - self.cl.add_event_handler(self.on_message, events.NewMessage) - - # Save shown message IDs to support replying with ".rN reply" - # For instance to reply to the last message ".r1 this is a reply" - # Deletion also works with ".dN". - self.message_ids = [] - - # Save the sent texts to allow editing with ".s text/replacement" - # For instance to edit the last "hello" with "bye" ".s hello/bye" - self.sent_text = collections.deque(maxlen=10) - - # Sending messages - tkinter.Label(self, text='Message:').grid(row=3, column=0) - self.message = tkinter.Entry(self) - self.message.grid(row=3, column=1, sticky=tkinter.EW) - self.message.bind('', self.send_message) - tkinter.Button(self, text='Send', - command=self.send_message).grid(row=3, column=2) - - # Post-init (async, connect client) - self.cl.loop.create_task(self.post_init()) - - async def post_init(self): - """ - Completes the initialization of our application. - Since `__init__` cannot be `async` we use this. - """ - if await self.cl.is_user_authorized(): - self.set_signed_in(await self.cl.get_me()) - else: - # User is not logged in, configure the button to ask them to login - self.sign_in_button.configure(text='Sign in') - self.sign_in_label.configure( - text='Sign in (phone/token):') - - async def on_message(self, event): - """ - Event handler that will add new messages to the message log. - """ - # We want to show only messages sent to this chat - if event.chat_id != self.chat_id: - return - - # Save the message ID so we know which to reply to - self.message_ids.append(event.id) - - # Decide a prefix (">> " for our messages, "" otherwise) - if event.out: - text = '>> ' - else: - sender = await event.get_sender() - text = '<{}> '.format(sanitize_str( - utils.get_display_name(sender))) - - # If the message has media show "(MediaType) " - if event.media: - text += '({}) '.format(event.media.__class__.__name__) - - text += sanitize_str(event.text) - text += '\n' - - # Append the text to the end with a newline, and scroll to the end - self.log.insert(tkinter.END, text) - self.log.yview(tkinter.END) - - # noinspection PyUnusedLocal - @callback - async def sign_in(self, event=None): - """ - Note the `event` argument. This is required since this callback - may be called from a ``widget.bind`` (such as ``''``), - which sends information about the event we don't care about. - - This callback logs out if authorized, signs in if a code was - sent or a bot token is input, or sends the code otherwise. - """ - self.sign_in_label.configure(text='Working...') - self.sign_in_entry.configure(state=tkinter.DISABLED) - if await self.cl.is_user_authorized(): - await self.cl.log_out() - self.destroy() - return - - value = self.sign_in_entry.get().strip() - if self.code: - self.set_signed_in(await self.cl.sign_in(code=value)) - elif ':' in value: - self.set_signed_in(await self.cl.sign_in(bot_token=value)) - else: - self.code = await self.cl.send_code_request(value) - self.sign_in_label.configure(text='Code:') - self.sign_in_entry.configure(state=tkinter.NORMAL) - self.sign_in_entry.delete(0, tkinter.END) - self.sign_in_entry.focus() - return - - def set_signed_in(self, me): - """ - Configures the application as "signed in" (displays user's - name and disables the entry to input phone/bot token/code). - """ - self.me = me - self.sign_in_label.configure(text='Signed in') - self.sign_in_entry.configure(state=tkinter.NORMAL) - self.sign_in_entry.delete(0, tkinter.END) - self.sign_in_entry.insert(tkinter.INSERT, utils.get_display_name(me)) - self.sign_in_entry.configure(state=tkinter.DISABLED) - self.sign_in_button.configure(text='Log out') - self.chat.focus() - - # noinspection PyUnusedLocal - @callback - async def send_message(self, event=None): - """ - Sends a message. Does nothing if the client is not connected. - """ - if not self.cl.is_connected(): - return - - # The user needs to configure a chat where the message should be sent. - # - # If the chat ID does not exist, it was not valid and the user must - # configure one; hint them by changing the background to red. - if not self.chat_id: - self.chat.configure(bg='red') - self.chat.focus() - return - - # Get the message, clear the text field and focus it again - text = self.message.get().strip() - self.message.delete(0, tkinter.END) - self.message.focus() - if not text: - return - - # NOTE: This part is optional but supports editing messages - # You can remove it if you find it too complicated. - # - # Check if the edit matches any text - m = EDIT.match(text) - if m: - find = re.compile(m.group(1).lstrip()) - # Cannot reversed(enumerate(...)), use index - for i in reversed(range(len(self.sent_text))): - msg_id, msg_text = self.sent_text[i] - if find.search(msg_text): - # Found text to replace, so replace it and edit - new = find.sub(m.group(2), msg_text) - self.sent_text[i] = (msg_id, new) - await self.cl.edit_message(self.chat_id, msg_id, new) - - # Notify that a replacement was made - self.log.insert(tkinter.END, '(message edited: {} -> {})\n' - .format(msg_text, new)) - self.log.yview(tkinter.END) - return - - # Check if we want to delete the message - m = DELETE.match(text) - if m: - try: - delete = self.message_ids.pop(-int(m.group(1))) - except IndexError: - pass - else: - await self.cl.delete_messages(self.chat_id, delete) - # Notify that a message was deleted - self.log.insert(tkinter.END, '(message deleted)\n') - self.log.yview(tkinter.END) - return - - # Check if we want to reply to some message - reply_to = None - m = REPLY.match(text) - if m: - text = m.group(2) - try: - reply_to = self.message_ids[-int(m.group(1))] - except IndexError: - pass - - # NOTE: This part is no longer optional. It sends the message. - # Send the message text and get back the sent message object - message = await self.cl.send_message(self.chat_id, text, - reply_to=reply_to) - - # Save the sent message ID and text to allow edits - self.sent_text.append((message.id, text)) - - # Process the sent message as if it were an event - await self.on_message(message) - - # noinspection PyUnusedLocal - @callback - async def check_chat(self, event=None): - """ - Checks the input chat where to send and listen messages from. - """ - if self.me is None: - return # Not logged in yet - - chat = self.chat.get().strip() - try: - chat = int(chat) - except ValueError: - pass - - try: - old = self.chat_id - # Valid chat ID, set it and configure the colour back to white - self.chat_id = await self.cl.get_peer_id(chat) - self.chat.configure(bg='white') - - # If the chat ID changed, clear the - # messages that we could edit or reply - if self.chat_id != old: - self.message_ids.clear() - self.sent_text.clear() - self.log.delete('1.0', tkinter.END) - if not self.me.bot: - for msg in reversed( - await self.cl.get_messages(self.chat_id, 100)): - await self.on_message(msg) - except ValueError: - # Invalid chat ID, let the user know with a yellow background - self.chat_id = None - self.chat.configure(bg='yellow') - - -async def main(interval=0.05): - client = TelegramClient(SESSION, API_ID, API_HASH) - try: - await client.connect() - except Exception as e: - print('Failed to connect', e, file=sys.stderr) - return - - app = App(client) - try: - while True: - # We want to update the application but get back - # to asyncio's event loop. For this we sleep a - # short time so the event loop can run. - # - # https://www.reddit.com/r/Python/comments/33ecpl - app.update() - await asyncio.sleep(interval) - except KeyboardInterrupt: - pass - except tkinter.TclError as e: - if 'application has been destroyed' not in e.args[0]: - raise - finally: - await app.cl.disconnect() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/telethon_examples/interactive_telegram_client.py b/telethon_examples/interactive_telegram_client.py deleted file mode 100644 index a132bdf0..00000000 --- a/telethon_examples/interactive_telegram_client.py +++ /dev/null @@ -1,406 +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 - - -def sprint(string, *args, **kwargs): - """Safe Print (handle UnicodeEncodeErrors on some terminals)""" - try: - print(string, *args, **kwargs) - except UnicodeEncodeError: - string = string.encode('utf-8', errors='ignore')\ - .decode('ascii', errors='ignore') - print(string, *args, **kwargs) - - -def print_title(title): - """Helper function to print titles to the console more nicely""" - sprint('\n') - sprint('=={}=='.format('=' * len(title))) - sprint('= {} ='.format(title)) - sprint('=={}=='.format('=' * len(title))) - - -def bytes_to_string(byte_count): - """Converts a byte count to a string (in KB, MB...)""" - suffix_index = 0 - while byte_count >= 1024: - byte_count /= 1024 - suffix_index += 1 - - return '{:.2f}{}'.format( - byte_count, [' bytes', 'KB', 'MB', 'GB', 'TB'][suffix_index] - ) - - -async def async_input(prompt): - """ - Python's ``input()`` is blocking, which means the event loop we set - above can't be running while we're blocking there. This method will - let the loop run while we wait for input. - """ - print(prompt, end='', flush=True) - return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip() - - -def get_env(name, message, cast=str): - """Helper to get environment variables interactively""" - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -class InteractiveTelegramClient(TelegramClient): - """Full featured Telegram client, meant to be used on an interactive - session to see what Telethon is capable off - - - This client allows the user to perform some basic interaction with - Telegram through Telethon, such as listing dialogs (open chats), - talking to people, downloading media, and receiving updates. - """ - - def __init__(self, session_user_id, api_id, api_hash, - proxy=None): - """ - Initializes the InteractiveTelegramClient. - :param session_user_id: Name of the *.session file. - :param api_id: Telegram's api_id acquired through my.telegram.org. - :param api_hash: Telegram's api_hash. - :param proxy: Optional proxy tuple/dictionary. - """ - print_title('Initialization') - - print('Initializing interactive example...') - - # The first step is to initialize the TelegramClient, as we are - # subclassing it, we need to call super().__init__(). On a more - # normal case you would want 'client = TelegramClient(...)' - super().__init__( - # These parameters should be passed always, session name and API - session_user_id, api_id, api_hash, - - # You can optionally change the connection mode by passing a - # type or an instance of it. This changes how the sent packets - # look (low-level concept you normally shouldn't worry about). - # Default is ConnectionTcpFull, smallest is ConnectionTcpAbridged. - connection=ConnectionTcpAbridged, - - # If you're using a proxy, set it here. - proxy=proxy - ) - - # Store {message.id: message} map here so that we can download - # media known the message ID, for every message having media. - self.found_media = {} - - async def init(self): - # Calling .connect() may raise a connection error False, so you need - # to except those before continuing. Otherwise you may want to retry - # as done here. - print('Connecting to Telegram servers...') - try: - await self.connect() - except IOError: - # We handle IOError and not ConnectionError because - # PySocks' errors do not subclass ConnectionError - # (so this will work with and without proxies). - print('Initial connection failed. Retrying...') - await self.connect() - - # If the user hasn't called .sign_in() yet, they won't - # be authorized. The first thing you must do is authorize. Calling - # .sign_in() should only be done once as the information is saved on - # the *.session file so you don't need to enter the code every time. - if not await self.is_user_authorized(): - print('First run. Sending code request...') - user_phone = input('Enter your phone: ') - await self.sign_in(user_phone) - - self_user = None - while self_user is None: - code = input('Enter the code you just received: ') - try: - self_user = await self.sign_in(code=code) - - # Two-step verification may be enabled, and .sign_in will - # raise this error. If that's the case ask for the password. - # Note that getpass() may not work on PyCharm due to a bug, - # if that's the case simply change it for input(). - except SessionPasswordNeededError: - pw = getpass('Two step verification is enabled. ' - 'Please enter your password: ') - - self_user = await self.sign_in(password=pw) - - async def run(self): - """Main loop of the TelegramClient, will wait for user action""" - - # Once everything is ready, we can add an event handler. - # - # Events are an abstraction over Telegram's "Updates" and - # are much easier to use. - self.add_event_handler(self.message_handler, events.NewMessage) - - # Enter a while loop to chat as long as the user wants - while True: - # Retrieve the top dialogs. You can set the limit to None to - # retrieve all of them if you wish, but beware that may take - # a long time if you have hundreds of them. - dialog_count = 15 - - # Entities represent the user, chat or channel - # corresponding to the dialog on the same index. - dialogs = await self.get_dialogs(limit=dialog_count) - - i = None - while i is None: - print_title('Dialogs window') - - # Display them so the user can choose - for i, dialog in enumerate(dialogs, start=1): - sprint('{}. {}'.format(i, get_display_name(dialog.entity))) - - # Let the user decide who they want to talk to - print() - print('> Who do you want to send messages to?') - print('> Available commands:') - print(' !q: Quits the dialogs window and exits.') - print(' !l: Logs out, terminating this session.') - print() - i = await async_input('Enter dialog ID or a command: ') - if i == '!q': - return - if i == '!l': - # Logging out will cause the user to need to reenter the - # code next time they want to use the library, and will - # also delete the *.session file off the filesystem. - # - # This is not the same as simply calling .disconnect(), - # which simply shuts down everything gracefully. - await self.log_out() - return - - try: - i = int(i if i else 0) - 1 - # Ensure it is inside the bounds, otherwise retry - if not 0 <= i < dialog_count: - i = None - except ValueError: - i = None - - # Retrieve the selected user (or chat, or channel) - entity = dialogs[i].entity - - # Show some information - print_title('Chat with "{}"'.format(get_display_name(entity))) - print('Available commands:') - print(' !q: Quits the current chat.') - print(' !Q: Quits the current chat and exits.') - print(' !h: prints the latest messages (message History).') - print(' !up : Uploads and sends the Photo from path.') - print(' !uf : Uploads and sends the File from path.') - print(' !d : Deletes a message by its id') - print(' !dm : Downloads the given message Media (if any).') - print(' !dp: Downloads the current dialog Profile picture.') - print(' !i: Prints information about this chat..') - print() - - # And start a while loop to chat - while True: - msg = await async_input('Enter a message: ') - # Quit - if msg == '!q': - break - elif msg == '!Q': - return - - # History - elif msg == '!h': - # First retrieve the messages and some information - messages = await self.get_messages(entity, limit=10) - - # Iterate over all (in reverse order so the latest appear - # the last in the console) and print them with format: - # "[hh:mm] Sender: Message" - for msg in reversed(messages): - # Note how we access .sender here. Since we made an - # API call using the self client, it will always have - # information about the sender. This is different to - # events, where Telegram may not always send the user. - name = get_display_name(msg.sender) - - # Format the message content - if getattr(msg, 'media', None): - self.found_media[msg.id] = msg - content = '<{}> {}'.format( - type(msg.media).__name__, msg.message) - - elif hasattr(msg, 'message'): - content = msg.message - elif hasattr(msg, 'action'): - content = str(msg.action) - else: - # Unknown message, simply print its class name - content = type(msg).__name__ - - # And print it to the user - sprint('[{}:{}] (ID={}) {}: {}'.format( - msg.date.hour, msg.date.minute, msg.id, name, content)) - - # Send photo - elif msg.startswith('!up '): - # Slice the message to get the path - path = msg[len('!up '):] - await self.send_photo(path=path, entity=entity) - - # Send file (document) - elif msg.startswith('!uf '): - # Slice the message to get the path - path = msg[len('!uf '):] - await self.send_document(path=path, entity=entity) - - # Delete messages - elif msg.startswith('!d '): - # Slice the message to get message ID - msg = msg[len('!d '):] - deleted_msg = await self.delete_messages(entity, msg) - print('Deleted {}'.format(deleted_msg)) - - # Download media - elif msg.startswith('!dm '): - # Slice the message to get message ID - await self.download_media_by_id(msg[len('!dm '):]) - - # Download profile photo - elif msg == '!dp': - print('Downloading profile picture to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = await self.download_profile_photo(entity, - 'usermedia') - if output: - print('Profile picture downloaded to', output) - else: - print('No profile picture found for this user!') - - elif msg == '!i': - attributes = list(entity.to_dict().items()) - pad = max(len(x) for x, _ in attributes) - for name, val in attributes: - print("{:<{width}} : {}".format(name, val, width=pad)) - - # Send chat message (if any) - elif msg: - await self.send_message(entity, msg, link_preview=False) - - async def send_photo(self, path, entity): - """Sends the file located at path to the desired entity as a photo""" - await self.send_file( - entity, path, - progress_callback=self.upload_progress_callback - ) - print('Photo sent!') - - async def send_document(self, path, entity): - """Sends the file located at path to the desired entity as a document""" - await self.send_file( - entity, path, - force_document=True, - progress_callback=self.upload_progress_callback - ) - print('Document sent!') - - async def download_media_by_id(self, media_id): - """Given a message ID, finds the media this message contained and - downloads it. - """ - try: - msg = self.found_media[int(media_id)] - except (ValueError, KeyError): - # ValueError when parsing, KeyError when accessing dictionary - print('Invalid media ID given or message not found!') - return - - print('Downloading media to usermedia/...') - os.makedirs('usermedia', exist_ok=True) - output = await self.download_media( - msg.media, - file='usermedia/', - progress_callback=self.download_progress_callback - ) - print('Media downloaded to {}!'.format(output)) - - @staticmethod - def download_progress_callback(downloaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress( - 'Downloaded', downloaded_bytes, total_bytes - ) - - @staticmethod - def upload_progress_callback(uploaded_bytes, total_bytes): - InteractiveTelegramClient.print_progress( - 'Uploaded', uploaded_bytes, total_bytes - ) - - @staticmethod - def print_progress(progress_type, downloaded_bytes, total_bytes): - print('{} {} out of {} ({:.2%})'.format( - progress_type, bytes_to_string(downloaded_bytes), - bytes_to_string(total_bytes), downloaded_bytes / total_bytes) - ) - - async def message_handler(self, event): - """Callback method for received events.NewMessage""" - - # Note that message_handler is called when a Telegram update occurs - # and an event is created. Telegram may not always send information - # about the ``.sender`` or the ``.chat``, so if you *really* want it - # you should use ``get_chat()`` and ``get_sender()`` while working - # with events. Since they are methods, you know they may make an API - # call, which can be expensive. - chat = await event.get_chat() - if event.is_group: - if event.out: - sprint('>> sent "{}" to chat {}'.format( - event.text, get_display_name(chat) - )) - else: - sprint('<< {} @ {} sent "{}"'.format( - get_display_name(await event.get_sender()), - get_display_name(chat), - event.text - )) - else: - if event.out: - sprint('>> "{}" to user {}'.format( - event.text, get_display_name(chat) - )) - else: - sprint('<< {} sent "{}"'.format( - get_display_name(chat), event.text - )) - - -async def main(): - SESSION = os.environ.get('TG_SESSION', 'interactive') - API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) - API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - client = InteractiveTelegramClient(SESSION, API_ID, API_HASH) - await client.init() - await client.run() - - -if __name__ == '__main__': - asyncio.run() diff --git a/telethon_examples/payment.py b/telethon_examples/payment.py deleted file mode 100644 index 03735c44..00000000 --- a/telethon_examples/payment.py +++ /dev/null @@ -1,181 +0,0 @@ -from telethon import TelegramClient, events, types, functions - -import asyncio -import logging -import tracemalloc -import os -import time -import sys - -""" -Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token - -If you are using test token, set test=True in generate_invoice function, -If you are using real token, set test=False -""" -provider_token = '' - -tracemalloc.start() -logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - level=logging.WARNING) -logger = logging.getLogger(__name__) - - -def get_env(name, message, cast=str): - if name in os.environ: - return os.environ[name] - while True: - value = input(message) - try: - return cast(value) - except ValueError as e: - print(e, file=sys.stderr) - time.sleep(1) - - -bot = TelegramClient( - os.environ.get('TG_SESSION', 'payment'), - get_env('TG_API_ID', 'Enter your API ID: ', int), - get_env('TG_API_HASH', 'Enter your API hash: '), - proxy=None -) - - -# That event is handled when customer enters his card/etc, on final pre-checkout -# If we don't `SetBotPrecheckoutResultsRequest`, money won't be charged from buyer, and nothing will happen next. -@bot.on(events.Raw(types.UpdateBotPrecheckoutQuery)) -async def payment_pre_checkout_handler(event: types.UpdateBotPrecheckoutQuery): - if event.payload.decode('UTF-8') == 'product A': - # so we have to confirm payment - await bot( - functions.messages.SetBotPrecheckoutResultsRequest( - query_id=event.query_id, - success=True, - error=None - ) - ) - elif event.payload.decode('UTF-8') == 'product B': - # same for another - await bot( - functions.messages.SetBotPrecheckoutResultsRequest( - query_id=event.query_id, - success=True, - error=None - ) - ) - else: - # for example, something went wrong (whatever reason). We can tell customer about that: - await bot( - functions.messages.SetBotPrecheckoutResultsRequest( - query_id=event.query_id, - success=False, - error='Something went wrong' - ) - ) - - raise events.StopPropagation - - -# That event is handled at the end, when customer payed. -@bot.on(events.Raw(types.UpdateNewMessage)) -async def payment_received_handler(event): - if isinstance(event.message.action, types.MessageActionPaymentSentMe): - payment: types.MessageActionPaymentSentMe = event.message.action - # do something after payment was received - if payment.payload.decode('UTF-8') == 'product A': - await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product A!') - elif payment.payload.decode('UTF-8') == 'product B': - await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product B!') - raise events.StopPropagation - - -# let's put it in one function for more easier way -def generate_invoice(price_label: str, price_amount: int, currency: str, title: str, - description: str, payload: str, start_param: str) -> types.InputMediaInvoice: - price = types.LabeledPrice(label=price_label, amount=price_amount) # label - just a text, amount=10000 means 100.00 - invoice = types.Invoice( - currency=currency, # currency like USD - prices=[price], # there could be a couple of prices. - test=True, # if you're working with test token, else set test=False. - # More info at https://core.telegram.org/bots/payments - - # params for requesting specific fields - name_requested=False, - phone_requested=False, - email_requested=False, - shipping_address_requested=False, - - # if price changes depending on shipping - flexible=False, - - # send data to provider - phone_to_provider=False, - email_to_provider=False - ) - return types.InputMediaInvoice( - title=title, - description=description, - invoice=invoice, - payload=payload.encode('UTF-8'), # payload, which will be sent to next 2 handlers - provider=provider_token, - - provider_data=types.DataJSON('{}'), - # data about the invoice, which will be shared with the payment provider. A detailed description of - # required fields should be provided by the payment provider. - - start_param=start_param, - # Unique deep-linking parameter. May also be used in UpdateBotPrecheckoutQuery - # see: https://core.telegram.org/bots#deep-linking - # it may be the empty string if not needed - - ) - - -@bot.on(events.NewMessage(pattern='/start')) -async def start_handler(event: events.NewMessage.Event): - await event.respond('/product_a - product A\n/product_b - product B\n/product_c - product, shall cause an error') - - -@bot.on(events.NewMessage(pattern='/product_a')) -async def start_handler(event: events.NewMessage.Event): - await bot.send_message( - event.chat_id, 'Sending invoice A', - file=generate_invoice( - price_label='Pay', price_amount=10000, currency='RUB', title='Title A', description='description A', - payload='product A', start_param='abc' - ) - ) - - -@bot.on(events.NewMessage(pattern='/product_b')) -async def start_handler(event: events.NewMessage.Event): - await bot.send_message( - event.chat_id, 'Sending invoice B', - file=generate_invoice( - price_label='Pay', price_amount=20000, currency='RUB', title='Title B', description='description B', - payload='product B', start_param='abc' - ) - ) - - -@bot.on(events.NewMessage(pattern='/product_c')) -async def start_handler(event: events.NewMessage.Event): - await bot.send_message( - event.chat_id, 'Sending invoice C', - file=generate_invoice( - price_label='Pay', price_amount=50000, currency='RUB', title='Title C', - description='description c - shall cause an error', payload='product C', start_param='abc' - ) - ) - - -async def main(): - await bot.start() - await bot.run_until_disconnected() - - -if __name__ == '__main__': - if not provider_token: - logger.error("No provider token supplied.") - exit(1) - asyncio.run(main()) diff --git a/telethon_examples/print_messages.py b/telethon_examples/print_messages.py deleted file mode 100644 index 21aafc59..00000000 --- a/telethon_examples/print_messages.py +++ /dev/null @@ -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 deleted file mode 100755 index 48ade9d4..00000000 --- a/telethon_examples/print_updates.py +++ /dev/null @@ -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 deleted file mode 100644 index 50498dc8..00000000 --- a/telethon_examples/quart_login.py +++ /dev/null @@ -1,140 +0,0 @@ -import base64 -import os - -from quart import Quart, render_template_string, request - -from telethon import TelegramClient, utils -from telethon.errors import SessionPasswordNeededError - - -def get_env(name, message): - if name in os.environ: - return os.environ[name] - return input(message) - - -BASE_TEMPLATE = ''' - - - - - Telethon + Quart - - {{ content | safe }} - -''' - -PHONE_FORM = ''' -
- Phone (international format): - -
-''' - -CODE_FORM = ''' -
- Telegram code: - -
-''' - -PASSWORD_FORM = ''' -
- Telegram password: - -
-''' - -# Session name, API ID and hash to use; loaded from environmental variables -SESSION = os.environ.get('TG_SESSION', 'quart') -API_ID = int(get_env('TG_API_ID', 'Enter your API ID: ')) -API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') - -# Telethon client -client = TelegramClient(SESSION, API_ID, API_HASH) -client.parse_mode = 'html' # <- Render things nicely -phone = None - -# Quart app -app = Quart(__name__) -app.secret_key = 'CHANGE THIS TO SOMETHING SECRET' - - -# Helper method to format messages nicely -async def format_message(message): - if message.photo: - content = '{}'.format( - base64.b64encode(await message.download_media(bytes)).decode(), - message.raw_text - ) - else: - # client.parse_mode = 'html', so bold etc. will work! - content = (message.text or '(action message)').replace('\n', '
') - - return '

{}: {}{}

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

{}

'.format(dialog.title) - async for m in client.iter_messages(dialog, 10): - result += await(format_message(m)) - - return await render_template_string(BASE_TEMPLATE, content=result) - - # Ask for the phone if we don't know it yet - if phone is None: - return await render_template_string(BASE_TEMPLATE, content=PHONE_FORM) - - # We have the phone, but we're not logged in, so ask for the code - return await render_template_string(BASE_TEMPLATE, content=CODE_FORM) - - -# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio -# event loop. If we had connected the `TelegramClient` before, `telethon` will -# use `asyncio.get_running_loop()` to create some additional tasks. If these -# loops are different, it won't work. -# -# To keep things simple, be sure to not create multiple asyncio loops! -if __name__ == '__main__': - app.run() diff --git a/telethon_examples/replier.py b/telethon_examples/replier.py deleted file mode 100755 index e498bad9..00000000 --- a/telethon_examples/replier.py +++ /dev/null @@ -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.is_reply and 'save pic' in event.raw_text: - reply_msg = await event.get_reply_message() - replied_to_user = await reply_msg.get_input_sender() - - message = await event.reply('Downloading your profile photo...') - file = await client.download_profile_photo(replied_to_user) - await message.edit('I saved your photo in {}'.format(file)) - - -client = TelegramClient( - os.environ.get('TG_SESSION', 'replier'), - get_env('TG_API_ID', 'Enter your API ID: ', int), - get_env('TG_API_HASH', 'Enter your API hash: '), - proxy=None -) - -with client: - # This remembers the events.NewMessage we registered before - client.add_event_handler(handler) - - print('(Press Ctrl+C to stop this)') - client.run_until_disconnected() diff --git a/telethon_examples/screenshot-gui.jpg b/telethon_examples/screenshot-gui.jpg deleted file mode 100644 index 4da34b0ac8b027123b8183c8a06aaa7889ebf44e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26010 zcmeFZ1$dl0vMAVQcFfEeGcz+&%pA9wnb|SN%*@Qp6f-kp%oICj$NYR|=D^II-M#zm zdwcJ7ImrK*x#sxC<-S-;+v-@XHoq{XGg0U#g%0LXg;ysZI50ML+-P>>MNP*6}X zFwn5@XbA9daPZhDs2|YqaR>?Uaq#en$ml7FNN7p%@IG=-(K0fzva%9Va`AC8^U|}h zGXGWy2n-AiJRCe00s?L~ zpZVV)TG+eS)Dg>Tf;q-N2sS*uh*rQQw804rwj@yu3$C+2NFYQp>H*6!iU9^DiWuN@ zTk;2%V{NE@f6&)hq+%N)Bw5oY4k+?i{y+l&F#Q1qC{qQ@JwLCFBESTHr-1mqLw{xg zVCEM9023!{_B8LQK|=ya&hRR1uOJii@BDvFL0S@o$p8oxvsB-k`S;QFlwxFnB}btU zmo6$OA3}Qj|H}J23_0x>#33fadxC(C@06FozNSR zV{s8$Ib#DSpnA4Y$LVCmzbgh=e>E`GB&Ly)IFJGWH6VKD36TGHi~zPYg~=uUWAwQs z304_?oMF?sJnAq=iaw!^GP+2US(t|YMw+i*lE`95f^w(g{fYcO%aUNFOPTCYmCQ>r zG0`Fz|7jRdB%-|R#cKx@GIP-k<570=@5(tO9|3Qtm1Z$t%t*CUijj>fIY8XI%FpAE zlM&IB)Qz8%+?1e`G}fF(4!_>6=xHP+L|h*KLsGCb8U?u0d#Tcbe5Hm^8#-xagN|7t zhUg&8n_1`Yf3W=(0L!A8f+)SGDtDl&si44x%U7C0fguBG-V}1APj>v3^H11u!2Nq8 z|27*?4{s#wvo!}&RN&!+sib{@u=`9*s7V0R#hJ7)HrcVf=&TV;h@^e+oTifmDJb?# z!eAYUlP!&$SnkZ9wIRAy^I{aQo3shfSCQa^QS>sIsN+Nh=CENU4^Z7q8<{hVqA2;# zqCw^v`0F_vQ;p(-IDEn#5f3mQ%a$RT4i)83+IzR&e*};z^uvJpC)W8{DYTwN1=`Ww zG-eL`)(Jux(2Seky63+MTss{XTG`VqYW<(2(3XLEiAJdIVMM`3(yYm!{|);)>hBsD zVB__RIxVDEXTe-BFbm>)Dw|SSe?0`yYi+!}a;`rz?nXIpl=vCI-*K9D`p^$c9Sz zI70c@9kT1kA$F|)b2We0t3ZxPalGt@;4dWW`Q+IMA+zR+LbP%PR~fa$gmNZz_NtZ5~mG?W04C3-%MKjIu`%}0-bprrEga6&yLS_Cz zkCOb85%72Wd-YE{`AhM?(EkA*hF^cP@D-eJ7W|L2KQVtZvv+m0<@|~JkI-L*`;XM$ zF?g4HZgdJq*i|LJWBy(GyHPHw{5$IJxWDW75z?~kcg(*_|85j7U~CKjj`}+eWE4Fl zOyy~PN&I)rze}OmMR~>F2Z@TFcR!-#-*EuIP>%orG4|}ND(_M1g2S(l$0bcP|438pN@@tXw&%m`+|Rqmj$PVSc}K?7u^HZ*@ry zwVO}12^_d;z<2)Y{(9-Utu&t~zh6?*Ua9H&U2Ot(`s+sxmI#QVl>IuIezX+4Xw+kZNx?R zN?ze(-+EG!|N03Q?@NcoM;ebOp-bgdIGZf}Uy;l#N#-4+>5_acP;v^Vj}@E!n0Zvh zl;~^(Mj$0v*^P-TnF8JRQB4w?{pLl>prZg)OT6=^HNPL6KAyY_4+amqAh+9f)WH9% zv7eEtEhFr)TAXwDt6lf>ry3CVFzJUv8Y#W$(cP=Kh31_dns|velKJtL;V7tuUIcx< zn6J>eTSk!mJ~Ql<#GeZ%OB9fYTG}0!sRK<3r)lRQ)f!F87OR{ES{t=|Tf5zD_dO_2 zI`y;;wYKuiHQ90;jC>K%jWwO^l%78!!+bD9Njttx&VvAxv|3I)gRc^xqSN-Fr<|F{ zPgh&yWgIBa5I8~AV63qm!%SkJ)zj#xbq8Jz?0@}Voz<3BOQ4NWoGNt4uP8bC^{G@E z$t0Ax!EiLH?j3O=8>2>LE)zW}qeR*F6%>~Yd^?V>$D80TvMnp?x37%5y2|_{-4-WH zqj1IuiWu4OUiWe)iB_fD0c`Qv4-lVob|gsy@$HayBJ$0l0?N^xNF{X818US@ziH4w ze6IMP0BW!n(x}=$NLfg{0a%dr$*o$}Z0xs2hY7^YG`$RHI+4mm*4Mc7LFK9F7z!kk z5$gMN2b1L`@whV@jH*X~%4E{}QT^8mKDgR0;dY- z*4BPaYdKJ_=)!IyMoqP!beRqIxn+N76rfj5Zw~EvZ*Rr^&5Anmt((a4yU=zaCMwbF z0TAw)UFd&AG?;HlkfT?EIx(1J5F;QCe**Hc=+_Vw9sdFUA36V5Vo{?9P$k5n>!9o!l$Fzrvi562~jmH*^gWH5!gH*boMxUSwnZ*dx@Y`Demzw zu4J(3?E{fxcS+$X5**IkF^Pn5ZItvGuyb3*V~M|^wX%kx#o^I3xb_$|r|oaYM!;}< ztl7$$Y>RBxfk3%{p>*=7yM(x*DaEj(P@RVl6AtAwu^uH&_|A0PA zyl2IXcari+mps|b2uAt~nFPiu(MZMcm)NI~-Hn!@gp|SA_K0?`@qG)BXj3>S85$@| zcw|FZJQE42 zh5bTIA>v;53l;1Vc?c>ED9uh_rTb6^C<4j~?FDg7VpCGmBD}J-L_Bq!avq*501v(~ z{TVJO<;XhXE(FDMALBcu&JpYYT}+$-GwwP2Si%Kz6kD9ki9Qm^NX#2x;I1z*`C~PH zNxQ^GZ@P@L0ZKx?`A&9w!iHmqN)B}IeR=cvq*~bL5EImB689T%Mhaaq_$tFpH#oA6 zkICYJPsU+hXnl%ukq(aEyJU7{zy}w4+NQ{u{hZh~VtqmR4@ zsw)Z2^3#bMT#Fk=bEmDQqV8g2E9)RZ18Z{w@M#34;q#6MzgzTJN${oqBxWx)e}*`q zE0`IDEw^q4gpjmFKY_aRG>s0x9QPa{jssUR)jM;a_M&|XJw<4KYg~Q;hF^zMM~&Yo zxF$&|ejEr@JXa1#%z2!H8OuP5w=4ds&Z{UJNgLiwWgH^R5_d0}MQ*#|>Pg$7+QxqMLc(X(@pi3@unek-I2yub;dIS|JeOi-GY?VWSwmElYx6;6Wk7yS z9vQw_*;o;y*1DQqThfiJ5kx?#D-$dLysH=tYlvwHwabN7QI0j0A;;e{f4_@o&Bqo? zs-Q_H)!w|r=SrgcQ)h1C+JPyJD5GqHe;o`c@j7=bKMf%(bJ?1ic9XwV3A{r|j5gW; zok{;>_^;z>f};u8C`j=LBIvtEDIREXE#VO-j@hP~MbtbBEeMH)^TG@(OLoZ72y1Iy zt11Jdu%wteUHBHs#t0(rB>O$v?k^a)J9V5EfNAG!h`B08C2L~VVr+YoQG?fv&u+DX zDh*$SRv#EkiN4T%0Q2?}4LruY7%MZ96_MmjHKDO0iNB8 zWnePwO*p>Z2h5h@-)njvddj$eG&L*iI^`Bkg1;mi zEv(tEtlJ$9J6ArrTJJ;VS2cyy40!kiP31VxjHTg_*#GmZbs~9C+Za?V+BJoJvpDHU zax5&XRqM5kDJTCE=~n5xV{>-Rl3e`y(JaOg4G}@kBG?I?o;TT7@a9YLv8@YE=r z3Zq57N{L`{zWbd9xY_ECHgS@k@;CDktf!q8|EtQnd`7iUL$Ka*^AL^d@E~xIl8jAe zazg;2Pwdb6YBOV(htyHL5b=5mM?#0p2Ff-nSdDS_e(2e1mJNO1xd zV_aqVmaaqc9J~kmmd`}%RnblS7Ji!82N9OB5lejl&FX4Pb#oM8Qm{n!M@Q4Pm_VY< za}N-1^C`jL1>R)lnE7_Byb27M=y+}~@wucFH!e7{>RYVgFWjm|fsKCVACi8}Ra~rM zj&Z&&{j`wrffw6OidQF4?o zCAOR*&f>^k$29RI+i0}L?G#H|8&ZJ~g`G)(g^A(&Rn>_yX z>(&w=0aRdXOgY*vgMEt|5NoO>Ibzn6LYu!X*IkC)B*31eV8MB@)});v<(D^%?4rfP zdxB43qZsqhwl_bMm>^X|5@11F8AJxyB>74X_eNIZc&@T@{VX)saxY#k_+tqClHCgh zD$<}0`boFQH{)E~{r*6*w75I2XriGSFLu6$9MIGk!M-VC{cxXcfkQQoPk(IhNs^_O zQlv^FYS}A@5*ojm$pAll&f3Qp7Ry5}Hn=Tz#blnHl5%)-vOxK+JSTGY$ zgbPNyPva*d?p`Xg3d|Cll*79E#oh&6Q_-LC2-9TT6+$x{%?FP4@z$MnV{B0SH!9Am zq}qd=XjE1xi9?kd&-{b~r3%FNyP^1%xLg$uBAYu!?H6!88xm)!s6c#(Fq*VTBU=;z z?%{_LdM-*!RwFQnn(?3vuGHgfxR4mZ*IhGwf1=*N{M)R2{00aooKNNqNDjZqFtyBJ zb&O(chAmR#6KSMUsmx8|uGw`T#>nFfCMIDgQyVaR_LW5BBF+^hY1lE>yjdoYe*?_1 zqQQ(8_&FNRL~{*cAu^d5S~Jad=Lx0a(NobdB7D(IEHpcD&npl=T-5HG`on?}7&-6E zZg9UVl!y+A83bWR#D1h);zt384c8!NMHzIIL}l11wc0bp!il`$B!nq$su@Cz^golb z3pwE&C~&jS_Xw6*mSw}7f^0A}Y;$gVz~Gc^hLQ6pjS z1x&02X-8`uB`EEOS}g`$yVrC&`FDDVmI9~RI2()Di+Om-!<%%e2B=_bTVXe;BlBOm}}v7R4RXhmnmWv~ zS87gz!E|=I54BoyVagq7;A@_@pLpFb_rAD?A38#m(F;wn)9P|TW@7L(WK;7QE%HH8 zbA?{QD5Uzs$CX)*o8^FzcYRI`&!%UboaPgv#m~@C@Dv~7dk&-n_wf1!M1_@@Fgplf z(xJ|XENYkhSA$DoOM+LpN^DAEHNUO zPjK@P8$u9AU^xyw2>6mK3%KgWaUF)4%qD zuM(()9y#QvL?d;3|M?4IVc#*<1&5F~%^<|@jPmSr1vU-80UY~7T}j8#bzwKCi3g3* zn73dh39bA!Ks{9#!CS8ZGZWFQ81C+}q333CnFLGZ@lHt-*1Vs@b9=608%wKYnXIn- zU}$69ko6XQJ+%(;P)c1kzUn_e6N$Ar#41o9P!2cBLRT@u{+hO5Lo^GZ2xOuVZ`O<* zG|>Yg&xv{+RxAbDZ)eatxl=aGGWRBTXVe%o&(CwHj$4+~P^O2g!LGaT^TcmUky$p7 zOQPt*dL&`PhoQ%kd`m}Dl6{cn{{bTsA3rfbs%aZ9GuE+ESsX4o&$mNtvX()#2p%uW znZ$ZI5DqC`KVe3qSerNMS=gU1YQJg3r};NdfuDa4DmU-$-iOz?{s>@A(Ho#c2+885 zdZoe2FzetY;GeAK4G<#v&pq zC~sIGjW4co1#fNB_UnL>#&)3p4NwA%QDRIOjH@lEFsH$h{x1Zm^LPX>qi;2u3I2=) z2?^m^Gdez>w3?oE#Jsjv!k0VhPg!DZ$pwU{B-RV8!p`EI-4x{TBAQhwa_EbB%giy( zH#iamj6QTpzmN$mn9d$>fkWSMQGM1DQK>yOIeLlJXvlfgc0f9nQv@ z>DT`YlF_W1IenUjPM74W+1pjUc9|t}58u*^1J$atQI&8UgVFBy-C#MS_x~A#f`LK8 zLP3IqLqNQ5XoG?Qkik)iQJLaMn1z(`XV5+w*!#!l)o$+~kqRrv=GH;5D5?OzkqLed zAQEwyMQ3C+>@{+%zqtIzPBDxC2&Ffw=p2rrta&6;5$j;e|6=fw`_$YqB>L5W&`51| z_XFi~ug}OEfD`S;A%>$Xi8UB6Npu98>H+t*Wu>pBcp0 z#Qr&@G&@v%EIi!1qHk4LTKL zS$8dbv>shCEUTkKKOQ8KDvsm^ZR?gZEjp9s@xU!o^8UWX1U zd=0PF1Zp43374N2qJQ{U{O`_imvSqB^HRho^|N}_a6aqt{}c@9djlBme%dnGXGkvV z+UJ{}NPQ>^)yWpj^Qo0#AUh{GdjnjsZSt7ltCw#eL}kgm&{2^K321HH7~fWIiR-@u zv!`!>w!h^6ApGwG=zVt@9&9_VE9~iPn+SQP7!KErRzz=E3iV#NpZZzds^ILV<52Rs zZabgBdKKAlo|_5}=!N0jSy@VxJT6&577O{3(PvsuEn|7-al#ib70|dOGcr>hDcLJf zz9vF|yfdB~bRVCx`qVuC0Umy&emn?@BL~MnjWkN_;5ga9w)Z9#+0;61wT~fwflZJ$~Z*r?>XCyPGkS+ztI ztNBuD!iGrun;Iogopf(_G#$8j+gv|dt7e|5nJ2A}MWb+}6F9N<-h;KaB#do!iA1le z;Vytg_EBft!IJ@gI~Me|%?;0v#PHaeM)+1q(u>mlJI^wA@2r!TJhE7hJZ=;L>X)*N z=j@K?%!PJuV1bi?-B`kx2bHTQzJ+kRA24vvtxd1dbP5dh8_Hup?*E48?V57_-vaU5 zkJ0^TyO=P6S1XjM5QsH!yPo54?=&Beo!2w~mQJ-ob#U{uLZv06g?A)%Jfr==!z(dd z-QPuan<<)<3I#6Ciuu@^s;Pp6f?^`;(fMYP>ELmhg?+hIxk0aUXheafKaxOa4!6qo zbQ40}1<6oi5ncwV40w6NGx?aA;nyr|gDEF~^J?Jxte}yS;~dM2YaVWP-b83fmvwNn z!%N%`{K^8<0?9Vdq0f16I<8PX^rH1-*Ic75V0oxFw+T;ETicE@a^-ST6bs%ysxbN) zc7dIZRXDZ4ssH)l2&1$(cE1M`d%bq5UFN(yuGtq8$%X%|$2|V-?FY$!7rH(Nbe!*if`u6PB;Be@FZu@clPI zH+FxmZmYi_IQj5&_(WM%sPP3H&~y&GnLi4aH?anDkDful`;tia5O^@k$EB64vb;}J zelU*nDr7xB%OyOz0(1b;-MMQ#J=u{kp63wJw#V#_tLIL=`!M`@cm>YU+QHRahIWKC z6;d)={>x1FNLMwGVHBLOXTCrxRXL>}(z+*x&LcjXMz|w>N_*XtqG)eCU6h@>1e0>) zxUw)L8kPKc31^;$UQPpZtT1HD`fd<3GUNiDdU z_0lKQF{3(CCUW;hfWGwS;a%pMW3(Rx+qSWF>*v^&mXAKc%zOUDiYi5Rouc8%A45U! zKhb$Fwk*D2Yza#_unVVifD`1=Ip54Ax+IUbV@_Ik$6LOte2X14HfRa=-`G;KivCQ` zEu4Pcv0+m9xR!X3j>6%$7Ivk~Q^!KwAosb?jn48@yAftxDgVi;c(p-7{}QbSpmA+R zvaXLcj{-O#9cSIP`J~ds<%mmVRlaE5;w~q~Uh1_&+4SVBwW+%>oNh@bsIk0Hd8V~g z(^=+CDniVKGa!D|<`9yW5toDNM3cMjd~`LZftsL!B6R|=u&^dWNT?xApWJGRs^;VM z$7cy%?hLrCZKj-4MkZ-ezO}ZAO4ly%VWXPL%s*hnPIl|8^rk`q3yH~Jxu*BS-R!F> zBnD!qGc%{-5B6JnOrqJY5)1PoSjYy*e+uxK61s`>opdU<{H0iPkZo6eK=vYDi;JAd z65}7)WZCxUBVNR0z{X4jsE^<0=>$!tm88wPuh7$sI3`m%j*qo!jndihED@iU4JY^^ zLZy9u9`kpPJOh}eU5yl<|m60RO6N2de&Sz&*d82#}dC;Vn9}#DC%HTB+5>K5Kv-G z@q>ga8CT8HV^LydNi%o9T!T7+H^pK_fY*GzR0VyJA|NJ&J+G;cLBM0lmw}oIcU;9D0KA4kAB{^qPrEW+e__!}!6^OG4*UDf9 zjbLGPOGha#6?_^TCRuK)k2SC_ncht0!dkIM=uTTXJKl$N+*huxO7Huia&%>|H)Whsc|$X)EB-hFF1D&kM=HJYI$6bb%6j=?f@^BPlb3($ z_?b&^t0;?JBe~$EGP6w}XL0OX-p~}?x#M+)*5dO|OD`9!dE+n*X{dJ=*e z9XfT8nBA)fj|{Tfy(abE=JAH;ts*QIt_0K=V9&eUFUpPxQMX0?8D(-!Lt3vug?S6( z2u(n@)Np&-F|+7(g=~Gl2qVQ3&hari{H)cSB~`?Saye)22gK8P7IY~Tj$~QC&bphe zGuS>L5{fFBrLhADx21Q_PxRpaT%P)%OA&2frA{FR3e$)@@)fIo8}O}}PvvXkU>?~Y zk4lOC$tK(nF~vw0b$6A0y22$=wZ9=!70 z)BV)C{Lt2&5`T*m%CDFzzf(+d&NBck7>}!{?O1O|vNdemo%5vw_FMCU!i(h{g80~U zWZ9pVT2Z87ySLXF%`KPawsO;d=uxX>^4CGVYUeyT(xR7H=a^I<1h5!X^tBm&=@( zu9kZP6cu&Js-$GuJg+?PZvgw~3E`LB#K&^>Yq?L{Sugc09Spvjr0HwxoYep#f$R)aCi~Nv z_o?-g@|++>lV7@}I1LOQiM5~D=)UV7#>T81uURc{kt#x=@e}iwh_yLUl#XyiI7L6W zV!+z!+U-%5Re9Qm%93O%C}Mf#guz*GoQpsCRH<>-ZPRJ#aRpJdEtmr4l76~1Vwz)+ zNJx_1jDKDKp}Zz(rp;GpH7p4-JD8Bgm$>F`4&S^s6`Zg4cGmIwo0h=t;_#YoCHOG3 zsfL4g<8`_c>=0Fvr*WMW>sNYj@92COcqLZ%C4Q2vXiA~|l27WvMu8P9rfRh6X^XFA zU*Yhs4h*u<6Pdqy=e6H%Sfuu2RjlOht)D^z=b0m^SB0D->p9_>GdRVTdmY?|Y}wKEj=t$M%T5X9=MDEW!( z>N|Jha(ZsJ{HH>W`y;lEJ%QwH-K{JdVCl$~R*I%3t!629(5 zdMa~&m{tm7-J=|_3jr^flqx>azH7{S>1tNkNFz(I#l62c-Ws~5b}cFRXD@!DU>EZ5gYTbBA__@G5e?uD>8Qgohh%VvOMHr`sA zh|2mg2gIa#oS~hdDi#O&zLG0z;hDi)%yDY>2#Zsq5ZTsb`wid8!eo-+IWn><^woQw z>RAsDpWt}<`EgvC;Hm7gwG;EqRp*R)?F>u?wNsVrULak)(b%xsW?BIAvHX;Q)kMc4 z4=POBzzn;)6UOl;3rz>wNM>OLV%=)Twin9cqBIdaH`hJ{pE0fb?b5>}c?lkdWwOY= zF^vdOvrAf9b-GbMvDPlF)cxjFPyH*ws)ADMoeY*llnl7j2D9 zhG}jOYV#lQtf%OMihD>N470z9N~X!&l&wW*PqxatUwk1~yHV7T|U>de>Q9_~?OvEzsLPU3gZ)<$5vm8KLogfU@(&$#O9n z2-{C)KF}6d+m&%^(<;hDVN2=wUgdSJ?mZ;miO&1WDmJvMYP}G*I)$LV>X9xpUt>zu zpYmz`nuG__O$Zx~Aam*Y6gBiQMGGs0GNx!lWSH9gAkHc|u>#A8L)#c&W{FC>cbcZt z{OtDz_@}WxSc)M=$5B0|t@a3=*@QEt=fXUl;!~f<^i~kG!#3lNJA3l}7W)@nu8^ug zegw3OAz(2ViqUab($s<+S}jk9AuS?vOojXN;=!`N-UrjYE2N)H6`4NTDmB)I{gT*) zw59p6iw>{Oa77}PKP9fpox;rnwP7fz;P%X+sR5ls`no`SyCO``y1ud6iLr&s$pfeP zIkFlcaU=<%gGT5%?9CQe>7h$DJ}V08u%`4u+$#R!D2M9T0aVV3b|DWW-tm|QV-#jsZnl|&(w{_Zd$^oOIL@11X0o`dDB zm6EMU+PndD9x_(fi#jJWod{Wl65)o&t*pHdhpDiYKRi{$G1nd2*l}z;d!B1=HtrBQ z`!Z3Uu!SKDP*Ph(q{|E6Ao#-S29s?)6N}JKyRB!c&na4wlO_^at$4GBUiFb!Ilcsr zLesqdSRnful8$t7wE*fM;%GfKT`iv&#^R=jUT6?2t_H!O;j1cnnVGsk=e?kxkNPM zfvTQr0X446qr)DYiKWgfTHIdWcv$jD9g;(wHq*phVd+m=WuB3oMO(Wdxkg#-_$ zQe#;>^E44i;hwEC$R~blOUxnyHzlIFt1tWA$px#i%FLn%y-U267Vc-^y&W|p%o4fS z4vBCxqfe>@FgWxIFKfEpaQ@0a*q38*+5G2d%&QaB=wj!3^ViBMmuZN)PSwZ+4PG({ zs8_@a4b$$sB`EW&YLyEwTJKyGupHc*n-7)O>a5;w#rw)hQN@Xx!VRw^ zcHPiaOCG97fjdaW63Zyu}?b$Q2~ygu}wCT)JMwgg2C(a!0mmp3k(P zMZYLdPbj~oS=vMUmhd%z1E;Uw6t{4g#fL38bcDpnb(nr^UP1Nq#bpSFS1d@1yCnj= zfi@=$7G701A1#!*J3BlSjjp^tAFDM)MKFzVbKC+v#I#?XWZw5J8Pxq;!oWT3sMM<) zck8a2Wv9tvYhpqAPOfNGAmh|TWPyb8Tuyh!t22sZ7R}f2-fd>cJ(|I$agUCo8aLmv zm#u{;iS)9jPeAz|`4A9;U(yNl( zi*n6%kgQGRuG|2Q&VrSXkWaVGS+R;b=^vYlmEQoU<@s>I%62}%u;KI?=>Qtn3xU5n zj51d&Tbb5r*S3YvH_Pc1^z4bXTIl<5S>^5x6^4VnNK*bQQU!Gm&Dz;_tfdDZFc{$d z*+YmB>c{EYbrwjA%nQS2YGo%rSHA)17v%(r7MUF}4p%1^o4)D%Yse3;+rO9!{>*=f7(4E#A86GH*&iQ&*42zB=&1=+T=|e5R-9H5957{FXZq#O^ zy~^9taq~ow5g&!2Cdk?9$mXwPykWt-u_{np1 zo+P@yJ8H*|u!Y%&VXZTUqdO+XvwG|wFR?d9sL{b*!9TRRr+tU}c?IiOmwo4TKD?Zi z>W|U%S%T=aMU5D~it&?zBhS?ndk*!1S8eD`X$(bd(;W0WGp1FUzGY;&qT9Wxily8P}U z)o1}qaDo%s`qH)DYj`>tOBjVC1yv4gf56uaFiWtuT6sroxGRurQstq$ceTbslY}II*Hck&F6p=7})0A81bm8fAeanM%)W z89c@)b%}@ zSD~*UW)dyGAS60MbSI>L7=IybPYrZga^Gk_qGSOTY*v2*tXg)sjMKF}b%mvI1we6e z_(|2yYyU`djotJ5NxJ&O+9r|%ocIJ}A~cLpHntw);JG?s(Vq?yl8#Z+P1G zhr_LnaP&4k5#9j3{C}CMH5_R(GAbxmj2;`87#CBfzG5~pnAZ(+HynD1$u%V)s-!w> z%_z!Fg%1qTI)HOAU(w6hU<_Nr?-)Lbd;Lt-Ov;osgzfDQ4a?i( zigg+oM}wz)KPDKcf*da;=I~Fsb+-iQQ}FGLUYK~NCfsN}l`=~t80)lJaaYc0nAbyR zk|i=uy6%sSf=wM%AgL$Nh*5tl+D%V@4B1j&OG%M1H1t%1bgLQUWj9)3HU$-D1_&IN z;0Ua|V&HzhVA`|#;ZrV>EFfl@OxiAi`W-)Bzs_)zH+e(JD%+$r{}Q2D4MVCr{#&+@ z4y9gxnr-Dx1;t1QRe!79gjbTKNxUOiZnKyHp2YV8;Mbv%q`R^1%$IVIgi;#rUsPNV z;%gxcGakK@pZybnxr}UQF)!&3!B*|%utV^?lqAv_1GKFmue~)>{B(?=?5NvmLg}y7 zHJi$sO?9N4HM6~IZveu4kT_SVCS%6uJ(?VNh~^tLC|(9|>||nUZ5~J<6sJE>Le2oJ zY$6*m3n9i)?Ez_XgDqs`o7Wh@@DLE;p#aXZQfQmG%z@a8MKW+JH-xF9_^5Z-KG=jw zp^nVxX#WPzNsYjaDGQ7QsGCgh-q1{QhI_zKL7U=5v%TQJjn>-6e^<4;B7tzuR?f#&tj60C!7lVZnGyq-NW; zwtT?c(Bu4J$d}Ds<$hDp1KM4VZRP$ei78b(>4(=)@p2K9Oj==_eUocJQeMYv4G)5CKH|EZ}r@xP@bm{bdN388Gs%1q9WOf-vqF!<_|6o+l$UB`=0pT7Nt_MBN}AXiV$k%7UF2d3LWUy@PJv6l1&;+ zaBHhYcfed#E#q0U(IkuWc}YR!cH{NxIKmIH=(s=XcmrfVCEZuLg>2}LOFc%)Um6cP z9W)a=8O+ikK)O*%tZcUuC$QZ>+21@`LN#2BV}&G)-O`O7+B@kxQwUG{WDY9+kS00_$2evQi4ReZ(CGS-MMRhB4#8sCT`WvDr*y@OPNH^o0F-F!prPI| z$~4kVl7BzSzvYRm*uRB<9Kpq8HF4%?jNRCHTf$Dp17Lw{=?a-sJ39Q65`~$u@5hOl^v z%Sl)TLappgmZ4ocns;r6_pwgEUpEago7C452k&=pchy9#k*sVBw+1bZ3O=7guneGZ zV}ntyjx8=76Tj!$(g9xe@t2pD{Ks`s}S%S&p($e!AV7TQk)cgn#i3m%_7ieP^~D zHgBtup2{D>1eB`L1>T+5{C{1Gz5yiub&PwB`fE3FD}BU#!T*BmoJ?BPX&Z`skVw$N&NEqlG-2T zke}{f#NUtGY(2=Jr-0HQwk6TCY{+0@pBH|Oxc@XT=vfNf<(sdy&pOd)nR2GpG5!A{ z!%*_@`?*t#(_-7f%x)Z)XY;koe}FTH%~#%pUayZ&3HK_Mc$J(@%9d9e%MbJ)hwZxH z=p}9QjYcZl&ihqx#YZc%zZD3!Eg!9)A~rG?ZvgL`Ys=!i zB*C`LInD|1hsLEjB?5;g#XY)#3Tsa%*r<;?QZCJ!8c6~-=lXm!tW*Nq0HI zQT4t#%YlGsF}D8);kK$fpFQ|{ej@2CHNCji{kWsa=|tD77Q7}Hgo(qDw!ZrntT4Fi zSmH9W_v052+1R4+PSqVk*O~W2O%?c8B$anv3T&S}^@INZ&%f7!7(<2r6E&4L@C~q{ zBy)=SIvTF8(S4`#>2crL%V()cf&1^YWGj@?#TZ7Tfy-c+v5p z_KPB?kmrY|uW;$qYQre2wY`bN?;3x;a0XA3)VMTWa6b9*f0-<5QXk``&;0N|{ zVpVVSG{`KMj$}F`3C68NSQq_Qpxz!Io z81XVI2g0*ubQ+D7)l5K8T$kQSrb)~DTKXnDuplGbR@eU*y6-Pq?@A6lD!g#IyR1?3 zJ71vb?aH&o5*WiTKIH$)fI9gG_{Hz_aD6xx$ZydYy&s$=d$GZJK5b5~f*wReEbGSc znFh|hDb4U%;Nor>JdLUg-)Sg|O#_zo6Sq8rythW_9(BNDtHVr?NcrPfGQCc#!~TCh z84=V9!wZ2UaD2+Xk1Rr!FvvL%d3MI#*P!mRiGV`;co=;$MZT1UTZ~wa0VpNPE85j~ zU0+KFH5;SPpiEGnbqcS{sxeJJ$%`g#4$kW zQ3E|?Pjym9heaUb65v~EticrTqu{ZQM3CNZ*ufy_WXsVZki|t-1YiDc=-=>Vqs9F` zeTr})OQxEaLm`JoOmKpW#|=&_NDVzG``VQ}1`ft((VKLaKlUm+34&%P}GL zO?1Sg#NdB(!U&sasrOK^70CCGvE3OL(Jxcb?=DvSw!;^9w1gk0AUh*|M7V#${Z51U z?w?rou-A4WJ){%X%Q$8Z#_U+H*JgMmTms>}0M>roM+mJ7sBX(~^(OtG2%m(nLHpwo zX3_^>g*Hsd)LjyjqQwOt7{(PE_9IoeRRl#*{8GuTHf5I{nVi|tl-1W|_W9$;T3)l7 zK|V=f2Z^-NK|U5zprTEnPR7c_|NLi`{F~d*i1Rj;|A2!lrwz0VB$|V=MwpUH04F}S zr0^Qr_xANRqP)l{5OAi=FH&uxiN1*~nRT$Kkd$}9Ha3hm;)QUHZHx=?QPqosxh7$f zVJyCbSc|W|# zomJ74X2mQN>nW_1qJ)Ew)nYQ_f-z+^|84n}3waq01E`pxMDRHr7k%vUaDK2n*-Cp+ zvvNJGdHvnAJKp=zmpKg*<6Em~e!1B7pAl301_>Y#>fwhA^(QO0R*y~I+WeJYT^hj^ zKfEdmfg`;?7>WtC_`c$A0IdLMqW|Rc#ap-DhWWxHB|8(5`gPCwp=bru%N7g)#Oe(I z!rEDR(>3^vOvc0Pv+Vo5jNNl$mx@L?R-tBR348yRrDhhqPsV5kr`Ny3UH+ZbnGOD5UQC05i|0fNK{!+rie6LJJ1anQYwo) z)BUrg{)<1i@N9_%Sds|tviof4(zKarO#?6mnY)?-s}tcP&5x>dqxbcp1RVzRxSV06 zFaI|Yq$r`(&*Ke3v7f1e&n6cOL6Omj{fF9-VJT2tS_ewYN+GkwLFX^aKMDn;*aGi| zIKgv~#0Jr|WI9fB@!@%Qsw61!xYQvTN*2&0@(fcSw>e9=U%vPajss7rOC(`~WG$Rs zPRzhF=rMw9PiG-A_qgo3Q^N(rdDDbBDPMx7cuHna7|XIZ?CA;D;5idPZ*$P@6I(|0 zFg#*oib+LK6JPPNK67s(oEX$)T+^@?UzQ7jVw!RwZrA(-{xw!xiJIM>x=q<4D8)pt1s2pHvvkqaRj!2L% zK^{@vDM3Y3J)@9eiqQ|JRk7(bA)&s7Dvko}WenQq)H63aM=M{A{$BMqc>7{m*gAG- zw62S1EdAM1PitJ>EZYyJUA~_wNcCRplnd+rM{(C3)YQ|iLk+~xLNP!nyi8mk`TeSAfUREfrq2QxN^_H*lM-uN(Tfol&}G<$4fDwBQ|#0>^qJRN2$|8O{5Av;L6w*7bht>Wp(mR9iQWS; zLhSkeML+MZ5PFsR*-{U%vT^7&@~s9l;aehCfX34s9y&EZ%=PS7 z=ZW=p0e1AUj15wqpC8F?Xo&V9Q({Vxg3a8ttK*cK&<5gk9|t^&mgWpi$~Kv-lx&mV z5SD*3aq}TWv3WbQ5)D1Ro%^=f9qseaD3(Wd2aYMe1@l+;6 zfHMPz9_|Q?Ns`bMeJ$iwdn@r7)|uhJKoN~VQ?JI5p*X~sY-a_~GH&Ji%-CQ+Y}GuSLO<*=N~* z7>Hfu%o#P1`cVt@HS_JONwT7ahl`c(&sfvTE$n*A!N{+b<+S*xhU`Eyl>=l8upvTf z70zp?E%Hx3&WS|Otji-?Voa(TY5IoD`_iA$8kCm7DmfLa)`omYh2+iJEKhzN+f~=2 z_n1_r#q_K&XRm&Q4D{w@@kRBxH7Rc4^a3eky{TwHA*$b>=6JT)35^3=Gp&E^^;Zxj zx^^P7EWj=Z+b?P{ES*a%I4n>^@$BoHl@1MWYMRznz4!#`NK_UBkNJ90=C*W82qhbO zvEjc8x3#>Mjm7aI++*4UCx{amHc?*=0+lD}*#tVDl-_7e7!}UW17vAmf?G+|8j;&Y zlP`0viiPK!H)QMyVQUqyzoi01+(v?uhBL}FlHXN#3@&mlm)88@4nPKa+2u8uqX_g+ z{#inn#bj4TzM8LDRKxx4b&cxlSTp;}yDl^QyW;NgE{h;V!&~OvwKJN+58J=s;vd>( zCXxDS;kUQmJtyT$HG&Fsbebq2EDJ1s*^)wVDXc5UQo!B<(Hx zHj-&cmhcKGnGnzv4xS_gi=U811SE4-wWmC9E^wPChSnRk&Se!qL2@j<*&EbWTo) z2b@&=_xbuexK9K{k+{u!_HDqY$eYu2dYo5)%oV}zo3J6B19*YNwK@l5L7<}6z~$u( zM+1kcEyh_%)jHMK0m%N_#z`gV&NIpWU^^10e&i;fwf05r;Ep_A!hixc!Mx<{aYK_w zyRMEapEFS*G?+?+0`QHGR`&Pehz)YK4=puIeC6#0pr%nN`Wa+>+ezrKvBxT)_^I%Q|;9@|BFUe0f2q=#5u zs}eV)z5)~~Q$7(d*gW*L(8qGe984KTvo(?E$d%MMi@!tHfr*03U*}H;L(e|#9P7ho znRUrOfBC~tUfIYkYShV(W9Zb)xrmaKP$nrjMm9i`y{owoLFu}q;(r#m6a+}Y^)XI5 zR`kS^?U9mK02i&8?UUon3TuX;knRO8C{`W)opoqiWywB3GR!^3^37{QdP8Ah>`=0U zXSgXJzr4JJ2BNnJXj*pu0C2DNP_;B;Rj1N8)w^jZ%@ z+g}`iz5)zUFwu^@CKsOIj62QB@=KD}$Wq)>7Lx!8F(_I{10yI@L#trU4I zj9J-!9mz5(`WR}KNJ-?v*F)WGvgn7@8`WV=GyF3m)NRn5Qdv+vIbpyDRv2y0De>qD zkUMSUIsq~sCEpR4uAx5z?$*VrYsfq0d|@Fn%@|mu6=@@(4E-Fdfi(FbYOX3J&XxPjxm7J$7pQbDcEVi?wBe#yMqECYit#a~2 zEokT1OT7}#E9QDK5=tTK5I^~f65z(rz>^a@)ZaMw;XA(%^4^HI#vQw(Jsf~I9t9)| zfD3vKBU0SG_uOVCM;3g&JN8GqsM!k?R|x<1Fn=`u!Od4sV}epJnMchr+0 z2%XMcBll`0K9@NDdz1tRU~E722ZalvmU)!q&ZFY6ytVxX%V{Sa><%o}%0tA^UI%z?TCK(L zYjqSBhJb5kdVO4KX3D$Y=4bkSO|VK`zws$!@Cq;z4afGqo>|ef83;WxI+9I4T=Rc6 zvV$9h>+Iu9^yFW`$ps@tdlGgv7?Suw(`pTmW(gG)NV{(0oWP(5lu@8 zX;_e$skIPyWGJ1A;yr;TM`~;^(E@P618DnEF=t6nx!RR$N;5|6qB?lKVWio6B=9iQ z7nMsK|5h%;GQu{f$3g*m$n~3XdOTSyp=ilE)y-kY z&AS^HY+0!2{IN#b!r`FcC=R5==*d|CK=@V+oueKlu5D)qPi3QlSv&kl@RYG419j=NQT{`*>R*SW#^A&U!4HMJQd;+iIm@Qk<;9H~E5$cjA z-El}G(PkCYCh;WG=CshuKig>cL2)Y9JIT~0T(=n?3BYLTB*Ntx`=1Heh)i5WYj@KQ zJ~9{DvU(|d1=t%R1eMr;7j4*C1M5?1Iuk2^O%Iaj@v^`D*(GA8hbq%QM60FG)Bvol zLQYBtWzl5F|JBRLv>TN(dZ?Cxcv!uRyit64X`3>ZXOjUE?mYxwTOP|U76TiMdtS@R zFx5>K^Pzbz2-&?ZmT=^MA5Ht^z>YS5wX?}vc%uXu`zi(FPsa<{4}=+9Tb}w`?zVmx zu@TiG>373_y#TTLTa<#ya_97jEu;E885-t!Z2)eH8FMIXt~kD)s9Tci#Xy8xf#K@C z{M4nVCA#Y{b~`N3!U>c1;c{7Jq?NSp@6A+i<>VK!UE|Q8^~xXF14$;hfjz6IR{(X# zit@p+dOt6)zS|}zTkz|&>tR&1=2?*)U}Wu&5OjrG!$4~p1zKyNVVCtM?2+qD@8yE@ zJSMKjl|EspuWLd@kn{51$pI8Cc5o|A+mK7@c@F_@dBjEWC>VaYj3W(@wJSbf;8@WX zG}ia6r%8G$|K6YpE@tBDQ~~uaYqGpk`lw$1G&%!os$<=90#%Sr)db()GSg-8r8Q-v zSM}Kv=%LvCutgMYYn8LYnEQk5+-Vo1+A{kGZH?h3$yQ79Bph)Q53sv?X~g#i}!{Zs|jO@5G8LSVjfGf!3POzO!qW zBO!Pdzrb{d1SaXX>`pAyr}+zTj^+WQwO%b^wcc#|jljKIny7a^b|&aix|Fu1U#k){BQU}szXGT- zOlmImNeB`e-j>OyIMb));VH^Ff(*sDiSum40vG2l$C zjCn<>-8pKQGM8SNFVJBB)&Kh?k7X0e>&rHz3f7?f*$;0De~u-wN14inr!GXIe(`sYGTVYGs!%zCZY)UNCA+Kb*_lkg69(=3=Q!#BMB~agoKdpjRntU9~TwR+nb@ zJpE#5yaJyUVoPEQT&7VjW6KOo7iw{`Y-_te-(PXF(Ci}>t+A5w&aaxG;s5KbuQKly zp+yg+oJRYk^L?iQoWznmrGy?`kpMG z)#oC@B6t6Nnz?&E@ANra_EBjJjqP;)9D~M(_rnB5ika^3J|BOzsDJb*LZXAv+Eo4( zpoocx)BuieIEy}_=`sIS7WwA}r9#)DtpDBN{x$Bu^$0pT`Zol_um3gtUl#xC3<3P7 T*@Qh!)Bx!}$yJEdu4exOJ`PiR diff --git a/telethon_generator/__init__.py b/telethon_generator/__init__.py deleted file mode 100644 index 8b137891..00000000 --- a/telethon_generator/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/telethon_generator/data/api.tl b/telethon_generator/data/api.tl deleted file mode 100644 index 6a606b74..00000000 --- a/telethon_generator/data/api.tl +++ /dev/null @@ -1,2078 +0,0 @@ -/////////////////////////////// -/////////////////// 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 - -/////////////////////////////// -///////// Main application API -/////////////////////////////// - -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#35a95cb9 chat_id:long = InputPeer; -inputPeerUser#dde8a54c user_id:long access_hash:long = InputPeer; -inputPeerChannel#27bcbbfc channel_id:long access_hash:long = InputPeer; -inputPeerUserFromMessage#a87b0a1c peer:InputPeer msg_id:int user_id:long = InputPeer; -inputPeerChannelFromMessage#bd2a0840 peer:InputPeer msg_id:int channel_id:long = InputPeer; - -inputUserEmpty#b98886cf = InputUser; -inputUserSelf#f7c1b13f = InputUser; -inputUser#f21158c6 user_id:long access_hash:long = InputUser; -inputUserFromMessage#1da448e2 peer:InputPeer msg_id:int user_id:long = 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:# spoiler:flags.2?true file:InputFile stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaPhoto#b3ba0635 flags:# spoiler:flags.1?true id:InputPhoto ttl_seconds:flags.0?int = InputMedia; -inputMediaGeoPoint#f9c44144 geo_point:InputGeoPoint = InputMedia; -inputMediaContact#f8ab7dfb phone_number:string first_name:string last_name:string vcard:string = InputMedia; -inputMediaUploadedDocument#5b38c6c1 flags:# nosound_video:flags.3?true force_file:flags.4?true spoiler:flags.5?true file:InputFile thumb:flags.2?InputFile mime_type:string attributes:Vector stickers:flags.0?Vector ttl_seconds:flags.1?int = InputMedia; -inputMediaDocument#33473058 flags:# spoiler:flags.2?true id:InputDocument ttl_seconds:flags.0?int query:flags.1?string = InputMedia; -inputMediaVenue#c13d1c11 geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string = InputMedia; -inputMediaPhotoExternal#e5bbfe1a flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; -inputMediaDocumentExternal#fb52dc99 flags:# spoiler:flags.1?true url:string ttl_seconds:flags.0?int = InputMedia; -inputMediaGame#d33f43f3 id:InputGame = InputMedia; -inputMediaInvoice#8eb5a6d5 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON start_param:flags.1?string extended_media:flags.2?InputMedia = InputMedia; -inputMediaGeoLive#971fa843 flags:# stopped:flags.0?true geo_point:InputGeoPoint heading:flags.2?int period:flags.1?int proximity_notification_radius:flags.3?int = InputMedia; -inputMediaPoll#f94e5f1 flags:# poll:Poll correct_answers:flags.0?Vector solution:flags.1?string solution_entities:flags.1?Vector = InputMedia; -inputMediaDice#e66fbf7b emoticon:string = InputMedia; - -inputChatPhotoEmpty#1ca48f57 = InputChatPhoto; -inputChatUploadedPhoto#bdcdaec0 flags:# file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.3?VideoSize = InputChatPhoto; -inputChatPhoto#8953ad37 id:InputPhoto = InputChatPhoto; - -inputGeoPointEmpty#e4c123d6 = InputGeoPoint; -inputGeoPoint#48222faf flags:# lat:double long:double accuracy_radius:flags.0?int = InputGeoPoint; - -inputPhotoEmpty#1cd7bf0d = InputPhoto; -inputPhoto#3bb3b94a id:long access_hash:long file_reference:bytes = InputPhoto; - -inputFileLocation#dfdaabe1 volume_id:long local_id:int secret:long file_reference:bytes = InputFileLocation; -inputEncryptedFileLocation#f5235d55 id:long access_hash:long = InputFileLocation; -inputDocumentFileLocation#bad07584 id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; -inputSecureFileLocation#cbc7ee28 id:long access_hash:long = InputFileLocation; -inputTakeoutFileLocation#29be5899 = InputFileLocation; -inputPhotoFileLocation#40181ffe id:long access_hash:long file_reference:bytes thumb_size:string = InputFileLocation; -inputPhotoLegacyFileLocation#d83466f3 id:long access_hash:long file_reference:bytes volume_id:long local_id:int secret:long = InputFileLocation; -inputPeerPhotoFileLocation#37257e99 flags:# big:flags.0?true peer:InputPeer photo_id:long = InputFileLocation; -inputStickerSetThumb#9d84f3db stickerset:InputStickerSet thumb_version:int = InputFileLocation; -inputGroupCallStream#598a92a flags:# call:InputGroupCall time_ms:long scale:int video_channel:flags.0?int video_quality:flags.0?int = InputFileLocation; - -peerUser#59511722 user_id:long = Peer; -peerChat#36c6019a chat_id:long = Peer; -peerChannel#a2a5371e channel_id:long = Peer; - -storage.fileUnknown#aa963b05 = storage.FileType; -storage.filePartial#40bc6f52 = storage.FileType; -storage.fileJpeg#7efe0e = storage.FileType; -storage.fileGif#cae1aadf = storage.FileType; -storage.filePng#a4f63c0 = storage.FileType; -storage.filePdf#ae1e508d = storage.FileType; -storage.fileMp3#528a0677 = storage.FileType; -storage.fileMov#4b09ebbc = storage.FileType; -storage.fileMp4#b3cea0e4 = storage.FileType; -storage.fileWebp#1081464c = storage.FileType; - -userEmpty#d3bc4b7a id:long = User; -user#8f97c628 flags:# self:flags.10?true contact:flags.11?true mutual_contact:flags.12?true deleted:flags.13?true bot:flags.14?true bot_chat_history:flags.15?true bot_nochats:flags.16?true verified:flags.17?true restricted:flags.18?true min:flags.20?true bot_inline_geo:flags.21?true support:flags.23?true scam:flags.24?true apply_min_photo:flags.25?true fake:flags.26?true bot_attach_menu:flags.27?true premium:flags.28?true attach_menu_enabled:flags.29?true flags2:# bot_can_edit:flags2.1?true id:long access_hash:flags.0?long first_name:flags.1?string last_name:flags.2?string username:flags.3?string phone:flags.4?string photo:flags.5?UserProfilePhoto status:flags.6?UserStatus bot_info_version:flags.14?int restriction_reason:flags.18?Vector bot_inline_placeholder:flags.19?string lang_code:flags.22?string emoji_status:flags.30?EmojiStatus usernames:flags2.0?Vector = User; - -userProfilePhotoEmpty#4f11bae1 = UserProfilePhoto; -userProfilePhoto#82d1f706 flags:# has_video:flags.0?true personal:flags.2?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = UserProfilePhoto; - -userStatusEmpty#9d05049 = UserStatus; -userStatusOnline#edb93949 expires:int = UserStatus; -userStatusOffline#8c703f was_online:int = UserStatus; -userStatusRecently#e26f42f1 = UserStatus; -userStatusLastWeek#7bf09fc = UserStatus; -userStatusLastMonth#77ebc742 = UserStatus; - -chatEmpty#29562865 id:long = Chat; -chat#41cbf256 flags:# creator:flags.0?true left:flags.2?true deactivated:flags.5?true call_active:flags.23?true call_not_empty:flags.24?true noforwards:flags.25?true id:long title:string photo:ChatPhoto participants_count:int date:int version:int migrated_to:flags.6?InputChannel admin_rights:flags.14?ChatAdminRights default_banned_rights:flags.18?ChatBannedRights = Chat; -chatForbidden#6592a1a7 id:long title:string = Chat; -channel#83259464 flags:# creator:flags.0?true left:flags.2?true broadcast:flags.5?true verified:flags.7?true megagroup:flags.8?true restricted:flags.9?true signatures:flags.11?true min:flags.12?true scam:flags.19?true has_link:flags.20?true has_geo:flags.21?true slowmode_enabled:flags.22?true call_active:flags.23?true call_not_empty:flags.24?true fake:flags.25?true gigagroup:flags.26?true noforwards:flags.27?true join_to_send:flags.28?true join_request:flags.29?true forum:flags.30?true flags2:# id:long access_hash:flags.13?long title:string username:flags.6?string photo:ChatPhoto date:int restriction_reason:flags.9?Vector admin_rights:flags.14?ChatAdminRights banned_rights:flags.15?ChatBannedRights default_banned_rights:flags.18?ChatBannedRights participants_count:flags.17?int usernames:flags2.0?Vector = Chat; -channelForbidden#17d493d5 flags:# broadcast:flags.5?true megagroup:flags.8?true id:long access_hash:long title:string until_date:flags.16?int = Chat; - -chatFull#c9d31138 flags:# can_set_username:flags.7?true has_scheduled:flags.8?true translations_disabled:flags.19?true id:long about:string participants:ChatParticipants chat_photo:flags.2?Photo notify_settings:PeerNotifySettings exported_invite:flags.13?ExportedChatInvite bot_info:flags.3?Vector pinned_msg_id:flags.6?int folder_id:flags.11?int call:flags.12?InputGroupCall ttl_period:flags.14?int groupcall_default_join_as:flags.15?Peer theme_emoticon:flags.16?string requests_pending:flags.17?int recent_requesters:flags.17?Vector available_reactions:flags.18?ChatReactions = ChatFull; -channelFull#f2355507 flags:# can_view_participants:flags.3?true can_set_username:flags.6?true can_set_stickers:flags.7?true hidden_prehistory:flags.10?true can_set_location:flags.16?true has_scheduled:flags.19?true can_view_stats:flags.20?true blocked:flags.22?true flags2:# can_delete_channel:flags2.0?true antispam:flags2.1?true participants_hidden:flags2.2?true translations_disabled:flags2.3?true id:long about:string participants_count:flags.0?int admins_count:flags.1?int kicked_count:flags.2?int banned_count:flags.2?int online_count:flags.13?int read_inbox_max_id:int read_outbox_max_id:int unread_count:int chat_photo:Photo notify_settings:PeerNotifySettings exported_invite:flags.23?ExportedChatInvite bot_info:Vector migrated_from_chat_id:flags.4?long migrated_from_max_id:flags.4?int pinned_msg_id:flags.5?int stickerset:flags.8?StickerSet available_min_id:flags.9?int folder_id:flags.11?int linked_chat_id:flags.14?long location:flags.15?ChannelLocation slowmode_seconds:flags.17?int slowmode_next_send_date:flags.18?int stats_dc:flags.12?int pts:int call:flags.21?InputGroupCall ttl_period:flags.24?int pending_suggestions:flags.25?Vector groupcall_default_join_as:flags.26?Peer theme_emoticon:flags.27?string requests_pending:flags.28?int recent_requesters:flags.28?Vector default_send_as:flags.29?Peer available_reactions:flags.30?ChatReactions = ChatFull; - -chatParticipant#c02d4007 user_id:long inviter_id:long date:int = ChatParticipant; -chatParticipantCreator#e46bcee4 user_id:long = ChatParticipant; -chatParticipantAdmin#a0933f5b user_id:long inviter_id:long date:int = ChatParticipant; - -chatParticipantsForbidden#8763d3e1 flags:# chat_id:long self_participant:flags.0?ChatParticipant = ChatParticipants; -chatParticipants#3cbc93f8 chat_id:long participants:Vector version:int = ChatParticipants; - -chatPhotoEmpty#37c1011c = ChatPhoto; -chatPhoto#1c6e1c11 flags:# has_video:flags.0?true photo_id:long stripped_thumb:flags.1?bytes dc_id:int = ChatPhoto; - -messageEmpty#90a6ca84 flags:# id:int peer_id:flags.0?Peer = Message; -message#38116ee0 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true post:flags.14?true from_scheduled:flags.18?true legacy:flags.19?true edit_hide:flags.21?true pinned:flags.24?true noforwards:flags.26?true id:int from_id:flags.8?Peer peer_id:Peer fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader date:int message:string media:flags.9?MessageMedia reply_markup:flags.6?ReplyMarkup entities:flags.7?Vector views:flags.10?int forwards:flags.10?int replies:flags.23?MessageReplies edit_date:flags.15?int post_author:flags.16?string grouped_id:flags.17?long reactions:flags.20?MessageReactions restriction_reason:flags.22?Vector ttl_period:flags.25?int = Message; -messageService#2b085862 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?Peer peer_id:Peer reply_to:flags.3?MessageReplyHeader date:int action:MessageAction ttl_period:flags.25?int = Message; - -messageMediaEmpty#3ded6320 = MessageMedia; -messageMediaPhoto#695150d7 flags:# spoiler:flags.3?true photo:flags.0?Photo ttl_seconds:flags.2?int = MessageMedia; -messageMediaGeo#56e0d474 geo:GeoPoint = MessageMedia; -messageMediaContact#70322949 phone_number:string first_name:string last_name:string vcard:string user_id:long = MessageMedia; -messageMediaUnsupported#9f84f49e = MessageMedia; -messageMediaDocument#9cb070d7 flags:# nopremium:flags.3?true spoiler:flags.4?true 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#f6a548d3 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument receipt_msg_id:flags.2?int currency:string total_amount:long start_param:string extended_media:flags.4?MessageExtendedMedia = MessageMedia; -messageMediaGeoLive#b940c666 flags:# geo:GeoPoint heading:flags.0?int period:int proximity_notification_radius:flags.1?int = MessageMedia; -messageMediaPoll#4bd6e798 poll:Poll results:PollResults = MessageMedia; -messageMediaDice#3f7ee58b value:int emoticon:string = MessageMedia; - -messageActionEmpty#b6aef7b0 = MessageAction; -messageActionChatCreate#bd47cbad title:string users:Vector = MessageAction; -messageActionChatEditTitle#b5a1ce5a title:string = MessageAction; -messageActionChatEditPhoto#7fcb13a8 photo:Photo = MessageAction; -messageActionChatDeletePhoto#95e3fbef = MessageAction; -messageActionChatAddUser#15cefd00 users:Vector = MessageAction; -messageActionChatDeleteUser#a43f30cc user_id:long = MessageAction; -messageActionChatJoinedByLink#31224c3 inviter_id:long = MessageAction; -messageActionChannelCreate#95d2ac92 title:string = MessageAction; -messageActionChatMigrateTo#e1037f92 channel_id:long = MessageAction; -messageActionChannelMigrateFrom#ea3948e9 title:string chat_id:long = MessageAction; -messageActionPinMessage#94bd38ed = MessageAction; -messageActionHistoryClear#9fbab604 = MessageAction; -messageActionGameScore#92a72876 game_id:long score:int = MessageAction; -messageActionPaymentSentMe#8f31b327 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string charge:PaymentCharge = MessageAction; -messageActionPaymentSent#96163f56 flags:# recurring_init:flags.2?true recurring_used:flags.3?true currency:string total_amount:long invoice_slug:flags.0?string = MessageAction; -messageActionPhoneCall#80e11a7f flags:# video:flags.2?true call_id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = MessageAction; -messageActionScreenshotTaken#4792929b = MessageAction; -messageActionCustomAction#fae69f56 message:string = MessageAction; -messageActionBotAllowed#c516d679 flags:# attach_menu:flags.1?true domain:flags.0?string app:flags.2?BotApp = MessageAction; -messageActionSecureValuesSentMe#1b287353 values:Vector credentials:SecureCredentialsEncrypted = MessageAction; -messageActionSecureValuesSent#d95c6154 types:Vector = MessageAction; -messageActionContactSignUp#f3f25f76 = MessageAction; -messageActionGeoProximityReached#98e0d697 from_id:Peer to_id:Peer distance:int = MessageAction; -messageActionGroupCall#7a0d7f42 flags:# call:InputGroupCall duration:flags.0?int = MessageAction; -messageActionInviteToGroupCall#502f92f7 call:InputGroupCall users:Vector = MessageAction; -messageActionSetMessagesTTL#3c134d7b flags:# period:int auto_setting_from:flags.0?long = MessageAction; -messageActionGroupCallScheduled#b3a07661 call:InputGroupCall schedule_date:int = MessageAction; -messageActionSetChatTheme#aa786345 emoticon:string = MessageAction; -messageActionChatJoinedByRequest#ebbca3cb = MessageAction; -messageActionWebViewDataSentMe#47dd8079 text:string data:string = MessageAction; -messageActionWebViewDataSent#b4c38cb5 text:string = MessageAction; -messageActionGiftPremium#c83d6aec flags:# currency:string amount:long months:int crypto_currency:flags.0?string crypto_amount:flags.0?long = MessageAction; -messageActionTopicCreate#d999256 flags:# title:string icon_color:int icon_emoji_id:flags.0?long = MessageAction; -messageActionTopicEdit#c0944820 flags:# title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = MessageAction; -messageActionSuggestProfilePhoto#57de635e photo:Photo = MessageAction; -messageActionRequestedPeer#fe77345d button_id:int peer:Peer = MessageAction; -messageActionSetChatWallPaper#bc44a927 wallpaper:WallPaper = MessageAction; -messageActionSetSameChatWallPaper#c0787d6d wallpaper:WallPaper = MessageAction; - -dialog#d58a08c6 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 unread_reactions_count:int notify_settings:PeerNotifySettings pts:flags.0?int draft:flags.1?DraftMessage folder_id:flags.4?int ttl_period:flags.5?int = Dialog; -dialogFolder#71bd134c flags:# pinned:flags.2?true folder:Folder peer:Peer top_message:int unread_muted_peers_count:int unread_unmuted_peers_count:int unread_muted_messages_count:int unread_unmuted_messages_count:int = Dialog; - -photoEmpty#2331b22d id:long = Photo; -photo#fb197a65 flags:# has_stickers:flags.0?true id:long access_hash:long file_reference:bytes date:int sizes:Vector video_sizes:flags.1?Vector dc_id:int = Photo; - -photoSizeEmpty#e17e23c type:string = PhotoSize; -photoSize#75c78e60 type:string w:int h:int size:int = PhotoSize; -photoCachedSize#21e1ad6 type:string w:int h:int bytes:bytes = PhotoSize; -photoStrippedSize#e0b0bc2e type:string bytes:bytes = PhotoSize; -photoSizeProgressive#fa3efb95 type:string w:int h:int sizes:Vector = PhotoSize; -photoPathSize#d8214d41 type:string bytes:bytes = PhotoSize; - -geoPointEmpty#1117dd5f = GeoPoint; -geoPoint#b2a2f663 flags:# long:double lat:double access_hash:long accuracy_radius:flags.0?int = GeoPoint; - -auth.sentCode#5e002502 flags:# type:auth.SentCodeType phone_code_hash:string next_type:flags.1?auth.CodeType timeout:flags.2?int = auth.SentCode; -auth.sentCodeSuccess#2390fe44 authorization:auth.Authorization = auth.SentCode; - -auth.authorization#2ea2c0d4 flags:# setup_password_required:flags.1?true otherwise_relogin_days:flags.1?int tmp_sessions:flags.0?int future_auth_token:flags.2?bytes user:User = auth.Authorization; -auth.authorizationSignUpRequired#44747e9a flags:# terms_of_service:flags.0?help.TermsOfService = auth.Authorization; - -auth.exportedAuthorization#b434e2b8 id:long bytes:bytes = auth.ExportedAuthorization; - -inputNotifyPeer#b8bc5b0c peer:InputPeer = InputNotifyPeer; -inputNotifyUsers#193b4417 = InputNotifyPeer; -inputNotifyChats#4a95e84e = InputNotifyPeer; -inputNotifyBroadcasts#b1db7c7e = InputNotifyPeer; -inputNotifyForumTopic#5c467992 peer:InputPeer top_msg_id:int = InputNotifyPeer; - -inputPeerNotifySettings#df1f002b flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int sound:flags.3?NotificationSound = InputPeerNotifySettings; - -peerNotifySettings#a83b0426 flags:# show_previews:flags.0?Bool silent:flags.1?Bool mute_until:flags.2?int ios_sound:flags.3?NotificationSound android_sound:flags.4?NotificationSound other_sound:flags.5?NotificationSound = PeerNotifySettings; - -peerSettings#a518110d flags:# report_spam:flags.0?true add_contact:flags.1?true block_contact:flags.2?true share_contact:flags.3?true need_contacts_exception:flags.4?true report_geo:flags.5?true autoarchived:flags.7?true invite_members:flags.8?true request_chat_broadcast:flags.10?true geo_distance:flags.6?int request_chat_title:flags.9?string request_chat_date:flags.9?int = PeerSettings; - -wallPaper#a437c3ed id:long flags:# creator:flags.0?true default:flags.1?true pattern:flags.3?true dark:flags.4?true access_hash:long slug:string document:Document settings:flags.2?WallPaperSettings = WallPaper; -wallPaperNoFile#e0804116 id:long flags:# default:flags.1?true dark:flags.4?true settings:flags.2?WallPaperSettings = WallPaper; - -inputReportReasonSpam#58dbcab8 = ReportReason; -inputReportReasonViolence#1e22c78d = ReportReason; -inputReportReasonPornography#2e59d922 = ReportReason; -inputReportReasonChildAbuse#adf44ee3 = ReportReason; -inputReportReasonOther#c1e4a2b1 = ReportReason; -inputReportReasonCopyright#9b89f93a = ReportReason; -inputReportReasonGeoIrrelevant#dbd4feed = ReportReason; -inputReportReasonFake#f5ddd6e7 = ReportReason; -inputReportReasonIllegalDrugs#a8eb2be = ReportReason; -inputReportReasonPersonalDetails#9ec7863d = ReportReason; - -userFull#93eadb53 flags:# blocked:flags.0?true phone_calls_available:flags.4?true phone_calls_private:flags.5?true can_pin_message:flags.7?true has_scheduled:flags.12?true video_calls_available:flags.13?true voice_messages_forbidden:flags.20?true translations_disabled:flags.23?true id:long about:flags.1?string settings:PeerSettings personal_photo:flags.21?Photo profile_photo:flags.2?Photo fallback_photo:flags.22?Photo notify_settings:PeerNotifySettings bot_info:flags.3?BotInfo pinned_msg_id:flags.6?int common_chats_count:int folder_id:flags.11?int ttl_period:flags.14?int theme_emoticon:flags.15?string private_forward_name:flags.16?string bot_group_admin_rights:flags.17?ChatAdminRights bot_broadcast_admin_rights:flags.18?ChatAdminRights premium_gifts:flags.19?Vector wallpaper:flags.24?WallPaper = UserFull; - -contact#145ade0b user_id:long mutual:Bool = Contact; - -importedContact#c13e3c50 user_id:long client_id:long = ImportedContact; - -contactStatus#16d9703b user_id:long status:UserStatus = ContactStatus; - -contacts.contactsNotModified#b74ba9d2 = contacts.Contacts; -contacts.contacts#eae87e42 contacts:Vector saved_count:int users:Vector = contacts.Contacts; - -contacts.importedContacts#77d01c3b imported:Vector popular_invites:Vector retry_contacts:Vector users:Vector = contacts.ImportedContacts; - -contacts.blocked#ade1591 blocked:Vector chats:Vector users:Vector = contacts.Blocked; -contacts.blockedSlice#e1664194 count:int blocked:Vector chats:Vector users:Vector = contacts.Blocked; - -messages.dialogs#15ba6c40 dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; -messages.dialogsSlice#71e094f3 count:int dialogs:Vector messages:Vector chats:Vector users:Vector = messages.Dialogs; -messages.dialogsNotModified#f0e3e596 count:int = messages.Dialogs; - -messages.messages#8c718e87 messages:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesSlice#3a54685e flags:# inexact:flags.1?true count:int next_rate:flags.0?int offset_id_offset:flags.2?int messages:Vector chats:Vector users:Vector = messages.Messages; -messages.channelMessages#c776ba4e flags:# inexact:flags.1?true pts:int count:int offset_id_offset:flags.2?int messages:Vector topics:Vector chats:Vector users:Vector = messages.Messages; -messages.messagesNotModified#74535f21 count:int = messages.Messages; - -messages.chats#64ff9fd5 chats:Vector = messages.Chats; -messages.chatsSlice#9cd81144 count:int chats:Vector = messages.Chats; - -messages.chatFull#e5d7d19c full_chat:ChatFull chats:Vector users:Vector = messages.ChatFull; - -messages.affectedHistory#b45c69d1 pts:int pts_count:int offset:int = messages.AffectedHistory; - -inputMessagesFilterEmpty#57e2f66c = MessagesFilter; -inputMessagesFilterPhotos#9609a51c = MessagesFilter; -inputMessagesFilterVideo#9fc00e65 = MessagesFilter; -inputMessagesFilterPhotoVideo#56e9f0e4 = MessagesFilter; -inputMessagesFilterDocument#9eddf188 = MessagesFilter; -inputMessagesFilterUrl#7ef0dd87 = MessagesFilter; -inputMessagesFilterGif#ffc86587 = MessagesFilter; -inputMessagesFilterVoice#50f5c392 = MessagesFilter; -inputMessagesFilterMusic#3751b49e = MessagesFilter; -inputMessagesFilterChatPhotos#3a20ecb8 = MessagesFilter; -inputMessagesFilterPhoneCalls#80c99768 flags:# missed:flags.0?true = MessagesFilter; -inputMessagesFilterRoundVoice#7a7c17a4 = MessagesFilter; -inputMessagesFilterRoundVideo#b549da53 = MessagesFilter; -inputMessagesFilterMyMentions#c1f8e69a = MessagesFilter; -inputMessagesFilterGeo#e7026d0d = MessagesFilter; -inputMessagesFilterContacts#e062db83 = MessagesFilter; -inputMessagesFilterPinned#1bb00451 = MessagesFilter; - -updateNewMessage#1f2b0afd message:Message pts:int pts_count:int = Update; -updateMessageID#4e90bfd6 id:int random_id:long = Update; -updateDeleteMessages#a20db0e5 messages:Vector pts:int pts_count:int = Update; -updateUserTyping#c01e857f user_id:long action:SendMessageAction = Update; -updateChatUserTyping#83487af0 chat_id:long from_id:Peer action:SendMessageAction = Update; -updateChatParticipants#7761198 participants:ChatParticipants = Update; -updateUserStatus#e5bdf8de user_id:long status:UserStatus = Update; -updateUserName#a7848924 user_id:long first_name:string last_name:string usernames:Vector = Update; -updateNewEncryptedMessage#12bcbd9a message:EncryptedMessage qts:int = Update; -updateEncryptedChatTyping#1710f156 chat_id:int = Update; -updateEncryption#b4a2e88d chat:EncryptedChat date:int = Update; -updateEncryptedMessagesRead#38fe25b7 chat_id:int max_date:int date:int = Update; -updateChatParticipantAdd#3dda5451 chat_id:long user_id:long inviter_id:long date:int version:int = Update; -updateChatParticipantDelete#e32f3d77 chat_id:long user_id:long version:int = Update; -updateDcOptions#8e5e9873 dc_options:Vector = Update; -updateNotifySettings#bec268ef peer:NotifyPeer notify_settings:PeerNotifySettings = Update; -updateServiceNotification#ebe46819 flags:# popup:flags.0?true inbox_date:flags.1?int type:string message:string media:MessageMedia entities:Vector = Update; -updatePrivacy#ee3b272a key:PrivacyKey rules:Vector = Update; -updateUserPhone#5492a13 user_id:long phone:string = Update; -updateReadHistoryInbox#9c974fdf flags:# folder_id:flags.0?int peer:Peer max_id:int still_unread_count:int pts:int pts_count:int = Update; -updateReadHistoryOutbox#2f2f21bf peer:Peer max_id:int pts:int pts_count:int = Update; -updateWebPage#7f891213 webpage:WebPage pts:int pts_count:int = Update; -updateReadMessagesContents#68c13933 messages:Vector pts:int pts_count:int = Update; -updateChannelTooLong#108d941f flags:# channel_id:long pts:flags.0?int = Update; -updateChannel#635b4c09 channel_id:long = Update; -updateNewChannelMessage#62ba04d9 message:Message pts:int pts_count:int = Update; -updateReadChannelInbox#922e6e10 flags:# folder_id:flags.0?int channel_id:long max_id:int still_unread_count:int pts:int = Update; -updateDeleteChannelMessages#c32d5b12 channel_id:long messages:Vector pts:int pts_count:int = Update; -updateChannelMessageViews#f226ac08 channel_id:long id:int views:int = Update; -updateChatParticipantAdmin#d7ca61a2 chat_id:long user_id:long is_admin:Bool version:int = Update; -updateNewStickerSet#688a30aa stickerset:messages.StickerSet = Update; -updateStickerSetsOrder#bb2d201 flags:# masks:flags.0?true emojis:flags.1?true order:Vector = Update; -updateStickerSets#31c24808 flags:# masks:flags.0?true emojis:flags.1?true = Update; -updateSavedGifs#9375341e = Update; -updateBotInlineQuery#496f379c flags:# query_id:long user_id:long query:string geo:flags.0?GeoPoint peer_type:flags.1?InlineQueryPeerType offset:string = Update; -updateBotInlineSend#12f12a07 flags:# user_id:long query:string geo:flags.0?GeoPoint id:string msg_id:flags.1?InputBotInlineMessageID = Update; -updateEditChannelMessage#1b3f4df7 message:Message pts:int pts_count:int = Update; -updateBotCallbackQuery#b9cfc48d flags:# query_id:long user_id:long peer:Peer msg_id:int chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; -updateEditMessage#e40370a3 message:Message pts:int pts_count:int = Update; -updateInlineBotCallbackQuery#691e9052 flags:# query_id:long user_id:long msg_id:InputBotInlineMessageID chat_instance:long data:flags.0?bytes game_short_name:flags.1?string = Update; -updateReadChannelOutbox#b75f99a9 channel_id:long max_id:int = Update; -updateDraftMessage#1b49ec6d flags:# peer:Peer top_msg_id:flags.0?int draft:DraftMessage = Update; -updateReadFeaturedStickers#571d2742 = Update; -updateRecentStickers#9a422c20 = Update; -updateConfig#a229dd06 = Update; -updatePtsChanged#3354678f = Update; -updateChannelWebPage#2f2ba99f channel_id:long webpage:WebPage pts:int pts_count:int = Update; -updateDialogPinned#6e6fe51c flags:# pinned:flags.0?true folder_id:flags.1?int peer:DialogPeer = Update; -updatePinnedDialogs#fa0f3ca2 flags:# folder_id:flags.1?int order:flags.0?Vector = Update; -updateBotWebhookJSON#8317c0c3 data:DataJSON = Update; -updateBotWebhookJSONQuery#9b9240a6 query_id:long data:DataJSON timeout:int = Update; -updateBotShippingQuery#b5aefd7d query_id:long user_id:long payload:bytes shipping_address:PostAddress = Update; -updateBotPrecheckoutQuery#8caa9a96 flags:# query_id:long user_id:long payload:bytes info:flags.0?PaymentRequestedInfo shipping_option_id:flags.1?string currency:string total_amount:long = Update; -updatePhoneCall#ab0f6b1e phone_call:PhoneCall = Update; -updateLangPackTooLong#46560264 lang_code:string = Update; -updateLangPack#56022f4d difference:LangPackDifference = Update; -updateFavedStickers#e511996d = Update; -updateChannelReadMessagesContents#ea29055d flags:# channel_id:long top_msg_id:flags.0?int messages:Vector = Update; -updateContactsReset#7084a7be = Update; -updateChannelAvailableMessages#b23fc698 channel_id:long available_min_id:int = Update; -updateDialogUnreadMark#e16459c3 flags:# unread:flags.0?true peer:DialogPeer = Update; -updateMessagePoll#aca1657b flags:# poll_id:long poll:flags.0?Poll results:PollResults = Update; -updateChatDefaultBannedRights#54c01850 peer:Peer default_banned_rights:ChatBannedRights version:int = Update; -updateFolderPeers#19360dc0 folder_peers:Vector pts:int pts_count:int = Update; -updatePeerSettings#6a7e7366 peer:Peer settings:PeerSettings = Update; -updatePeerLocated#b4afcfb0 peers:Vector = Update; -updateNewScheduledMessage#39a51dfb message:Message = Update; -updateDeleteScheduledMessages#90866cee peer:Peer messages:Vector = Update; -updateTheme#8216fba3 theme:Theme = Update; -updateGeoLiveViewed#871fb939 peer:Peer msg_id:int = Update; -updateLoginToken#564fe691 = Update; -updateMessagePollVote#106395c9 poll_id:long user_id:long options:Vector qts:int = Update; -updateDialogFilter#26ffde7d flags:# id:int filter:flags.0?DialogFilter = Update; -updateDialogFilterOrder#a5d72105 order:Vector = Update; -updateDialogFilters#3504914f = Update; -updatePhoneCallSignalingData#2661bf09 phone_call_id:long data:bytes = Update; -updateChannelMessageForwards#d29a27f4 channel_id:long id:int forwards:int = Update; -updateReadChannelDiscussionInbox#d6b19546 flags:# channel_id:long top_msg_id:int read_max_id:int broadcast_id:flags.0?long broadcast_post:flags.0?int = Update; -updateReadChannelDiscussionOutbox#695c9e7c channel_id:long top_msg_id:int read_max_id:int = Update; -updatePeerBlocked#246a4b22 peer_id:Peer blocked:Bool = Update; -updateChannelUserTyping#8c88c923 flags:# channel_id:long top_msg_id:flags.0?int from_id:Peer action:SendMessageAction = Update; -updatePinnedMessages#ed85eab5 flags:# pinned:flags.0?true peer:Peer messages:Vector pts:int pts_count:int = Update; -updatePinnedChannelMessages#5bb98608 flags:# pinned:flags.0?true channel_id:long messages:Vector pts:int pts_count:int = Update; -updateChat#f89a6a4e chat_id:long = Update; -updateGroupCallParticipants#f2ebdb4e call:InputGroupCall participants:Vector version:int = Update; -updateGroupCall#14b24500 chat_id:long call:GroupCall = Update; -updatePeerHistoryTTL#bb9bb9a5 flags:# peer:Peer ttl_period:flags.0?int = Update; -updateChatParticipant#d087663a flags:# chat_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChatParticipant new_participant:flags.1?ChatParticipant invite:flags.2?ExportedChatInvite qts:int = Update; -updateChannelParticipant#985d3abb flags:# via_chatlist:flags.3?true channel_id:long date:int actor_id:long user_id:long prev_participant:flags.0?ChannelParticipant new_participant:flags.1?ChannelParticipant invite:flags.2?ExportedChatInvite qts:int = Update; -updateBotStopped#c4870a49 user_id:long date:int stopped:Bool qts:int = Update; -updateGroupCallConnection#b783982 flags:# presentation:flags.0?true params:DataJSON = Update; -updateBotCommands#4d712f2e peer:Peer bot_id:long commands:Vector = Update; -updatePendingJoinRequests#7063c3db peer:Peer requests_pending:int recent_requesters:Vector = Update; -updateBotChatInviteRequester#11dfa986 peer:Peer date:int user_id:long about:string invite:ExportedChatInvite qts:int = Update; -updateMessageReactions#5e1b3cb8 flags:# peer:Peer msg_id:int top_msg_id:flags.0?int reactions:MessageReactions = Update; -updateAttachMenuBots#17b7a20b = Update; -updateWebViewResultSent#1592b79d query_id:long = Update; -updateBotMenuButton#14b85813 bot_id:long button:BotMenuButton = Update; -updateSavedRingtones#74d8be99 = Update; -updateTranscribedAudio#84cd5a flags:# pending:flags.0?true peer:Peer msg_id:int transcription_id:long text:string = Update; -updateReadFeaturedEmojiStickers#fb4c496c = Update; -updateUserEmojiStatus#28373599 user_id:long emoji_status:EmojiStatus = Update; -updateRecentEmojiStatuses#30f443db = Update; -updateRecentReactions#6f7863f4 = Update; -updateMoveStickerSetToTop#86fccf85 flags:# masks:flags.0?true emojis:flags.1?true stickerset:long = Update; -updateMessageExtendedMedia#5a73a98c peer:Peer msg_id:int extended_media:MessageExtendedMedia = Update; -updateChannelPinnedTopic#192efbe3 flags:# pinned:flags.0?true channel_id:long topic_id:int = Update; -updateChannelPinnedTopics#fe198602 flags:# channel_id:long order:flags.0?Vector = Update; -updateUser#20529438 user_id:long = Update; -updateAutoSaveSettings#ec05b097 = Update; -updateGroupInvitePrivacyForbidden#ccf08ad6 user_id:long = Update; - -updates.state#a56c2a3e pts:int qts:int date:int seq:int unread_count:int = updates.State; - -updates.differenceEmpty#5d75a138 date:int seq:int = updates.Difference; -updates.difference#f49ca0 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector state:updates.State = updates.Difference; -updates.differenceSlice#a8fb1981 new_messages:Vector new_encrypted_messages:Vector other_updates:Vector chats:Vector users:Vector intermediate_state:updates.State = updates.Difference; -updates.differenceTooLong#4afe8f6d pts:int = updates.Difference; - -updatesTooLong#e317af7e = Updates; -updateShortMessage#313bc7f8 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int user_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; -updateShortChatMessage#4d6deea5 flags:# out:flags.1?true mentioned:flags.4?true media_unread:flags.5?true silent:flags.13?true id:int from_id:long chat_id:long message:string pts:int pts_count:int date:int fwd_from:flags.2?MessageFwdHeader via_bot_id:flags.11?long reply_to:flags.3?MessageReplyHeader entities:flags.7?Vector ttl_period:flags.25?int = Updates; -updateShort#78d4dec1 update:Update date:int = Updates; -updatesCombined#725b04c3 updates:Vector users:Vector chats:Vector date:int seq_start:int seq:int = Updates; -updates#74ae4240 updates:Vector users:Vector chats:Vector date:int seq:int = Updates; -updateShortSentMessage#9015e101 flags:# out:flags.1?true id:int pts:int pts_count:int date:int media:flags.9?MessageMedia entities:flags.7?Vector ttl_period:flags.25?int = Updates; - -photos.photos#8dca6aa5 photos:Vector users:Vector = photos.Photos; -photos.photosSlice#15051f54 count:int photos:Vector users:Vector = photos.Photos; - -photos.photo#20212ca8 photo:Photo users:Vector = photos.Photo; - -upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File; -upload.fileCdnRedirect#f18cda44 dc_id:int file_token:bytes encryption_key:bytes encryption_iv:bytes file_hashes:Vector = upload.File; - -dcOption#18b7a10d flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true cdn:flags.3?true static:flags.4?true this_port_only:flags.5?true id:int ip_address:string port:int secret:flags.10?bytes = DcOption; - -config#cc1a241e flags:# default_p2p_contacts:flags.3?true preload_featured_stickers:flags.4?true revoke_pm_inbox:flags.6?true blocked_mode:flags.8?true force_try_ipv6:flags.14?true date:int expires:int test_mode:Bool this_dc:int dc_options:Vector dc_txt_domain_name:string chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int push_chat_period_ms:int push_chat_limit:int edit_time_limit:int revoke_time_limit:int revoke_pm_time_limit:int rating_e_decay:int stickers_recent_limit:int channels_read_media_period:int tmp_sessions:flags.0?int call_receive_timeout_ms:int call_ring_timeout_ms:int call_connect_timeout_ms:int call_packet_timeout_ms:int me_url_prefix:string autoupdate_url_prefix:flags.7?string gif_search_username:flags.9?string venue_search_username:flags.10?string img_search_username:flags.11?string static_maps_provider:flags.12?string caption_length_max:int message_length_max:int webfile_dc_id:int suggested_lang_code:flags.2?string lang_pack_version:flags.2?int base_lang_pack_version:flags.2?int reactions_default:flags.15?Reaction autologin_token:flags.16?string = Config; - -nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; - -help.appUpdate#ccbbce30 flags:# can_not_skip:flags.0?true id:int version:string text:string entities:Vector document:flags.1?Document url:flags.2?string sticker:flags.3?Document = help.AppUpdate; -help.noAppUpdate#c45a6536 = help.AppUpdate; - -help.inviteText#18cb9f78 message:string = help.InviteText; - -encryptedChatEmpty#ab7ec0a0 id:int = EncryptedChat; -encryptedChatWaiting#66b25953 id:int access_hash:long date:int admin_id:long participant_id:long = EncryptedChat; -encryptedChatRequested#48f1d94c flags:# folder_id:flags.0?int id:int access_hash:long date:int admin_id:long participant_id:long g_a:bytes = EncryptedChat; -encryptedChat#61f0d4c7 id:int access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long = EncryptedChat; -encryptedChatDiscarded#1e1c7c45 flags:# history_deleted:flags.0?true id:int = EncryptedChat; - -inputEncryptedChat#f141b5e1 chat_id:int access_hash:long = InputEncryptedChat; - -encryptedFileEmpty#c21f497e = EncryptedFile; -encryptedFile#a8008cd8 id:long access_hash:long size:long dc_id:int key_fingerprint:int = EncryptedFile; - -inputEncryptedFileEmpty#1837c364 = InputEncryptedFile; -inputEncryptedFileUploaded#64bd0306 id:long parts:int md5_checksum:string key_fingerprint:int = InputEncryptedFile; -inputEncryptedFile#5a17b5e5 id:long access_hash:long = InputEncryptedFile; -inputEncryptedFileBigUploaded#2dc173c8 id:long parts:int key_fingerprint:int = InputEncryptedFile; - -encryptedMessage#ed18c118 random_id:long chat_id:int date:int bytes:bytes file:EncryptedFile = EncryptedMessage; -encryptedMessageService#23734b06 random_id:long chat_id:int date:int bytes:bytes = EncryptedMessage; - -messages.dhConfigNotModified#c0e24635 random:bytes = messages.DhConfig; -messages.dhConfig#2c221edd g:int p:bytes version:int random:bytes = messages.DhConfig; - -messages.sentEncryptedMessage#560f8935 date:int = messages.SentEncryptedMessage; -messages.sentEncryptedFile#9493ff32 date:int file:EncryptedFile = messages.SentEncryptedMessage; - -inputDocumentEmpty#72f0eaae = InputDocument; -inputDocument#1abfb575 id:long access_hash:long file_reference:bytes = InputDocument; - -documentEmpty#36f8c871 id:long = Document; -document#8fd4c4d8 flags:# id:long access_hash:long file_reference:bytes date:int mime_type:string size:long thumbs:flags.0?Vector video_thumbs:flags.1?Vector dc_id:int attributes:Vector = Document; - -help.support#17c6b5f6 phone_number:string user:User = help.Support; - -notifyPeer#9fd40bd8 peer:Peer = NotifyPeer; -notifyUsers#b4c83b4c = NotifyPeer; -notifyChats#c007cec3 = NotifyPeer; -notifyBroadcasts#d612e8ef = NotifyPeer; -notifyForumTopic#226e6308 peer:Peer top_msg_id:int = NotifyPeer; - -sendMessageTypingAction#16bf744e = SendMessageAction; -sendMessageCancelAction#fd5ec8f5 = SendMessageAction; -sendMessageRecordVideoAction#a187d66f = SendMessageAction; -sendMessageUploadVideoAction#e9763aec progress:int = SendMessageAction; -sendMessageRecordAudioAction#d52f73f7 = SendMessageAction; -sendMessageUploadAudioAction#f351d7ab progress:int = SendMessageAction; -sendMessageUploadPhotoAction#d1d34a26 progress:int = SendMessageAction; -sendMessageUploadDocumentAction#aa0cd9e4 progress:int = SendMessageAction; -sendMessageGeoLocationAction#176f8ba1 = SendMessageAction; -sendMessageChooseContactAction#628cbc6f = SendMessageAction; -sendMessageGamePlayAction#dd6a8f48 = SendMessageAction; -sendMessageRecordRoundAction#88f27fbc = SendMessageAction; -sendMessageUploadRoundAction#243e1c66 progress:int = SendMessageAction; -speakingInGroupCallAction#d92c2285 = SendMessageAction; -sendMessageHistoryImportAction#dbda9246 progress:int = SendMessageAction; -sendMessageChooseStickerAction#b05ac6b1 = SendMessageAction; -sendMessageEmojiInteraction#25972bcb emoticon:string msg_id:int interaction:DataJSON = SendMessageAction; -sendMessageEmojiInteractionSeen#b665902e emoticon:string = SendMessageAction; - -contacts.found#b3134d9d my_results:Vector results:Vector chats:Vector users:Vector = contacts.Found; - -inputPrivacyKeyStatusTimestamp#4f96cb18 = InputPrivacyKey; -inputPrivacyKeyChatInvite#bdfb0426 = InputPrivacyKey; -inputPrivacyKeyPhoneCall#fabadc5f = InputPrivacyKey; -inputPrivacyKeyPhoneP2P#db9e70d2 = InputPrivacyKey; -inputPrivacyKeyForwards#a4dd4c08 = InputPrivacyKey; -inputPrivacyKeyProfilePhoto#5719bacc = InputPrivacyKey; -inputPrivacyKeyPhoneNumber#352dafa = InputPrivacyKey; -inputPrivacyKeyAddedByPhone#d1219bdd = InputPrivacyKey; -inputPrivacyKeyVoiceMessages#aee69d68 = InputPrivacyKey; - -privacyKeyStatusTimestamp#bc2eab30 = PrivacyKey; -privacyKeyChatInvite#500e6dfa = PrivacyKey; -privacyKeyPhoneCall#3d662b7b = PrivacyKey; -privacyKeyPhoneP2P#39491cc8 = PrivacyKey; -privacyKeyForwards#69ec56a3 = PrivacyKey; -privacyKeyProfilePhoto#96151fed = PrivacyKey; -privacyKeyPhoneNumber#d19ae46d = PrivacyKey; -privacyKeyAddedByPhone#42ffd42b = PrivacyKey; -privacyKeyVoiceMessages#697f414 = PrivacyKey; - -inputPrivacyValueAllowContacts#d09e07b = InputPrivacyRule; -inputPrivacyValueAllowAll#184b35ce = InputPrivacyRule; -inputPrivacyValueAllowUsers#131cc67f users:Vector = InputPrivacyRule; -inputPrivacyValueDisallowContacts#ba52007 = InputPrivacyRule; -inputPrivacyValueDisallowAll#d66b66c9 = InputPrivacyRule; -inputPrivacyValueDisallowUsers#90110467 users:Vector = InputPrivacyRule; -inputPrivacyValueAllowChatParticipants#840649cf chats:Vector = InputPrivacyRule; -inputPrivacyValueDisallowChatParticipants#e94f0f86 chats:Vector = InputPrivacyRule; - -privacyValueAllowContacts#fffe1bac = PrivacyRule; -privacyValueAllowAll#65427b82 = PrivacyRule; -privacyValueAllowUsers#b8905fb2 users:Vector = PrivacyRule; -privacyValueDisallowContacts#f888fa1a = PrivacyRule; -privacyValueDisallowAll#8b73e763 = PrivacyRule; -privacyValueDisallowUsers#e4621141 users:Vector = PrivacyRule; -privacyValueAllowChatParticipants#6b134e8e chats:Vector = PrivacyRule; -privacyValueDisallowChatParticipants#41c87565 chats:Vector = PrivacyRule; - -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; -documentAttributeCustomEmoji#fd149899 flags:# free:flags.0?true text_color:flags.1?true alt:string stickerset:InputStickerSet = DocumentAttribute; - -messages.stickersNotModified#f1749a22 = messages.Stickers; -messages.stickers#30a6ec7e hash:long stickers:Vector = messages.Stickers; - -stickerPack#12b299d4 emoticon:string documents:Vector = StickerPack; - -messages.allStickersNotModified#e86602c3 = messages.AllStickers; -messages.allStickers#cdbbcebb hash:long sets:Vector = messages.AllStickers; - -messages.affectedMessages#84d19185 pts:int pts_count:int = messages.AffectedMessages; - -webPageEmpty#eb1477e8 id:long = WebPage; -webPagePending#c586da1c id:long date:int = WebPage; -webPage#e89c45b2 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 attributes:flags.12?Vector = WebPage; -webPageNotModified#7311ca11 flags:# cached_page_views:flags.0?int = WebPage; - -authorization#ad01d61d flags:# current:flags.0?true official_app:flags.1?true password_pending:flags.2?true encrypted_requests_disabled:flags.3?true call_requests_disabled:flags.4?true hash:long device_model:string platform:string system_version:string api_id:int app_name:string app_version:string date_created:int date_active:int ip:string country:string region:string = Authorization; - -account.authorizations#4bff8ea0 authorization_ttl_days:int authorizations:Vector = account.Authorizations; - -account.password#957b50fb flags:# has_recovery:flags.0?true has_secure_values:flags.1?true has_password:flags.2?true current_algo:flags.2?PasswordKdfAlgo srp_B:flags.2?bytes srp_id:flags.2?long hint:flags.3?string email_unconfirmed_pattern:flags.4?string new_algo:PasswordKdfAlgo new_secure_algo:SecurePasswordKdfAlgo secure_random:bytes pending_reset_date:flags.5?int login_email_pattern:flags.6?string = account.Password; - -account.passwordSettings#9a5c33e5 flags:# email:flags.0?string secure_settings:flags.1?SecureSecretSettings = account.PasswordSettings; - -account.passwordInputSettings#c23727c9 flags:# new_algo:flags.0?PasswordKdfAlgo new_password_hash:flags.0?bytes hint:flags.0?string email:flags.1?string new_secure_settings:flags.2?SecureSecretSettings = account.PasswordInputSettings; - -auth.passwordRecovery#137948a5 email_pattern:string = auth.PasswordRecovery; - -receivedNotifyMessage#a384b779 id:int flags:int = ReceivedNotifyMessage; - -chatInviteExported#ab4a819 flags:# revoked:flags.0?true permanent:flags.5?true request_needed:flags.6?true link:string admin_id:long date:int start_date:flags.4?int expire_date:flags.1?int usage_limit:flags.2?int usage:flags.3?int requested:flags.7?int title:flags.8?string = ExportedChatInvite; -chatInvitePublicJoinRequests#ed107ab7 = ExportedChatInvite; - -chatInviteAlready#5a686d7c chat:Chat = ChatInvite; -chatInvite#300c44c1 flags:# channel:flags.0?true broadcast:flags.1?true public:flags.2?true megagroup:flags.3?true request_needed:flags.6?true title:string about:flags.5?string photo:Photo participants_count:int participants:flags.4?Vector = ChatInvite; -chatInvitePeek#61695cb0 chat:Chat expires:int = ChatInvite; - -inputStickerSetEmpty#ffb62b95 = InputStickerSet; -inputStickerSetID#9de7a269 id:long access_hash:long = InputStickerSet; -inputStickerSetShortName#861cc8a0 short_name:string = InputStickerSet; -inputStickerSetAnimatedEmoji#28703c8 = InputStickerSet; -inputStickerSetDice#e67f520e emoticon:string = InputStickerSet; -inputStickerSetAnimatedEmojiAnimations#cde3739 = InputStickerSet; -inputStickerSetPremiumGifts#c88b3b02 = InputStickerSet; -inputStickerSetEmojiGenericAnimations#4c4d4ce = InputStickerSet; -inputStickerSetEmojiDefaultStatuses#29d0f5ee = InputStickerSet; -inputStickerSetEmojiDefaultTopicIcons#44c1f8e9 = InputStickerSet; - -stickerSet#2dd14edc flags:# archived:flags.1?true official:flags.2?true masks:flags.3?true animated:flags.5?true videos:flags.6?true emojis:flags.7?true installed_date:flags.0?int id:long access_hash:long title:string short_name:string thumbs:flags.4?Vector thumb_dc_id:flags.4?int thumb_version:flags.4?int thumb_document_id:flags.8?long count:int hash:int = StickerSet; - -messages.stickerSet#6e153f16 set:StickerSet packs:Vector keywords:Vector documents:Vector = messages.StickerSet; -messages.stickerSetNotModified#d3f924eb = messages.StickerSet; - -botCommand#c27ac8c7 command:string description:string = BotCommand; - -botInfo#8f300b57 flags:# user_id:flags.0?long description:flags.1?string description_photo:flags.4?Photo description_document:flags.5?Document commands:flags.2?Vector menu_button:flags.3?BotMenuButton = BotInfo; - -keyboardButton#a2fa4880 text:string = KeyboardButton; -keyboardButtonUrl#258aff05 text:string url:string = KeyboardButton; -keyboardButtonCallback#35bbdb6b flags:# requires_password:flags.0?true text:string data:bytes = KeyboardButton; -keyboardButtonRequestPhone#b16a6c29 text:string = KeyboardButton; -keyboardButtonRequestGeoLocation#fc796b3f text:string = KeyboardButton; -keyboardButtonSwitchInline#93b9fbb5 flags:# same_peer:flags.0?true text:string query:string peer_types:flags.1?Vector = KeyboardButton; -keyboardButtonGame#50f41ccf text:string = KeyboardButton; -keyboardButtonBuy#afd93fbb text:string = KeyboardButton; -keyboardButtonUrlAuth#10b78d29 flags:# text:string fwd_text:flags.0?string url:string button_id:int = KeyboardButton; -inputKeyboardButtonUrlAuth#d02e7fd4 flags:# request_write_access:flags.0?true text:string fwd_text:flags.1?string url:string bot:InputUser = KeyboardButton; -keyboardButtonRequestPoll#bbc7515d flags:# quiz:flags.0?Bool text:string = KeyboardButton; -inputKeyboardButtonUserProfile#e988037b text:string user_id:InputUser = KeyboardButton; -keyboardButtonUserProfile#308660c1 text:string user_id:long = KeyboardButton; -keyboardButtonWebView#13767230 text:string url:string = KeyboardButton; -keyboardButtonSimpleWebView#a0c0505c text:string url:string = KeyboardButton; -keyboardButtonRequestPeer#d0b468c text:string button_id:int peer_type:RequestPeerType = KeyboardButton; - -keyboardButtonRow#77608b83 buttons:Vector = KeyboardButtonRow; - -replyKeyboardHide#a03e5b85 flags:# selective:flags.2?true = ReplyMarkup; -replyKeyboardForceReply#86b40b08 flags:# single_use:flags.1?true selective:flags.2?true placeholder:flags.3?string = ReplyMarkup; -replyKeyboardMarkup#85dd99d1 flags:# resize:flags.0?true single_use:flags.1?true selective:flags.2?true persistent:flags.4?true rows:Vector placeholder:flags.3?string = ReplyMarkup; -replyInlineMarkup#48a30254 rows:Vector = ReplyMarkup; - -messageEntityUnknown#bb92ba95 offset:int length:int = MessageEntity; -messageEntityMention#fa04579d offset:int length:int = MessageEntity; -messageEntityHashtag#6f635b0d offset:int length:int = MessageEntity; -messageEntityBotCommand#6cef8ac7 offset:int length:int = MessageEntity; -messageEntityUrl#6ed02538 offset:int length:int = MessageEntity; -messageEntityEmail#64e475c2 offset:int length:int = MessageEntity; -messageEntityBold#bd610bc9 offset:int length:int = MessageEntity; -messageEntityItalic#826f8b60 offset:int length:int = MessageEntity; -messageEntityCode#28a20571 offset:int length:int = MessageEntity; -messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity; -messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity; -messageEntityMentionName#dc7b1140 offset:int length:int user_id:long = MessageEntity; -inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity; -messageEntityPhone#9b69e34b offset:int length:int = MessageEntity; -messageEntityCashtag#4c4e743f offset:int length:int = MessageEntity; -messageEntityUnderline#9c4e7e8b offset:int length:int = MessageEntity; -messageEntityStrike#bf0693d4 offset:int length:int = MessageEntity; -messageEntityBlockquote#20df5d0 offset:int length:int = MessageEntity; -messageEntityBankCard#761e6af4 offset:int length:int = MessageEntity; -messageEntitySpoiler#32ca960f offset:int length:int = MessageEntity; -messageEntityCustomEmoji#c8cf05f8 offset:int length:int document_id:long = MessageEntity; - -inputChannelEmpty#ee8c1e86 = InputChannel; -inputChannel#f35aec28 channel_id:long access_hash:long = InputChannel; -inputChannelFromMessage#5b934f9d peer:InputPeer msg_id:int channel_id:long = InputChannel; - -contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer; - -messageRange#ae30253 min_id:int max_id:int = MessageRange; - -updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference; -updates.channelDifferenceTooLong#a4bcc6fe flags:# final:flags.0?true timeout:flags.1?int dialog:Dialog messages:Vector chats:Vector users:Vector = updates.ChannelDifference; -updates.channelDifference#2064674e flags:# final:flags.0?true pts:int timeout:flags.1?int new_messages:Vector other_updates:Vector chats:Vector users:Vector = updates.ChannelDifference; - -channelMessagesFilterEmpty#94d42ee7 = ChannelMessagesFilter; -channelMessagesFilter#cd77d957 flags:# exclude_new_messages:flags.1?true ranges:Vector = ChannelMessagesFilter; - -channelParticipant#c00c07c0 user_id:long date:int = ChannelParticipant; -channelParticipantSelf#35a8bfa7 flags:# via_request:flags.0?true user_id:long inviter_id:long date:int = ChannelParticipant; -channelParticipantCreator#2fe601d3 flags:# user_id:long admin_rights:ChatAdminRights rank:flags.0?string = ChannelParticipant; -channelParticipantAdmin#34c3bb53 flags:# can_edit:flags.0?true self:flags.1?true user_id:long inviter_id:flags.1?long promoted_by:long date:int admin_rights:ChatAdminRights rank:flags.2?string = ChannelParticipant; -channelParticipantBanned#6df8014e flags:# left:flags.0?true peer:Peer kicked_by:long date:int banned_rights:ChatBannedRights = ChannelParticipant; -channelParticipantLeft#1b03f006 peer:Peer = ChannelParticipant; - -channelParticipantsRecent#de3f3c79 = ChannelParticipantsFilter; -channelParticipantsAdmins#b4608969 = ChannelParticipantsFilter; -channelParticipantsKicked#a3b54985 q:string = ChannelParticipantsFilter; -channelParticipantsBots#b0d1865b = ChannelParticipantsFilter; -channelParticipantsBanned#1427a5e1 q:string = ChannelParticipantsFilter; -channelParticipantsSearch#656ac4b q:string = ChannelParticipantsFilter; -channelParticipantsContacts#bb6ae88d q:string = ChannelParticipantsFilter; -channelParticipantsMentions#e04b5ceb flags:# q:flags.0?string top_msg_id:flags.1?int = ChannelParticipantsFilter; - -channels.channelParticipants#9ab0feaf count:int participants:Vector chats:Vector users:Vector = channels.ChannelParticipants; -channels.channelParticipantsNotModified#f0173fe9 = channels.ChannelParticipants; - -channels.channelParticipant#dfb80317 participant:ChannelParticipant chats:Vector users:Vector = channels.ChannelParticipant; - -help.termsOfService#780a0310 flags:# popup:flags.0?true id:DataJSON text:string entities:Vector min_age_confirm:flags.1?int = help.TermsOfService; - -messages.savedGifsNotModified#e8025ca2 = messages.SavedGifs; -messages.savedGifs#84a02a0d hash:long gifs:Vector = messages.SavedGifs; - -inputBotInlineMessageMediaAuto#3380c786 flags:# 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#96929a85 flags:# geo_point:InputGeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaVenue#417bbf11 flags:# geo_point:InputGeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaContact#a6edbffd flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageGame#4b425864 flags:# reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; -inputBotInlineMessageMediaInvoice#d7e78225 flags:# title:string description:string photo:flags.0?InputWebDocument invoice:Invoice payload:bytes provider:string provider_data:DataJSON reply_markup:flags.2?ReplyMarkup = InputBotInlineMessage; - -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#51846fd flags:# geo:GeoPoint heading:flags.0?int period:flags.1?int proximity_notification_radius:flags.3?int reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaVenue#8a86659c flags:# geo:GeoPoint title:string address:string provider:string venue_id:string venue_type:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaContact#18d1cdc2 flags:# phone_number:string first_name:string last_name:string vcard:string reply_markup:flags.2?ReplyMarkup = BotInlineMessage; -botInlineMessageMediaInvoice#354a9b09 flags:# shipping_address_requested:flags.1?true test:flags.3?true title:string description:string photo:flags.0?WebDocument currency:string total_amount:long reply_markup:flags.2?ReplyMarkup = BotInlineMessage; - -botInlineResult#11965f3a flags:# id:string type:string title:flags.1?string description:flags.2?string url:flags.3?string thumb:flags.4?WebDocument content:flags.5?WebDocument send_message:BotInlineMessage = BotInlineResult; -botInlineMediaResult#17db940b flags:# id:string type:string photo:flags.0?Photo document:flags.1?Document title:flags.2?string description:flags.3?string send_message:BotInlineMessage = BotInlineResult; - -messages.botResults#e021f2f6 flags:# gallery:flags.0?true query_id:long next_offset:flags.1?string switch_pm:flags.2?InlineBotSwitchPM switch_webview:flags.3?InlineBotWebView results:Vector cache_time:int users:Vector = messages.BotResults; - -exportedMessageLink#5dab1af4 link:string html:string = ExportedMessageLink; - -messageFwdHeader#5f777dce flags:# imported:flags.7?true from_id:flags.0?Peer from_name:flags.5?string date:int channel_post:flags.2?int post_author:flags.3?string saved_from_peer:flags.4?Peer saved_from_msg_id:flags.4?int psa_type:flags.6?string = MessageFwdHeader; - -auth.codeTypeSms#72a3158c = auth.CodeType; -auth.codeTypeCall#741cd3e3 = auth.CodeType; -auth.codeTypeFlashCall#226ccefb = auth.CodeType; -auth.codeTypeMissedCall#d61ad6ee = auth.CodeType; -auth.codeTypeFragmentSms#6ed998c = auth.CodeType; - -auth.sentCodeTypeApp#3dbb5986 length:int = auth.SentCodeType; -auth.sentCodeTypeSms#c000bba2 length:int = auth.SentCodeType; -auth.sentCodeTypeCall#5353e5a7 length:int = auth.SentCodeType; -auth.sentCodeTypeFlashCall#ab03c6d9 pattern:string = auth.SentCodeType; -auth.sentCodeTypeMissedCall#82006484 prefix:string length:int = auth.SentCodeType; -auth.sentCodeTypeEmailCode#f450f59b flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true email_pattern:string length:int reset_available_period:flags.3?int reset_pending_date:flags.4?int = auth.SentCodeType; -auth.sentCodeTypeSetUpEmailRequired#a5491dea flags:# apple_signin_allowed:flags.0?true google_signin_allowed:flags.1?true = auth.SentCodeType; -auth.sentCodeTypeFragmentSms#d9565c39 url:string length:int = auth.SentCodeType; -auth.sentCodeTypeFirebaseSms#e57b1432 flags:# nonce:flags.0?bytes receipt:flags.1?string push_timeout:flags.1?int length:int = auth.SentCodeType; - -messages.botCallbackAnswer#36585ea4 flags:# alert:flags.1?true has_url:flags.3?true native_ui:flags.4?true message:flags.0?string url:flags.2?string cache_time:int = messages.BotCallbackAnswer; - -messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData; - -inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID; -inputBotInlineMessageID64#b6d915d7 dc_id:int owner_id:long id:int access_hash:long = InputBotInlineMessageID; - -inlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM; - -messages.peerDialogs#3371c354 dialogs:Vector messages:Vector chats:Vector users:Vector state:updates.State = messages.PeerDialogs; - -topPeer#edcdc05b peer:Peer rating:double = TopPeer; - -topPeerCategoryBotsPM#ab661b5b = TopPeerCategory; -topPeerCategoryBotsInline#148677e2 = TopPeerCategory; -topPeerCategoryCorrespondents#637b7ed = TopPeerCategory; -topPeerCategoryGroups#bd17a14a = TopPeerCategory; -topPeerCategoryChannels#161d9628 = TopPeerCategory; -topPeerCategoryPhoneCalls#1e76a78c = TopPeerCategory; -topPeerCategoryForwardUsers#a8406ca9 = TopPeerCategory; -topPeerCategoryForwardChats#fbeec0f0 = TopPeerCategory; - -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#c6dc0c66 count:int = messages.FeaturedStickers; -messages.featuredStickers#be382906 flags:# premium:flags.0?true hash:long count:int sets:Vector unread:Vector = messages.FeaturedStickers; - -messages.recentStickersNotModified#b17f890 = messages.RecentStickers; -messages.recentStickers#88d37c56 hash:long packs:Vector stickers:Vector dates:Vector = messages.RecentStickers; - -messages.archivedStickers#4fcba9c8 count:int sets:Vector = messages.ArchivedStickers; - -messages.stickerSetInstallResultSuccess#38641628 = messages.StickerSetInstallResult; -messages.stickerSetInstallResultArchive#35e410a8 sets:Vector = messages.StickerSetInstallResult; - -stickerSetCovered#6410a5d2 set:StickerSet cover:Document = StickerSetCovered; -stickerSetMultiCovered#3407e51b set:StickerSet covers:Vector = StickerSetCovered; -stickerSetFullCovered#40d13c0e set:StickerSet packs:Vector keywords:Vector documents:Vector = StickerSetCovered; -stickerSetNoCovered#77b15d1c set:StickerSet = StickerSetCovered; - -maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords; - -inputStickeredMediaPhoto#4a992157 id:InputPhoto = InputStickeredMedia; -inputStickeredMediaDocument#438865b id:InputDocument = InputStickeredMedia; - -game#bdf9653b flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document = Game; - -inputGameID#32c3e77 id:long access_hash:long = InputGame; -inputGameShortName#c331e80a bot_id:InputUser short_name:string = InputGame; - -highScore#73a379eb pos:int user_id:long score:int = HighScore; - -messages.highScores#9a3bfd99 scores:Vector users:Vector = messages.HighScores; - -textEmpty#dc3d824f = RichText; -textPlain#744694e0 text:string = RichText; -textBold#6724abc4 text:RichText = RichText; -textItalic#d912a59c text:RichText = RichText; -textUnderline#c12622c4 text:RichText = RichText; -textStrike#9bf8bb95 text:RichText = RichText; -textFixed#6c3f19b9 text:RichText = RichText; -textUrl#3c2884c1 text:RichText url:string webpage_id:long = RichText; -textEmail#de5a0dd6 text:RichText email:string = RichText; -textConcat#7e6260d7 texts:Vector = RichText; -textSubscript#ed6a8504 text:RichText = RichText; -textSuperscript#c7fb5e01 text:RichText = RichText; -textMarked#34b8621 text:RichText = RichText; -textPhone#1ccb966a text:RichText phone:string = RichText; -textImage#81ccf4f document_id:long w:int h:int = RichText; -textAnchor#35553762 text:RichText name:string = RichText; - -pageBlockUnsupported#13567e8a = PageBlock; -pageBlockTitle#70abc3fd text:RichText = PageBlock; -pageBlockSubtitle#8ffa9a1f text:RichText = PageBlock; -pageBlockAuthorDate#baafe5e0 author:RichText published_date:int = PageBlock; -pageBlockHeader#bfd064ec text:RichText = PageBlock; -pageBlockSubheader#f12bb6e1 text:RichText = PageBlock; -pageBlockParagraph#467a0766 text:RichText = PageBlock; -pageBlockPreformatted#c070d93e text:RichText language:string = PageBlock; -pageBlockFooter#48870999 text:RichText = PageBlock; -pageBlockDivider#db20b188 = PageBlock; -pageBlockAnchor#ce0d37b0 name:string = PageBlock; -pageBlockList#e4e88011 items:Vector = PageBlock; -pageBlockBlockquote#263d7c26 text:RichText caption:RichText = PageBlock; -pageBlockPullquote#4f4456d3 text:RichText caption:RichText = PageBlock; -pageBlockPhoto#1759c560 flags:# photo_id:long caption:PageCaption url:flags.0?string webpage_id:flags.0?long = PageBlock; -pageBlockVideo#7c8fe7b6 flags:# autoplay:flags.0?true loop:flags.1?true video_id:long caption:PageCaption = PageBlock; -pageBlockCover#39f23300 cover:PageBlock = PageBlock; -pageBlockEmbed#a8718dc5 flags:# full_width:flags.0?true allow_scrolling:flags.3?true url:flags.1?string html:flags.2?string poster_photo_id:flags.4?long w:flags.5?int h:flags.5?int caption:PageCaption = PageBlock; -pageBlockEmbedPost#f259a80b url:string webpage_id:long author_photo_id:long author:string date:int blocks:Vector caption:PageCaption = PageBlock; -pageBlockCollage#65a0fa4d items:Vector caption:PageCaption = PageBlock; -pageBlockSlideshow#31f9590 items:Vector caption:PageCaption = PageBlock; -pageBlockChannel#ef1751b5 channel:Chat = PageBlock; -pageBlockAudio#804361ea audio_id:long caption:PageCaption = PageBlock; -pageBlockKicker#1e148390 text:RichText = PageBlock; -pageBlockTable#bf4dea82 flags:# bordered:flags.0?true striped:flags.1?true title:RichText rows:Vector = PageBlock; -pageBlockOrderedList#9a8ae1e1 items:Vector = PageBlock; -pageBlockDetails#76768bed flags:# open:flags.0?true blocks:Vector title:RichText = PageBlock; -pageBlockRelatedArticles#16115a96 title:RichText articles:Vector = PageBlock; -pageBlockMap#a44f3ef6 geo:GeoPoint zoom:int w:int h:int caption:PageCaption = PageBlock; - -phoneCallDiscardReasonMissed#85e42301 = PhoneCallDiscardReason; -phoneCallDiscardReasonDisconnect#e095c1a0 = PhoneCallDiscardReason; -phoneCallDiscardReasonHangup#57adc690 = PhoneCallDiscardReason; -phoneCallDiscardReasonBusy#faf7e8c9 = PhoneCallDiscardReason; - -dataJSON#7d748d04 data:string = DataJSON; - -labeledPrice#cb296bf8 label:string amount:long = LabeledPrice; - -invoice#3e85a91b flags:# test:flags.0?true name_requested:flags.1?true phone_requested:flags.2?true email_requested:flags.3?true shipping_address_requested:flags.4?true flexible:flags.5?true phone_to_provider:flags.6?true email_to_provider:flags.7?true recurring:flags.9?true currency:string prices:Vector max_tip_amount:flags.8?long suggested_tip_amounts:flags.8?Vector recurring_terms_url:flags.9?string = Invoice; - -paymentCharge#ea02c27e id:string provider_charge_id:string = PaymentCharge; - -postAddress#1e8caaeb street_line1:string street_line2:string city:string state:string country_iso2:string post_code:string = PostAddress; - -paymentRequestedInfo#909c3f94 flags:# name:flags.0?string phone:flags.1?string email:flags.2?string shipping_address:flags.3?PostAddress = PaymentRequestedInfo; - -paymentSavedCredentialsCard#cdc27a1f id:string title:string = PaymentSavedCredentials; - -webDocument#1c570ed1 url:string access_hash:long size:int mime_type:string attributes:Vector = WebDocument; -webDocumentNoProxy#f9c8bcc6 url:string size:int mime_type:string attributes:Vector = WebDocument; - -inputWebDocument#9bed434d url:string size:int mime_type:string attributes:Vector = InputWebDocument; - -inputWebFileLocation#c239d686 url:string access_hash:long = InputWebFileLocation; -inputWebFileGeoPointLocation#9f2221c9 geo_point:InputGeoPoint access_hash:long w:int h:int zoom:int scale:int = InputWebFileLocation; -inputWebFileAudioAlbumThumbLocation#f46fe924 flags:# small:flags.2?true document:flags.0?InputDocument title:flags.1?string performer:flags.1?string = InputWebFileLocation; - -upload.webFile#21e753bc size:int mime_type:string file_type:storage.FileType mtime:int bytes:bytes = upload.WebFile; - -payments.paymentForm#a0058751 flags:# can_save_credentials:flags.2?true password_missing:flags.3?true form_id:long bot_id:long title:string description:string photo:flags.5?WebDocument invoice:Invoice provider_id:long url:string native_provider:flags.4?string native_params:flags.4?DataJSON additional_methods:flags.6?Vector saved_info:flags.0?PaymentRequestedInfo saved_credentials:flags.1?Vector users:Vector = payments.PaymentForm; - -payments.validatedRequestedInfo#d1451883 flags:# id:flags.0?string shipping_options:flags.1?Vector = payments.ValidatedRequestedInfo; - -payments.paymentResult#4e5f810d updates:Updates = payments.PaymentResult; -payments.paymentVerificationNeeded#d8411139 url:string = payments.PaymentResult; - -payments.paymentReceipt#70c4fe03 flags:# date:int bot_id:long provider_id:long title:string description:string photo:flags.2?WebDocument invoice:Invoice info:flags.0?PaymentRequestedInfo shipping:flags.1?ShippingOption tip_amount:flags.3?long currency:string total_amount:long credentials_title:string users:Vector = payments.PaymentReceipt; - -payments.savedInfo#fb8fe43c flags:# has_saved_credentials:flags.1?true saved_info:flags.0?PaymentRequestedInfo = payments.SavedInfo; - -inputPaymentCredentialsSaved#c10eb2cf id:string tmp_password:bytes = InputPaymentCredentials; -inputPaymentCredentials#3417d728 flags:# save:flags.0?true data:DataJSON = InputPaymentCredentials; -inputPaymentCredentialsApplePay#aa1c39f payment_data:DataJSON = InputPaymentCredentials; -inputPaymentCredentialsGooglePay#8ac32801 payment_token:DataJSON = InputPaymentCredentials; - -account.tmpPassword#db64fd34 tmp_password:bytes valid_until:int = account.TmpPassword; - -shippingOption#b6213cdf id:string title:string prices:Vector = ShippingOption; - -inputStickerSetItem#32da9e9c flags:# document:InputDocument emoji:string mask_coords:flags.0?MaskCoords keywords:flags.1?string = InputStickerSetItem; - -inputPhoneCall#1e36fded id:long access_hash:long = InputPhoneCall; - -phoneCallEmpty#5366c915 id:long = PhoneCall; -phoneCallWaiting#c5226f17 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long protocol:PhoneCallProtocol receive_date:flags.0?int = PhoneCall; -phoneCallRequested#14b0ed0c flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_hash:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCallAccepted#3660c311 flags:# video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_b:bytes protocol:PhoneCallProtocol = PhoneCall; -phoneCall#967f7c67 flags:# p2p_allowed:flags.5?true video:flags.6?true id:long access_hash:long date:int admin_id:long participant_id:long g_a_or_b:bytes key_fingerprint:long protocol:PhoneCallProtocol connections:Vector start_date:int = PhoneCall; -phoneCallDiscarded#50ca4de1 flags:# need_rating:flags.2?true need_debug:flags.3?true video:flags.6?true id:long reason:flags.0?PhoneCallDiscardReason duration:flags.1?int = PhoneCall; - -phoneConnection#9cc123c7 flags:# tcp:flags.0?true id:long ip:string ipv6:string port:int peer_tag:bytes = PhoneConnection; -phoneConnectionWebrtc#635fe375 flags:# turn:flags.0?true stun:flags.1?true id:long ip:string ipv6:string port:int username:string password:string = PhoneConnection; - -phoneCallProtocol#fc878fc8 flags:# udp_p2p:flags.0?true udp_reflector:flags.1?true min_layer:int max_layer:int library_versions:Vector = PhoneCallProtocol; - -phone.phoneCall#ec82e140 phone_call:PhoneCall users:Vector = phone.PhoneCall; - -upload.cdnFileReuploadNeeded#eea8e46e request_token:bytes = upload.CdnFile; -upload.cdnFile#a99fca4f bytes:bytes = upload.CdnFile; - -cdnPublicKey#c982eaba dc_id:int public_key:string = CdnPublicKey; - -cdnConfig#5725e40a public_keys:Vector = CdnConfig; - -langPackString#cad181f6 key:string value:string = LangPackString; -langPackStringPluralized#6c47ac9f flags:# key:string zero_value:flags.0?string one_value:flags.1?string two_value:flags.2?string few_value:flags.3?string many_value:flags.4?string other_value:string = LangPackString; -langPackStringDeleted#2979eeb2 key:string = LangPackString; - -langPackDifference#f385c1f6 lang_code:string from_version:int version:int strings:Vector = LangPackDifference; - -langPackLanguage#eeca5ce3 flags:# official:flags.0?true rtl:flags.2?true beta:flags.3?true name:string native_name:string lang_code:string base_lang_code:flags.1?string plural_code:string strings_count:int translated_count:int translations_url:string = LangPackLanguage; - -channelAdminLogEventActionChangeTitle#e6dfb825 prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeAbout#55188a2e prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeUsername#6a4afc38 prev_value:string new_value:string = ChannelAdminLogEventAction; -channelAdminLogEventActionChangePhoto#434bd2af prev_photo:Photo new_photo:Photo = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleInvites#1b7907ae new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleSignatures#26ae0971 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionUpdatePinned#e9e82c18 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionEditMessage#709b2405 prev_message:Message new_message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionDeleteMessage#42e047bb message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantJoin#183040d3 = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantLeave#f89777f2 = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantInvite#e31c34d8 participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantToggleBan#e6d83d7e prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantToggleAdmin#d5676710 prev_participant:ChannelParticipant new_participant:ChannelParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeStickerSet#b1c3caa7 prev_stickerset:InputStickerSet new_stickerset:InputStickerSet = ChannelAdminLogEventAction; -channelAdminLogEventActionTogglePreHistoryHidden#5f5c95f1 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionDefaultBannedRights#2df5fc0a prev_banned_rights:ChatBannedRights new_banned_rights:ChatBannedRights = ChannelAdminLogEventAction; -channelAdminLogEventActionStopPoll#8f079643 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeLinkedChat#50c7ac8 prev_value:long new_value:long = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeLocation#e6b76ae prev_value:ChannelLocation new_value:ChannelLocation = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleSlowMode#53909779 prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionStartGroupCall#23209745 call:InputGroupCall = ChannelAdminLogEventAction; -channelAdminLogEventActionDiscardGroupCall#db9f9140 call:InputGroupCall = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantMute#f92424d2 participant:GroupCallParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantUnmute#e64429c0 participant:GroupCallParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleGroupCallSetting#56d6a247 join_muted:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantJoinByInvite#fe9fc158 flags:# via_chatlist:flags.0?true invite:ExportedChatInvite = ChannelAdminLogEventAction; -channelAdminLogEventActionExportedInviteDelete#5a50fca4 invite:ExportedChatInvite = ChannelAdminLogEventAction; -channelAdminLogEventActionExportedInviteRevoke#410a134e invite:ExportedChatInvite = ChannelAdminLogEventAction; -channelAdminLogEventActionExportedInviteEdit#e90ebb59 prev_invite:ExportedChatInvite new_invite:ExportedChatInvite = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantVolume#3e7f6847 participant:GroupCallParticipant = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeHistoryTTL#6e941a38 prev_value:int new_value:int = ChannelAdminLogEventAction; -channelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions = ChannelAdminLogEventAction; -channelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector new_value:Vector = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction; -channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction; -channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic = ChannelAdminLogEventAction; -channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction; -channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic = ChannelAdminLogEventAction; -channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction; - -channelAdminLogEvent#1fad68cd id:long date:int user_id:long action:ChannelAdminLogEventAction = ChannelAdminLogEvent; - -channels.adminLogResults#ed8af74d events:Vector chats:Vector users:Vector = channels.AdminLogResults; - -channelAdminLogEventsFilter#ea107ae4 flags:# join:flags.0?true leave:flags.1?true invite:flags.2?true ban:flags.3?true unban:flags.4?true kick:flags.5?true unkick:flags.6?true promote:flags.7?true demote:flags.8?true info:flags.9?true settings:flags.10?true pinned:flags.11?true edit:flags.12?true delete:flags.13?true group_call:flags.14?true invites:flags.15?true send:flags.16?true forums:flags.17?true = ChannelAdminLogEventsFilter; - -popularContact#5ce14175 client_id:long importers:int = PopularContact; - -messages.favedStickersNotModified#9e8fa6d3 = messages.FavedStickers; -messages.favedStickers#2cb51097 hash:long packs:Vector stickers:Vector = messages.FavedStickers; - -recentMeUrlUnknown#46e1d13d url:string = RecentMeUrl; -recentMeUrlUser#b92c09e2 url:string user_id:long = RecentMeUrl; -recentMeUrlChat#b2da71d2 url:string chat_id:long = RecentMeUrl; -recentMeUrlChatInvite#eb49081d url:string chat_invite:ChatInvite = RecentMeUrl; -recentMeUrlStickerSet#bc0a57dc url:string set:StickerSetCovered = RecentMeUrl; - -help.recentMeUrls#e0310d7 urls:Vector chats:Vector users:Vector = help.RecentMeUrls; - -inputSingleMedia#1cc6e91f flags:# media:InputMedia random_id:long message:string entities:flags.0?Vector = InputSingleMedia; - -webAuthorization#a6f8f452 hash:long bot_id:long domain:string browser:string platform:string date_created:int date_active:int ip:string region:string = WebAuthorization; - -account.webAuthorizations#ed56c9fc authorizations:Vector users:Vector = account.WebAuthorizations; - -inputMessageID#a676a322 id:int = InputMessage; -inputMessageReplyTo#bad88395 id:int = InputMessage; -inputMessagePinned#86872538 = InputMessage; -inputMessageCallbackQuery#acfa1a7e id:int query_id:long = InputMessage; - -inputDialogPeer#fcaafeb7 peer:InputPeer = InputDialogPeer; -inputDialogPeerFolder#64600527 folder_id:int = InputDialogPeer; - -dialogPeer#e56dbf05 peer:Peer = DialogPeer; -dialogPeerFolder#514519e2 folder_id:int = DialogPeer; - -messages.foundStickerSetsNotModified#d54b65d = messages.FoundStickerSets; -messages.foundStickerSets#8af09dd2 hash:long sets:Vector = messages.FoundStickerSets; - -fileHash#f39b035c offset:long limit:int hash:bytes = FileHash; - -inputClientProxy#75588b3f address:string port:int = InputClientProxy; - -help.termsOfServiceUpdateEmpty#e3309f7f expires:int = help.TermsOfServiceUpdate; -help.termsOfServiceUpdate#28ecf961 expires:int terms_of_service:help.TermsOfService = help.TermsOfServiceUpdate; - -inputSecureFileUploaded#3334b0f0 id:long parts:int md5_checksum:string file_hash:bytes secret:bytes = InputSecureFile; -inputSecureFile#5367e5be id:long access_hash:long = InputSecureFile; - -secureFileEmpty#64199744 = SecureFile; -secureFile#7d09c27e id:long access_hash:long size:long dc_id:int date:int file_hash:bytes secret:bytes = SecureFile; - -secureData#8aeabec3 data:bytes data_hash:bytes secret:bytes = SecureData; - -securePlainPhone#7d6099dd phone:string = SecurePlainData; -securePlainEmail#21ec5a5f email:string = SecurePlainData; - -secureValueTypePersonalDetails#9d2a81e3 = SecureValueType; -secureValueTypePassport#3dac6a00 = SecureValueType; -secureValueTypeDriverLicense#6e425c4 = SecureValueType; -secureValueTypeIdentityCard#a0d0744b = SecureValueType; -secureValueTypeInternalPassport#99a48f23 = SecureValueType; -secureValueTypeAddress#cbe31e26 = SecureValueType; -secureValueTypeUtilityBill#fc36954e = SecureValueType; -secureValueTypeBankStatement#89137c0d = SecureValueType; -secureValueTypeRentalAgreement#8b883488 = SecureValueType; -secureValueTypePassportRegistration#99e3806a = SecureValueType; -secureValueTypeTemporaryRegistration#ea02ec33 = SecureValueType; -secureValueTypePhone#b320aadb = SecureValueType; -secureValueTypeEmail#8e3ca7ee = SecureValueType; - -secureValue#187fa0ca flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?SecureFile reverse_side:flags.2?SecureFile selfie:flags.3?SecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData hash:bytes = SecureValue; - -inputSecureValue#db21d0a7 flags:# type:SecureValueType data:flags.0?SecureData front_side:flags.1?InputSecureFile reverse_side:flags.2?InputSecureFile selfie:flags.3?InputSecureFile translation:flags.6?Vector files:flags.4?Vector plain_data:flags.5?SecurePlainData = InputSecureValue; - -secureValueHash#ed1ecdb0 type:SecureValueType hash:bytes = SecureValueHash; - -secureValueErrorData#e8a40bd9 type:SecureValueType data_hash:bytes field:string text:string = SecureValueError; -secureValueErrorFrontSide#be3dfa type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorReverseSide#868a2aa5 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorSelfie#e537ced6 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorFile#7a700873 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorFiles#666220e9 type:SecureValueType file_hash:Vector text:string = SecureValueError; -secureValueError#869d758f type:SecureValueType hash:bytes text:string = SecureValueError; -secureValueErrorTranslationFile#a1144770 type:SecureValueType file_hash:bytes text:string = SecureValueError; -secureValueErrorTranslationFiles#34636dd8 type:SecureValueType file_hash:Vector text:string = SecureValueError; - -secureCredentialsEncrypted#33f0ea47 data:bytes hash:bytes secret:bytes = SecureCredentialsEncrypted; - -account.authorizationForm#ad2e1cd8 flags:# required_types:Vector values:Vector errors:Vector users:Vector privacy_policy_url:flags.0?string = account.AuthorizationForm; - -account.sentEmailCode#811f854f email_pattern:string length:int = account.SentEmailCode; - -help.deepLinkInfoEmpty#66afa166 = help.DeepLinkInfo; -help.deepLinkInfo#6a4ee832 flags:# update_app:flags.0?true message:string entities:flags.1?Vector = help.DeepLinkInfo; - -savedPhoneContact#1142bd56 phone:string first_name:string last_name:string date:int = SavedContact; - -account.takeout#4dba4501 id:long = account.Takeout; - -passwordKdfAlgoUnknown#d45ab096 = PasswordKdfAlgo; -passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow#3a912d4a salt1:bytes salt2:bytes g:int p:bytes = PasswordKdfAlgo; - -securePasswordKdfAlgoUnknown#4a8537 = SecurePasswordKdfAlgo; -securePasswordKdfAlgoPBKDF2HMACSHA512iter100000#bbf2dda0 salt:bytes = SecurePasswordKdfAlgo; -securePasswordKdfAlgoSHA512#86471d92 salt:bytes = SecurePasswordKdfAlgo; - -secureSecretSettings#1527bcac secure_algo:SecurePasswordKdfAlgo secure_secret:bytes secure_secret_id:long = SecureSecretSettings; - -inputCheckPasswordEmpty#9880f658 = InputCheckPasswordSRP; -inputCheckPasswordSRP#d27ff082 srp_id:long A:bytes M1:bytes = InputCheckPasswordSRP; - -secureRequiredType#829d99da flags:# native_names:flags.0?true selfie_required:flags.1?true translation_required:flags.2?true type:SecureValueType = SecureRequiredType; -secureRequiredTypeOneOf#27477b4 types:Vector = SecureRequiredType; - -help.passportConfigNotModified#bfb9f457 = help.PassportConfig; -help.passportConfig#a098d6af hash:int countries_langs:DataJSON = help.PassportConfig; - -inputAppEvent#1d1b1245 time:double type:string peer:long data:JSONValue = InputAppEvent; - -jsonObjectValue#c0de1bd9 key:string value:JSONValue = JSONObjectValue; - -jsonNull#3f6d7b68 = JSONValue; -jsonBool#c7345e6a value:Bool = JSONValue; -jsonNumber#2be0dfa4 value:double = JSONValue; -jsonString#b71e767a value:string = JSONValue; -jsonArray#f7444763 value:Vector = JSONValue; -jsonObject#99c1d49d value:Vector = JSONValue; - -pageTableCell#34566b6a flags:# header:flags.0?true align_center:flags.3?true align_right:flags.4?true valign_middle:flags.5?true valign_bottom:flags.6?true text:flags.7?RichText colspan:flags.1?int rowspan:flags.2?int = PageTableCell; - -pageTableRow#e0c0c5e5 cells:Vector = PageTableRow; - -pageCaption#6f747657 text:RichText credit:RichText = PageCaption; - -pageListItemText#b92fb6cd text:RichText = PageListItem; -pageListItemBlocks#25e073fc blocks:Vector = PageListItem; - -pageListOrderedItemText#5e068047 num:string text:RichText = PageListOrderedItem; -pageListOrderedItemBlocks#98dd8936 num:string blocks:Vector = PageListOrderedItem; - -pageRelatedArticle#b390dc08 flags:# url:string webpage_id:long title:flags.0?string description:flags.1?string photo_id:flags.2?long author:flags.3?string published_date:flags.4?int = PageRelatedArticle; - -page#98657f0d flags:# part:flags.0?true rtl:flags.1?true v2:flags.2?true url:string blocks:Vector photos:Vector documents:Vector views:flags.3?int = Page; - -help.supportName#8c05f1c9 name:string = help.SupportName; - -help.userInfoEmpty#f3ae2eed = help.UserInfo; -help.userInfo#1eb3758 message:string entities:Vector author:string date:int = help.UserInfo; - -pollAnswer#6ca9c2e9 text:string option:bytes = PollAnswer; - -poll#86e18161 id:long flags:# closed:flags.0?true public_voters:flags.1?true multiple_choice:flags.2?true quiz:flags.3?true question:string answers:Vector close_period:flags.4?int close_date:flags.5?int = Poll; - -pollAnswerVoters#3b6ddad2 flags:# chosen:flags.0?true correct:flags.1?true option:bytes voters:int = PollAnswerVoters; - -pollResults#dcb82ea3 flags:# min:flags.0?true results:flags.1?Vector total_voters:flags.2?int recent_voters:flags.3?Vector solution:flags.4?string solution_entities:flags.4?Vector = PollResults; - -chatOnlines#f041e250 onlines:int = ChatOnlines; - -statsURL#47a971e0 url:string = StatsURL; - -chatAdminRights#5fb224d5 flags:# change_info:flags.0?true post_messages:flags.1?true edit_messages:flags.2?true delete_messages:flags.3?true ban_users:flags.4?true invite_users:flags.5?true pin_messages:flags.7?true add_admins:flags.9?true anonymous:flags.10?true manage_call:flags.11?true other:flags.12?true manage_topics:flags.13?true = ChatAdminRights; - -chatBannedRights#9f120418 flags:# view_messages:flags.0?true send_messages:flags.1?true send_media:flags.2?true send_stickers:flags.3?true send_gifs:flags.4?true send_games:flags.5?true send_inline:flags.6?true embed_links:flags.7?true send_polls:flags.8?true change_info:flags.10?true invite_users:flags.15?true pin_messages:flags.17?true manage_topics:flags.18?true send_photos:flags.19?true send_videos:flags.20?true send_roundvideos:flags.21?true send_audios:flags.22?true send_voices:flags.23?true send_docs:flags.24?true send_plain:flags.25?true until_date:int = ChatBannedRights; - -inputWallPaper#e630b979 id:long access_hash:long = InputWallPaper; -inputWallPaperSlug#72091c80 slug:string = InputWallPaper; -inputWallPaperNoFile#967a462e id:long = InputWallPaper; - -account.wallPapersNotModified#1c199183 = account.WallPapers; -account.wallPapers#cdc3858c hash:long wallpapers:Vector = account.WallPapers; - -codeSettings#ad253d78 flags:# allow_flashcall:flags.0?true current_number:flags.1?true allow_app_hash:flags.4?true allow_missed_call:flags.5?true allow_firebase:flags.7?true logout_tokens:flags.6?Vector token:flags.8?string app_sandbox:flags.8?Bool = CodeSettings; - -wallPaperSettings#1dc1bca4 flags:# blur:flags.1?true motion:flags.2?true background_color:flags.0?int second_background_color:flags.4?int third_background_color:flags.5?int fourth_background_color:flags.6?int intensity:flags.3?int rotation:flags.4?int = WallPaperSettings; - -autoDownloadSettings#8efab953 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:long file_size_max:long video_upload_maxbitrate:int = AutoDownloadSettings; - -account.autoDownloadSettings#63cacf26 low:AutoDownloadSettings medium:AutoDownloadSettings high:AutoDownloadSettings = account.AutoDownloadSettings; - -emojiKeyword#d5b3b9f9 keyword:string emoticons:Vector = EmojiKeyword; -emojiKeywordDeleted#236df622 keyword:string emoticons:Vector = EmojiKeyword; - -emojiKeywordsDifference#5cc761bd lang_code:string from_version:int version:int keywords:Vector = EmojiKeywordsDifference; - -emojiURL#a575739d url:string = EmojiURL; - -emojiLanguage#b3fb5361 lang_code:string = EmojiLanguage; - -folder#ff544e65 flags:# autofill_new_broadcasts:flags.0?true autofill_public_groups:flags.1?true autofill_new_correspondents:flags.2?true id:int title:string photo:flags.3?ChatPhoto = Folder; - -inputFolderPeer#fbd2c296 peer:InputPeer folder_id:int = InputFolderPeer; - -folderPeer#e9baa668 peer:Peer folder_id:int = FolderPeer; - -messages.searchCounter#e844ebff flags:# inexact:flags.1?true filter:MessagesFilter count:int = messages.SearchCounter; - -urlAuthResultRequest#92d33a0e flags:# request_write_access:flags.0?true bot:User domain:string = UrlAuthResult; -urlAuthResultAccepted#8f8c0e4e url:string = UrlAuthResult; -urlAuthResultDefault#a9d6db1f = UrlAuthResult; - -channelLocationEmpty#bfb5ad8b = ChannelLocation; -channelLocation#209b82db geo_point:GeoPoint address:string = ChannelLocation; - -peerLocated#ca461b5d peer:Peer expires:int distance:int = PeerLocated; -peerSelfLocated#f8ec284b expires:int = PeerLocated; - -restrictionReason#d072acb4 platform:string reason:string text:string = RestrictionReason; - -inputTheme#3c5693e9 id:long access_hash:long = InputTheme; -inputThemeSlug#f5890df1 slug:string = InputTheme; - -theme#a00e67d6 flags:# creator:flags.0?true default:flags.1?true for_chat:flags.5?true id:long access_hash:long slug:string title:string document:flags.2?Document settings:flags.3?Vector emoticon:flags.6?string installs_count:flags.4?int = Theme; - -account.themesNotModified#f41eb622 = account.Themes; -account.themes#9a3d8c6d hash:long themes:Vector = account.Themes; - -auth.loginToken#629f1980 expires:int token:bytes = auth.LoginToken; -auth.loginTokenMigrateTo#68e9916 dc_id:int token:bytes = auth.LoginToken; -auth.loginTokenSuccess#390d5c5e authorization:auth.Authorization = auth.LoginToken; - -account.contentSettings#57e28221 flags:# sensitive_enabled:flags.0?true sensitive_can_change:flags.1?true = account.ContentSettings; - -messages.inactiveChats#a927fec5 dates:Vector chats:Vector users:Vector = messages.InactiveChats; - -baseThemeClassic#c3a12462 = BaseTheme; -baseThemeDay#fbd81688 = BaseTheme; -baseThemeNight#b7b31ea8 = BaseTheme; -baseThemeTinted#6d5f77ee = BaseTheme; -baseThemeArctic#5b11125a = BaseTheme; - -inputThemeSettings#8fde504f flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?InputWallPaper wallpaper_settings:flags.1?WallPaperSettings = InputThemeSettings; - -themeSettings#fa58b6d4 flags:# message_colors_animated:flags.2?true base_theme:BaseTheme accent_color:int outbox_accent_color:flags.3?int message_colors:flags.0?Vector wallpaper:flags.1?WallPaper = ThemeSettings; - -webPageAttributeTheme#54b56617 flags:# documents:flags.0?Vector settings:flags.1?ThemeSettings = WebPageAttribute; - -messageUserVote#34d247b4 user_id:long option:bytes date:int = MessageUserVote; -messageUserVoteInputOption#3ca5b0ec user_id:long date:int = MessageUserVote; -messageUserVoteMultiple#8a65e557 user_id:long options:Vector date:int = MessageUserVote; - -messages.votesList#823f649 flags:# count:int votes:Vector users:Vector next_offset:flags.0?string = messages.VotesList; - -bankCardOpenUrl#f568028a url:string name:string = BankCardOpenUrl; - -payments.bankCardData#3e24e573 title:string open_urls:Vector = payments.BankCardData; - -dialogFilter#7438f7e8 flags:# contacts:flags.0?true non_contacts:flags.1?true groups:flags.2?true broadcasts:flags.3?true bots:flags.4?true exclude_muted:flags.11?true exclude_read:flags.12?true exclude_archived:flags.13?true id:int title:string emoticon:flags.25?string pinned_peers:Vector include_peers:Vector exclude_peers:Vector = DialogFilter; -dialogFilterDefault#363293ae = DialogFilter; -dialogFilterChatlist#d64a04a8 flags:# has_my_invites:flags.26?true id:int title:string emoticon:flags.25?string pinned_peers:Vector include_peers:Vector = DialogFilter; - -dialogFilterSuggested#77744d4a filter:DialogFilter description:string = DialogFilterSuggested; - -statsDateRangeDays#b637edaf min_date:int max_date:int = StatsDateRangeDays; - -statsAbsValueAndPrev#cb43acde current:double previous:double = StatsAbsValueAndPrev; - -statsPercentValue#cbce2fe0 part:double total:double = StatsPercentValue; - -statsGraphAsync#4a27eb2d token:string = StatsGraph; -statsGraphError#bedc9822 error:string = StatsGraph; -statsGraph#8ea464b6 flags:# json:DataJSON zoom_token:flags.0?string = StatsGraph; - -messageInteractionCounters#ad4fc9bd msg_id:int views:int forwards:int = MessageInteractionCounters; - -stats.broadcastStats#bdf78394 period:StatsDateRangeDays followers:StatsAbsValueAndPrev views_per_post:StatsAbsValueAndPrev shares_per_post:StatsAbsValueAndPrev enabled_notifications:StatsPercentValue growth_graph:StatsGraph followers_graph:StatsGraph mute_graph:StatsGraph top_hours_graph:StatsGraph interactions_graph:StatsGraph iv_interactions_graph:StatsGraph views_by_source_graph:StatsGraph new_followers_by_source_graph:StatsGraph languages_graph:StatsGraph recent_message_interactions:Vector = stats.BroadcastStats; - -help.promoDataEmpty#98f6ac75 expires:int = help.PromoData; -help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer chats:Vector users:Vector psa_type:flags.1?string psa_message:flags.2?string = help.PromoData; - -videoSize#de33b094 flags:# type:string w:int h:int size:int video_start_ts:flags.0?double = VideoSize; -videoSizeEmojiMarkup#f85c413c emoji_id:long background_colors:Vector = VideoSize; -videoSizeStickerMarkup#da082fe stickerset:InputStickerSet sticker_id:long background_colors:Vector = VideoSize; - -statsGroupTopPoster#9d04af9b user_id:long messages:int avg_chars:int = StatsGroupTopPoster; - -statsGroupTopAdmin#d7584c87 user_id:long deleted:int kicked:int banned:int = StatsGroupTopAdmin; - -statsGroupTopInviter#535f779d user_id:long invitations:int = StatsGroupTopInviter; - -stats.megagroupStats#ef7ff916 period:StatsDateRangeDays members:StatsAbsValueAndPrev messages:StatsAbsValueAndPrev viewers:StatsAbsValueAndPrev posters:StatsAbsValueAndPrev growth_graph:StatsGraph members_graph:StatsGraph new_members_by_source_graph:StatsGraph languages_graph:StatsGraph messages_graph:StatsGraph actions_graph:StatsGraph top_hours_graph:StatsGraph weekdays_graph:StatsGraph top_posters:Vector top_admins:Vector top_inviters:Vector users:Vector = stats.MegagroupStats; - -globalPrivacySettings#bea2f424 flags:# archive_and_mute_new_noncontact_peers:flags.0?Bool = GlobalPrivacySettings; - -help.countryCode#4203c5ef flags:# country_code:string prefixes:flags.0?Vector patterns:flags.1?Vector = help.CountryCode; - -help.country#c3878e23 flags:# hidden:flags.0?true iso2:string default_name:string name:flags.1?string country_codes:Vector = help.Country; - -help.countriesListNotModified#93cc1f32 = help.CountriesList; -help.countriesList#87d0759e countries:Vector hash:int = help.CountriesList; - -messageViews#455b853d flags:# views:flags.0?int forwards:flags.1?int replies:flags.2?MessageReplies = MessageViews; - -messages.messageViews#b6c4f543 views:Vector chats:Vector users:Vector = messages.MessageViews; - -messages.discussionMessage#a6341782 flags:# messages:Vector max_id:flags.0?int read_inbox_max_id:flags.1?int read_outbox_max_id:flags.2?int unread_count:int chats:Vector users:Vector = messages.DiscussionMessage; - -messageReplyHeader#a6d57763 flags:# reply_to_scheduled:flags.2?true forum_topic:flags.3?true reply_to_msg_id:int reply_to_peer_id:flags.0?Peer reply_to_top_id:flags.1?int = MessageReplyHeader; - -messageReplies#83d60fc2 flags:# comments:flags.0?true replies:int replies_pts:int recent_repliers:flags.1?Vector channel_id:flags.0?long max_id:flags.2?int read_max_id:flags.3?int = MessageReplies; - -peerBlocked#e8fd8014 peer_id:Peer date:int = PeerBlocked; - -stats.messageStats#8999f295 views_graph:StatsGraph = stats.MessageStats; - -groupCallDiscarded#7780bcb4 id:long access_hash:long duration:int = GroupCall; -groupCall#d597650c flags:# join_muted:flags.1?true can_change_join_muted:flags.2?true join_date_asc:flags.6?true schedule_start_subscribed:flags.8?true can_start_video:flags.9?true record_video_active:flags.11?true rtmp_stream:flags.12?true listeners_hidden:flags.13?true id:long access_hash:long participants_count:int title:flags.3?string stream_dc_id:flags.4?int record_start_date:flags.5?int schedule_date:flags.7?int unmuted_video_count:flags.10?int unmuted_video_limit:int version:int = GroupCall; - -inputGroupCall#d8aa840f id:long access_hash:long = InputGroupCall; - -groupCallParticipant#eba636fe flags:# muted:flags.0?true left:flags.1?true can_self_unmute:flags.2?true just_joined:flags.4?true versioned:flags.5?true min:flags.8?true muted_by_you:flags.9?true volume_by_admin:flags.10?true self:flags.12?true video_joined:flags.15?true peer:Peer date:int active_date:flags.3?int source:int volume:flags.7?int about:flags.11?string raise_hand_rating:flags.13?long video:flags.6?GroupCallParticipantVideo presentation:flags.14?GroupCallParticipantVideo = GroupCallParticipant; - -phone.groupCall#9e727aad call:GroupCall participants:Vector participants_next_offset:string chats:Vector users:Vector = phone.GroupCall; - -phone.groupParticipants#f47751b6 count:int participants:Vector next_offset:string chats:Vector users:Vector version:int = phone.GroupParticipants; - -inlineQueryPeerTypeSameBotPM#3081ed9d = InlineQueryPeerType; -inlineQueryPeerTypePM#833c0fac = InlineQueryPeerType; -inlineQueryPeerTypeChat#d766c50a = InlineQueryPeerType; -inlineQueryPeerTypeMegagroup#5ec4be43 = InlineQueryPeerType; -inlineQueryPeerTypeBroadcast#6334ee9a = InlineQueryPeerType; -inlineQueryPeerTypeBotPM#e3b2d0c = InlineQueryPeerType; - -messages.historyImport#1662af0b id:long = messages.HistoryImport; - -messages.historyImportParsed#5e0fb7b9 flags:# pm:flags.0?true group:flags.1?true title:flags.2?string = messages.HistoryImportParsed; - -messages.affectedFoundMessages#ef8d3e6c pts:int pts_count:int offset:int messages:Vector = messages.AffectedFoundMessages; - -chatInviteImporter#8c5adfd9 flags:# requested:flags.0?true via_chatlist:flags.3?true user_id:long date:int about:flags.2?string approved_by:flags.1?long = ChatInviteImporter; - -messages.exportedChatInvites#bdc62dcc count:int invites:Vector users:Vector = messages.ExportedChatInvites; - -messages.exportedChatInvite#1871be50 invite:ExportedChatInvite users:Vector = messages.ExportedChatInvite; -messages.exportedChatInviteReplaced#222600ef invite:ExportedChatInvite new_invite:ExportedChatInvite users:Vector = messages.ExportedChatInvite; - -messages.chatInviteImporters#81b6b00a count:int importers:Vector users:Vector = messages.ChatInviteImporters; - -chatAdminWithInvites#f2ecef23 admin_id:long invites_count:int revoked_invites_count:int = ChatAdminWithInvites; - -messages.chatAdminsWithInvites#b69b72d7 admins:Vector users:Vector = messages.ChatAdminsWithInvites; - -messages.checkedHistoryImportPeer#a24de717 confirm_text:string = messages.CheckedHistoryImportPeer; - -phone.joinAsPeers#afe5623f peers:Vector chats:Vector users:Vector = phone.JoinAsPeers; - -phone.exportedGroupCallInvite#204bd158 link:string = phone.ExportedGroupCallInvite; - -groupCallParticipantVideoSourceGroup#dcb118b7 semantics:string sources:Vector = GroupCallParticipantVideoSourceGroup; - -groupCallParticipantVideo#67753ac8 flags:# paused:flags.0?true endpoint:string source_groups:Vector audio_source:flags.1?int = GroupCallParticipantVideo; - -stickers.suggestedShortName#85fea03f short_name:string = stickers.SuggestedShortName; - -botCommandScopeDefault#2f6cb2ab = BotCommandScope; -botCommandScopeUsers#3c4f04d8 = BotCommandScope; -botCommandScopeChats#6fe1a881 = BotCommandScope; -botCommandScopeChatAdmins#b9aa606a = BotCommandScope; -botCommandScopePeer#db9d897d peer:InputPeer = BotCommandScope; -botCommandScopePeerAdmins#3fd863d1 peer:InputPeer = BotCommandScope; -botCommandScopePeerUser#a1321f3 peer:InputPeer user_id:InputUser = BotCommandScope; - -account.resetPasswordFailedWait#e3779861 retry_date:int = account.ResetPasswordResult; -account.resetPasswordRequestedWait#e9effc7d until_date:int = account.ResetPasswordResult; -account.resetPasswordOk#e926d63e = account.ResetPasswordResult; - -sponsoredMessage#fc25b828 flags:# recommended:flags.5?true show_peer_photo:flags.6?true random_id:bytes from_id:flags.3?Peer chat_invite:flags.4?ChatInvite chat_invite_hash:flags.4?string channel_post:flags.2?int start_param:flags.0?string message:string entities:flags.1?Vector sponsor_info:flags.7?string additional_info:flags.8?string = SponsoredMessage; - -messages.sponsoredMessages#c9ee1d87 flags:# posts_between:flags.0?int messages:Vector chats:Vector users:Vector = messages.SponsoredMessages; -messages.sponsoredMessagesEmpty#1839490f = messages.SponsoredMessages; - -searchResultsCalendarPeriod#c9b0539f date:int min_msg_id:int max_msg_id:int count:int = SearchResultsCalendarPeriod; - -messages.searchResultsCalendar#147ee23c flags:# inexact:flags.0?true count:int min_date:int min_msg_id:int offset_id_offset:flags.1?int periods:Vector messages:Vector chats:Vector users:Vector = messages.SearchResultsCalendar; - -searchResultPosition#7f648b67 msg_id:int date:int offset:int = SearchResultsPosition; - -messages.searchResultsPositions#53b22baf count:int positions:Vector = messages.SearchResultsPositions; - -channels.sendAsPeers#f496b0c6 peers:Vector chats:Vector users:Vector = channels.SendAsPeers; - -users.userFull#3b6d152e full_user:UserFull chats:Vector users:Vector = users.UserFull; - -messages.peerSettings#6880b94d settings:PeerSettings chats:Vector users:Vector = messages.PeerSettings; - -auth.loggedOut#c3a2835f flags:# future_auth_token:flags.0?bytes = auth.LoggedOut; - -reactionCount#a3d1cb80 flags:# chosen_order:flags.0?int reaction:Reaction count:int = ReactionCount; - -messageReactions#4f2b9479 flags:# min:flags.0?true can_see_list:flags.2?true results:Vector recent_reactions:flags.1?Vector = MessageReactions; - -messages.messageReactionsList#31bd492d flags:# count:int reactions:Vector chats:Vector users:Vector next_offset:flags.0?string = messages.MessageReactionsList; - -availableReaction#c077ec01 flags:# inactive:flags.0?true premium:flags.2?true reaction:string title:string static_icon:Document appear_animation:Document select_animation:Document activate_animation:Document effect_animation:Document around_animation:flags.1?Document center_icon:flags.1?Document = AvailableReaction; - -messages.availableReactionsNotModified#9f071957 = messages.AvailableReactions; -messages.availableReactions#768e3aad hash:int reactions:Vector = messages.AvailableReactions; - -messagePeerReaction#8c79b63c flags:# big:flags.0?true unread:flags.1?true peer_id:Peer date:int reaction:Reaction = MessagePeerReaction; - -groupCallStreamChannel#80eb48af channel:int scale:int last_timestamp_ms:long = GroupCallStreamChannel; - -phone.groupCallStreamChannels#d0e482b2 channels:Vector = phone.GroupCallStreamChannels; - -phone.groupCallStreamRtmpUrl#2dbf3432 url:string key:string = phone.GroupCallStreamRtmpUrl; - -attachMenuBotIconColor#4576f3f0 name:string color:int = AttachMenuBotIconColor; - -attachMenuBotIcon#b2a7386b flags:# name:string icon:Document colors:flags.0?Vector = AttachMenuBotIcon; - -attachMenuBot#c8aa2cd2 flags:# inactive:flags.0?true has_settings:flags.1?true request_write_access:flags.2?true bot_id:long short_name:string peer_types:Vector icons:Vector = AttachMenuBot; - -attachMenuBotsNotModified#f1d88a5c = AttachMenuBots; -attachMenuBots#3c4301c0 hash:long bots:Vector users:Vector = AttachMenuBots; - -attachMenuBotsBot#93bf667f bot:AttachMenuBot users:Vector = AttachMenuBotsBot; - -webViewResultUrl#c14557c query_id:long url:string = WebViewResult; - -simpleWebViewResultUrl#882f76bb url:string = SimpleWebViewResult; - -webViewMessageSent#c94511c flags:# msg_id:flags.0?InputBotInlineMessageID = WebViewMessageSent; - -botMenuButtonDefault#7533a588 = BotMenuButton; -botMenuButtonCommands#4258c205 = BotMenuButton; -botMenuButton#c7b57ce6 text:string url:string = BotMenuButton; - -account.savedRingtonesNotModified#fbf6e8b1 = account.SavedRingtones; -account.savedRingtones#c1e92cc5 hash:long ringtones:Vector = account.SavedRingtones; - -notificationSoundDefault#97e8bebe = NotificationSound; -notificationSoundNone#6f0c34df = NotificationSound; -notificationSoundLocal#830b9ae4 title:string data:string = NotificationSound; -notificationSoundRingtone#ff6c8049 id:long = NotificationSound; - -account.savedRingtone#b7263f6d = account.SavedRingtone; -account.savedRingtoneConverted#1f307eb7 document:Document = account.SavedRingtone; - -attachMenuPeerTypeSameBotPM#7d6be90e = AttachMenuPeerType; -attachMenuPeerTypeBotPM#c32bfa1a = AttachMenuPeerType; -attachMenuPeerTypePM#f146d31f = AttachMenuPeerType; -attachMenuPeerTypeChat#509113f = AttachMenuPeerType; -attachMenuPeerTypeBroadcast#7bfbdefc = AttachMenuPeerType; - -inputInvoiceMessage#c5b56859 peer:InputPeer msg_id:int = InputInvoice; -inputInvoiceSlug#c326caef slug:string = InputInvoice; - -payments.exportedInvoice#aed0cbd9 url:string = payments.ExportedInvoice; - -messages.transcribedAudio#93752c52 flags:# pending:flags.0?true transcription_id:long text:string = messages.TranscribedAudio; - -help.premiumPromo#5334759c status_text:string status_entities:Vector video_sections:Vector videos:Vector period_options:Vector users:Vector = help.PremiumPromo; - -inputStorePaymentPremiumSubscription#a6751e66 flags:# restore:flags.0?true upgrade:flags.1?true = InputStorePaymentPurpose; -inputStorePaymentGiftPremium#616f7fe8 user_id:InputUser currency:string amount:long = InputStorePaymentPurpose; - -premiumGiftOption#74c34319 flags:# months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumGiftOption; - -paymentFormMethod#88f8f21b url:string title:string = PaymentFormMethod; - -emojiStatusEmpty#2de11aae = EmojiStatus; -emojiStatus#929b619d document_id:long = EmojiStatus; -emojiStatusUntil#fa30a8c7 document_id:long until:int = EmojiStatus; - -account.emojiStatusesNotModified#d08ce645 = account.EmojiStatuses; -account.emojiStatuses#90c467d1 hash:long statuses:Vector = account.EmojiStatuses; - -reactionEmpty#79f5d419 = Reaction; -reactionEmoji#1b2286b8 emoticon:string = Reaction; -reactionCustomEmoji#8935fc73 document_id:long = Reaction; - -chatReactionsNone#eafc32bc = ChatReactions; -chatReactionsAll#52928bca flags:# allow_custom:flags.0?true = ChatReactions; -chatReactionsSome#661d4037 reactions:Vector = ChatReactions; - -messages.reactionsNotModified#b06fdbdf = messages.Reactions; -messages.reactions#eafdf716 hash:long reactions:Vector = messages.Reactions; - -emailVerifyPurposeLoginSetup#4345be73 phone_number:string phone_code_hash:string = EmailVerifyPurpose; -emailVerifyPurposeLoginChange#527d22eb = EmailVerifyPurpose; -emailVerifyPurposePassport#bbf51685 = EmailVerifyPurpose; - -emailVerificationCode#922e55a9 code:string = EmailVerification; -emailVerificationGoogle#db909ec2 token:string = EmailVerification; -emailVerificationApple#96d074fd token:string = EmailVerification; - -account.emailVerified#2b96cd1b email:string = account.EmailVerified; -account.emailVerifiedLogin#e1bb0d61 email:string sent_code:auth.SentCode = account.EmailVerified; - -premiumSubscriptionOption#5f2d1df2 flags:# current:flags.1?true can_purchase_upgrade:flags.2?true transaction:flags.3?string months:int currency:string amount:long bot_url:string store_product:flags.0?string = PremiumSubscriptionOption; - -sendAsPeer#b81c7034 flags:# premium_required:flags.0?true peer:Peer = SendAsPeer; - -messageExtendedMediaPreview#ad628cc8 flags:# w:flags.0?int h:flags.0?int thumb:flags.1?PhotoSize video_duration:flags.2?int = MessageExtendedMedia; -messageExtendedMedia#ee479c64 media:MessageMedia = MessageExtendedMedia; - -stickerKeyword#fcfeb29c document_id:long keyword:Vector = StickerKeyword; - -username#b4073647 flags:# editable:flags.0?true active:flags.1?true username:string = Username; - -forumTopicDeleted#23f109b id:int = ForumTopic; -forumTopic#71701da9 flags:# my:flags.1?true closed:flags.2?true pinned:flags.3?true short:flags.5?true hidden:flags.6?true id:int date:int title:string icon_color:int icon_emoji_id:flags.0?long top_message:int read_inbox_max_id:int read_outbox_max_id:int unread_count:int unread_mentions_count:int unread_reactions_count:int from_id:Peer notify_settings:PeerNotifySettings draft:flags.4?DraftMessage = ForumTopic; - -messages.forumTopics#367617d3 flags:# order_by_create_date:flags.0?true count:int topics:Vector messages:Vector chats:Vector users:Vector pts:int = messages.ForumTopics; - -defaultHistoryTTL#43b46b20 period:int = DefaultHistoryTTL; - -exportedContactToken#41bf109b url:string expires:int = ExportedContactToken; - -requestPeerTypeUser#5f3b8a00 flags:# bot:flags.0?Bool premium:flags.1?Bool = RequestPeerType; -requestPeerTypeChat#c9f06e1b flags:# creator:flags.0?true bot_participant:flags.5?true has_username:flags.3?Bool forum:flags.4?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; -requestPeerTypeBroadcast#339bef6c flags:# creator:flags.0?true has_username:flags.3?Bool user_admin_rights:flags.1?ChatAdminRights bot_admin_rights:flags.2?ChatAdminRights = RequestPeerType; - -emojiListNotModified#481eadfa = EmojiList; -emojiList#7a1e11d1 hash:long document_id:Vector = EmojiList; - -emojiGroup#7a9abda9 title:string icon_emoji_id:long emoticons:Vector = EmojiGroup; - -messages.emojiGroupsNotModified#6fb4ad87 = messages.EmojiGroups; -messages.emojiGroups#881fb94b hash:int groups:Vector = messages.EmojiGroups; - -textWithEntities#751f3146 text:string entities:Vector = TextWithEntities; - -messages.translateResult#33db32f8 result:Vector = messages.TranslatedText; - -autoSaveSettings#c84834ce flags:# photos:flags.0?true videos:flags.1?true video_max_size:flags.2?long = AutoSaveSettings; - -autoSaveException#81602d47 peer:Peer settings:AutoSaveSettings = AutoSaveException; - -account.autoSaveSettings#4c3e069d users_settings:AutoSaveSettings chats_settings:AutoSaveSettings broadcasts_settings:AutoSaveSettings exceptions:Vector chats:Vector users:Vector = account.AutoSaveSettings; - -help.appConfigNotModified#7cde641d = help.AppConfig; -help.appConfig#dd18782e hash:int config:JSONValue = help.AppConfig; - -inputBotAppID#a920bd7a id:long access_hash:long = InputBotApp; -inputBotAppShortName#908c0407 bot_id:InputUser short_name:string = InputBotApp; - -botAppNotModified#5da674b7 = BotApp; -botApp#95fcd1d6 flags:# id:long access_hash:long short_name:string title:string description:string photo:Photo document:flags.0?Document hash:long = BotApp; - -messages.botApp#eb50adf5 flags:# inactive:flags.0?true request_write_access:flags.1?true app:BotApp = messages.BotApp; - -appWebViewResultUrl#3c1b4f0d url:string = AppWebViewResult; - -inlineBotWebView#b57295d5 text:string url:string = InlineBotWebView; - -readParticipantDate#4a4ff172 user_id:long date:int = ReadParticipantDate; - -inputChatlistDialogFilter#f3e0da33 filter_id:int = InputChatlist; - -exportedChatlistInvite#c5181ac flags:# title:string url:string peers:Vector = ExportedChatlistInvite; - -chatlists.exportedChatlistInvite#10e6e3a6 filter:DialogFilter invite:ExportedChatlistInvite = chatlists.ExportedChatlistInvite; - -chatlists.exportedInvites#10ab6dc7 invites:Vector chats:Vector users:Vector = chatlists.ExportedInvites; - -chatlists.chatlistInviteAlready#fa87f659 filter_id:int missing_peers:Vector already_peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; -chatlists.chatlistInvite#1dcd839d flags:# title:string emoticon:flags.0?string peers:Vector chats:Vector users:Vector = chatlists.ChatlistInvite; - -chatlists.chatlistUpdates#93bd878d missing_peers:Vector chats:Vector users:Vector = chatlists.ChatlistUpdates; - -bots.botInfo#e8a775b0 name:string about:string description:string = bots.BotInfo; - ----functions--- - -invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; -invokeAfterMsgs#3dc4b4f0 {X:Type} msg_ids:Vector query:!X = X; -initConnection#c1cd5ea9 {X:Type} flags:# api_id:int device_model:string system_version:string app_version:string system_lang_code:string lang_pack:string lang_code:string proxy:flags.0?InputClientProxy params:flags.1?JSONValue query:!X = X; -invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X; -invokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X; -invokeWithMessagesRange#365275f2 {X:Type} range:MessageRange query:!X = X; -invokeWithTakeout#aca9fd2e {X:Type} takeout_id:long query:!X = X; - -auth.sendCode#a677244f phone_number:string api_id:int api_hash:string settings:CodeSettings = auth.SentCode; -auth.signUp#80eee427 phone_number:string phone_code_hash:string first_name:string last_name:string = auth.Authorization; -auth.signIn#8d52a951 flags:# phone_number:string phone_code_hash:string phone_code:flags.0?string email_verification:flags.1?EmailVerification = auth.Authorization; -auth.logOut#3e72ba19 = auth.LoggedOut; -auth.resetAuthorizations#9fab0d1a = Bool; -auth.exportAuthorization#e5bfffcd dc_id:int = auth.ExportedAuthorization; -auth.importAuthorization#a57a7dad id:long bytes:bytes = auth.Authorization; -auth.bindTempAuthKey#cdd42a05 perm_auth_key_id:long nonce:long expires_at:int encrypted_message:bytes = Bool; -auth.importBotAuthorization#67a3ff2c flags:int api_id:int api_hash:string bot_auth_token:string = auth.Authorization; -auth.checkPassword#d18b4d16 password:InputCheckPasswordSRP = auth.Authorization; -auth.requestPasswordRecovery#d897bc66 = auth.PasswordRecovery; -auth.recoverPassword#37096c70 flags:# code:string new_settings:flags.0?account.PasswordInputSettings = auth.Authorization; -auth.resendCode#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; -auth.exportLoginToken#b7e085fe api_id:int api_hash:string except_ids:Vector = auth.LoginToken; -auth.importLoginToken#95ac5ce4 token:bytes = auth.LoginToken; -auth.acceptLoginToken#e894ad4d token:bytes = Authorization; -auth.checkRecoveryPassword#d36bf79 code:string = Bool; -auth.importWebTokenAuthorization#2db873a9 api_id:int api_hash:string web_auth_token:string = auth.Authorization; -auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool; -auth.resetLoginEmail#7e960193 phone_number:string phone_code_hash:string = auth.SentCode; - -account.registerDevice#ec86017a flags:# no_muted:flags.0?true token_type:int token:string app_sandbox:Bool secret:bytes other_uids:Vector = Bool; -account.unregisterDevice#6a0d3206 token_type:int token:string other_uids:Vector = Bool; -account.updateNotifySettings#84be5b93 peer:InputNotifyPeer settings:InputPeerNotifySettings = Bool; -account.getNotifySettings#12b3ad31 peer:InputNotifyPeer = PeerNotifySettings; -account.resetNotifySettings#db7e1747 = Bool; -account.updateProfile#78515775 flags:# first_name:flags.0?string last_name:flags.1?string about:flags.2?string = User; -account.updateStatus#6628562c offline:Bool = Bool; -account.getWallPapers#7967d36 hash:long = account.WallPapers; -account.reportPeer#c5ba3d86 peer:InputPeer reason:ReportReason message:string = Bool; -account.checkUsername#2714d86c username:string = Bool; -account.updateUsername#3e0bdd7c username:string = User; -account.getPrivacy#dadbc950 key:InputPrivacyKey = account.PrivacyRules; -account.setPrivacy#c9f81ce8 key:InputPrivacyKey rules:Vector = account.PrivacyRules; -account.deleteAccount#a2c0cf74 flags:# reason:string password:flags.0?InputCheckPasswordSRP = Bool; -account.getAccountTTL#8fc711d = AccountDaysTTL; -account.setAccountTTL#2442485e ttl:AccountDaysTTL = Bool; -account.sendChangePhoneCode#82574ae5 phone_number:string settings:CodeSettings = auth.SentCode; -account.changePhone#70c32edb phone_number:string phone_code_hash:string phone_code:string = User; -account.updateDeviceLocked#38df3532 period:int = Bool; -account.getAuthorizations#e320c158 = account.Authorizations; -account.resetAuthorization#df77f3bc hash:long = Bool; -account.getPassword#548a30f5 = account.Password; -account.getPasswordSettings#9cd4eaf9 password:InputCheckPasswordSRP = account.PasswordSettings; -account.updatePasswordSettings#a59b102f password:InputCheckPasswordSRP new_settings:account.PasswordInputSettings = Bool; -account.sendConfirmPhoneCode#1b3faa88 hash:string settings:CodeSettings = auth.SentCode; -account.confirmPhone#5f2178c3 phone_code_hash:string phone_code:string = Bool; -account.getTmpPassword#449e0b51 password:InputCheckPasswordSRP period:int = account.TmpPassword; -account.getWebAuthorizations#182e6d6f = account.WebAuthorizations; -account.resetWebAuthorization#2d01b9ef hash:long = Bool; -account.resetWebAuthorizations#682d2594 = Bool; -account.getAllSecureValues#b288bc7d = Vector; -account.getSecureValue#73665bc2 types:Vector = Vector; -account.saveSecureValue#899fe31d value:InputSecureValue secure_secret_id:long = SecureValue; -account.deleteSecureValue#b880bc4b types:Vector = Bool; -account.getAuthorizationForm#a929597a bot_id:long scope:string public_key:string = account.AuthorizationForm; -account.acceptAuthorization#f3ed4c73 bot_id:long scope:string public_key:string value_hashes:Vector credentials:SecureCredentialsEncrypted = Bool; -account.sendVerifyPhoneCode#a5a356f9 phone_number:string settings:CodeSettings = auth.SentCode; -account.verifyPhone#4dd3a7f6 phone_number:string phone_code_hash:string phone_code:string = Bool; -account.sendVerifyEmailCode#98e037bb purpose:EmailVerifyPurpose email:string = account.SentEmailCode; -account.verifyEmail#32da4cf purpose:EmailVerifyPurpose verification:EmailVerification = account.EmailVerified; -account.initTakeoutSession#8ef3eab0 flags:# contacts:flags.0?true message_users:flags.1?true message_chats:flags.2?true message_megagroups:flags.3?true message_channels:flags.4?true files:flags.5?true file_max_size:flags.5?long = account.Takeout; -account.finishTakeoutSession#1d2652ee flags:# success:flags.0?true = Bool; -account.confirmPasswordEmail#8fdf1920 code:string = Bool; -account.resendPasswordEmail#7a7f2a15 = Bool; -account.cancelPasswordEmail#c1cbd5b6 = Bool; -account.getContactSignUpNotification#9f07c728 = Bool; -account.setContactSignUpNotification#cff43f61 silent:Bool = Bool; -account.getNotifyExceptions#53577479 flags:# compare_sound:flags.1?true peer:flags.0?InputNotifyPeer = Updates; -account.getWallPaper#fc8ddbea wallpaper:InputWallPaper = WallPaper; -account.uploadWallPaper#e39a8f03 flags:# for_chat:flags.0?true file:InputFile mime_type:string settings:WallPaperSettings = WallPaper; -account.saveWallPaper#6c5a5b37 wallpaper:InputWallPaper unsave:Bool settings:WallPaperSettings = Bool; -account.installWallPaper#feed5769 wallpaper:InputWallPaper settings:WallPaperSettings = Bool; -account.resetWallPapers#bb3b9804 = Bool; -account.getAutoDownloadSettings#56da0b3f = account.AutoDownloadSettings; -account.saveAutoDownloadSettings#76f36233 flags:# low:flags.0?true high:flags.1?true settings:AutoDownloadSettings = Bool; -account.uploadTheme#1c3db333 flags:# file:InputFile thumb:flags.0?InputFile file_name:string mime_type:string = Document; -account.createTheme#652e4400 flags:# slug:string title:string document:flags.2?InputDocument settings:flags.3?Vector = Theme; -account.updateTheme#2bf40ccc flags:# format:string theme:InputTheme slug:flags.0?string title:flags.1?string document:flags.2?InputDocument settings:flags.3?Vector = Theme; -account.saveTheme#f257106c theme:InputTheme unsave:Bool = Bool; -account.installTheme#c727bb3b flags:# dark:flags.0?true theme:flags.1?InputTheme format:flags.2?string base_theme:flags.3?BaseTheme = Bool; -account.getTheme#3a5869ec format:string theme:InputTheme = Theme; -account.getThemes#7206e458 format:string hash:long = account.Themes; -account.setContentSettings#b574b16b flags:# sensitive_enabled:flags.0?true = Bool; -account.getContentSettings#8b9b4dae = account.ContentSettings; -account.getMultiWallPapers#65ad71dc wallpapers:Vector = Vector; -account.getGlobalPrivacySettings#eb2b4cf6 = GlobalPrivacySettings; -account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = GlobalPrivacySettings; -account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool; -account.resetPassword#9308ce1b = account.ResetPasswordResult; -account.declinePasswordReset#4c9409f6 = Bool; -account.getChatThemes#d638de89 hash:long = account.Themes; -account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool; -account.changeAuthorizationSettings#40f48462 flags:# hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool; -account.getSavedRingtones#e1902288 hash:long = account.SavedRingtones; -account.saveRingtone#3dea5b03 id:InputDocument unsave:Bool = account.SavedRingtone; -account.uploadRingtone#831a83a2 file:InputFile file_name:string mime_type:string = Document; -account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool; -account.getDefaultEmojiStatuses#d6753386 hash:long = account.EmojiStatuses; -account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses; -account.clearRecentEmojiStatuses#18201aae = Bool; -account.reorderUsernames#ef500eab order:Vector = Bool; -account.toggleUsername#58d6b376 username:string active:Bool = Bool; -account.getDefaultProfilePhotoEmojis#e2750328 hash:long = EmojiList; -account.getDefaultGroupPhotoEmojis#915860ae hash:long = EmojiList; -account.getAutoSaveSettings#adcbbcda = account.AutoSaveSettings; -account.saveAutoSaveSettings#d69b8361 flags:# users:flags.0?true chats:flags.1?true broadcasts:flags.2?true peer:flags.3?InputPeer settings:AutoSaveSettings = Bool; -account.deleteAutoSaveExceptions#53bc0020 = Bool; - -users.getUsers#d91a548 id:Vector = Vector; -users.getFullUser#b60f5918 id:InputUser = users.UserFull; -users.setSecureValueErrors#90c894b5 id:InputUser errors:Vector = Bool; - -contacts.getContactIDs#7adc669d hash:long = Vector; -contacts.getStatuses#c4a353ee = Vector; -contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; -contacts.importContacts#2c800be5 contacts:Vector = contacts.ImportedContacts; -contacts.deleteContacts#96a0e00 id:Vector = Updates; -contacts.deleteByPhones#1013fd9e phones:Vector = Bool; -contacts.block#68cc1411 id:InputPeer = Bool; -contacts.unblock#bea65d50 id:InputPeer = 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#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true phone_calls:flags.3?true forward_users:flags.4?true forward_chats:flags.5?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:long = contacts.TopPeers; -contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; -contacts.resetSaved#879537f1 = Bool; -contacts.getSaved#82f1e39f = Vector; -contacts.toggleTopPeers#8514bdda enabled:Bool = Bool; -contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates; -contacts.acceptContact#f831a20f id:InputUser = Updates; -contacts.getLocated#d348bc44 flags:# background:flags.1?true geo_point:InputGeoPoint self_expires:flags.0?int = Updates; -contacts.blockFromReplies#29a8962c flags:# delete_message:flags.0?true delete_history:flags.1?true report_spam:flags.2?true msg_id:int = Updates; -contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer; -contacts.exportContactToken#f8654027 = ExportedContactToken; -contacts.importContactToken#13005788 token:string = User; - -messages.getMessages#63c66506 id:Vector = messages.Messages; -messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs; -messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.search#a0fda762 flags:# peer:InputPeer q:string from_id:flags.0?InputPeer top_msg_id:flags.1?int filter:MessagesFilter min_date:int max_date:int offset_id:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.readHistory#e306d3a peer:InputPeer max_id:int = messages.AffectedMessages; -messages.deleteHistory#b08f922a flags:# just_clear:flags.0?true revoke:flags.1?true peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; -messages.deleteMessages#e58e95d2 flags:# revoke:flags.0?true id:Vector = messages.AffectedMessages; -messages.receivedMessages#5a954c0 max_id:int = Vector; -messages.setTyping#58943ee2 flags:# peer:InputPeer top_msg_id:flags.0?int action:SendMessageAction = Bool; -messages.sendMessage#1cc20387 flags:# no_webpage:flags.1?true silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.sendMedia#7547c966 flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int media:InputMedia message:string random_id:long reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.forwardMessages#c661bbc4 flags:# silent:flags.5?true background:flags.6?true with_my_score:flags.8?true drop_author:flags.11?true drop_media_captions:flags.12?true noforwards:flags.14?true from_peer:InputPeer id:Vector random_id:Vector to_peer:InputPeer top_msg_id:flags.9?int schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.reportSpam#cf1592db peer:InputPeer = Bool; -messages.getPeerSettings#efd9a6a2 peer:InputPeer = messages.PeerSettings; -messages.report#8953ab4e peer:InputPeer id:Vector reason:ReportReason message:string = Bool; -messages.getChats#49e9528f id:Vector = messages.Chats; -messages.getFullChat#aeb00b34 chat_id:long = messages.ChatFull; -messages.editChatTitle#73783ffd chat_id:long title:string = Updates; -messages.editChatPhoto#35ddd674 chat_id:long photo:InputChatPhoto = Updates; -messages.addChatUser#f24753e3 chat_id:long user_id:InputUser fwd_limit:int = Updates; -messages.deleteChatUser#a2185cab flags:# revoke_history:flags.0?true chat_id:long user_id:InputUser = Updates; -messages.createChat#34a818 flags:# users:Vector title:string ttl_period:flags.0?int = 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#f393aea0 flags:# delete_history:flags.0?true chat_id:int = Bool; -messages.setEncryptedTyping#791451ed peer:InputEncryptedChat typing:Bool = Bool; -messages.readEncryptedHistory#7f4b690a peer:InputEncryptedChat max_date:int = Bool; -messages.sendEncrypted#44fa7a15 flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; -messages.sendEncryptedFile#5559481d flags:# silent:flags.0?true peer:InputEncryptedChat random_id:long data:bytes file:InputEncryptedFile = messages.SentEncryptedMessage; -messages.sendEncryptedService#32d439a4 peer:InputEncryptedChat random_id:long data:bytes = messages.SentEncryptedMessage; -messages.receivedQueue#55a5bb66 max_qts:int = Vector; -messages.reportEncryptedSpam#4b0c8c0f peer:InputEncryptedChat = Bool; -messages.readMessageContents#36a73f77 id:Vector = messages.AffectedMessages; -messages.getStickers#d5a5d3a1 emoticon:string hash:long = messages.Stickers; -messages.getAllStickers#b8a0a1a8 hash:long = messages.AllStickers; -messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia; -messages.exportChatInvite#a02ce5d5 flags:# legacy_revoke_permanent:flags.2?true request_needed:flags.3?true peer:InputPeer expire_date:flags.0?int usage_limit:flags.1?int title:flags.4?string = ExportedChatInvite; -messages.checkChatInvite#3eadb1bb hash:string = ChatInvite; -messages.importChatInvite#6c50051c hash:string = Updates; -messages.getStickerSet#c8a0ec74 stickerset:InputStickerSet hash:int = messages.StickerSet; -messages.installStickerSet#c78fe460 stickerset:InputStickerSet archived:Bool = messages.StickerSetInstallResult; -messages.uninstallStickerSet#f96e55de stickerset:InputStickerSet = Bool; -messages.startBot#e6df7378 bot:InputUser peer:InputPeer random_id:long start_param:string = Updates; -messages.getMessagesViews#5784d3e1 peer:InputPeer id:Vector increment:Bool = messages.MessageViews; -messages.editChatAdmin#a85bd1c2 chat_id:long user_id:InputUser is_admin:Bool = Bool; -messages.migrateChat#a2875319 chat_id:long = Updates; -messages.searchGlobal#4bc6589a flags:# folder_id:flags.0?int q:string filter:MessagesFilter min_date:int max_date:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; -messages.reorderStickerSets#78337739 flags:# masks:flags.0?true emojis:flags.1?true order:Vector = Bool; -messages.getDocumentByHash#b1f2061f sha256:bytes size:long mime_type:string = Document; -messages.getSavedGifs#5cf09635 hash:long = messages.SavedGifs; -messages.saveGif#327a30cb id:InputDocument unsave:Bool = Bool; -messages.getInlineBotResults#514e999d flags:# bot:InputUser peer:InputPeer geo_point:flags.0?InputGeoPoint query:string offset:string = messages.BotResults; -messages.setInlineBotResults#bb12a419 flags:# gallery:flags.0?true private:flags.1?true query_id:long results:Vector cache_time:int next_offset:flags.2?string switch_pm:flags.3?InlineBotSwitchPM switch_webview:flags.4?InlineBotWebView = Bool; -messages.sendInlineBotResult#d3fbdccb 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 top_msg_id:flags.9?int random_id:long query_id:long id:string schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.getMessageEditData#fda68d36 peer:InputPeer id:int = messages.MessageEditData; -messages.editMessage#48f71778 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 schedule_date:flags.15?int = 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#9342ca07 flags:# game:flags.1?true peer:InputPeer msg_id:int data:flags.0?bytes password:flags.2?InputCheckPasswordSRP = messages.BotCallbackAnswer; -messages.setBotCallbackAnswer#d58f130a flags:# alert:flags.1?true query_id:long message:flags.0?string url:flags.2?string cache_time:int = Bool; -messages.getPeerDialogs#e470bcfd peers:Vector = messages.PeerDialogs; -messages.saveDraft#b4331e3f flags:# no_webpage:flags.1?true reply_to_msg_id:flags.0?int top_msg_id:flags.2?int peer:InputPeer message:string entities:flags.3?Vector = Bool; -messages.getAllDrafts#6a3f8d65 = Updates; -messages.getFeaturedStickers#64780b14 hash:long = messages.FeaturedStickers; -messages.readFeaturedStickers#5b118126 id:Vector = Bool; -messages.getRecentStickers#9da9403b flags:# attached:flags.0?true hash:long = messages.RecentStickers; -messages.saveRecentSticker#392718f8 flags:# attached:flags.0?true id:InputDocument unsave:Bool = Bool; -messages.clearRecentStickers#8999602d flags:# attached:flags.0?true = Bool; -messages.getArchivedStickers#57f17692 flags:# masks:flags.0?true emojis:flags.1?true offset_id:long limit:int = messages.ArchivedStickers; -messages.getMaskStickers#640f82b8 hash:long = messages.AllStickers; -messages.getAttachedStickers#cc5b67cc media:InputStickeredMedia = Vector; -messages.setGameScore#8ef8ecc0 flags:# edit_message:flags.0?true force:flags.1?true peer:InputPeer id:int user_id:InputUser score:int = Updates; -messages.setInlineGameScore#15ad9f64 flags:# edit_message:flags.0?true force:flags.1?true id:InputBotInlineMessageID user_id:InputUser score:int = Bool; -messages.getGameHighScores#e822649d peer:InputPeer id:int user_id:InputUser = messages.HighScores; -messages.getInlineGameHighScores#f635e1b id:InputBotInlineMessageID user_id:InputUser = messages.HighScores; -messages.getCommonChats#e40ca104 user_id:InputUser max_id:long limit:int = messages.Chats; -messages.getAllChats#875f74be 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#4f1aaa9 hash:long = messages.FavedStickers; -messages.faveSticker#b9ffc55b id:InputDocument unfave:Bool = Bool; -messages.getUnreadMentions#f107e790 flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readMentions#36e5bf4d flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; -messages.getRecentLocations#702a40e0 peer:InputPeer limit:int hash:long = messages.Messages; -messages.sendMultiMedia#b6f11a1c flags:# silent:flags.5?true background:flags.6?true clear_draft:flags.7?true noforwards:flags.14?true update_stickersets_order:flags.15?true peer:InputPeer reply_to_msg_id:flags.0?int top_msg_id:flags.9?int multi_media:Vector schedule_date:flags.10?int send_as:flags.13?InputPeer = Updates; -messages.uploadEncryptedFile#5057c497 peer:InputEncryptedChat file:InputEncryptedFile = EncryptedFile; -messages.searchStickerSets#35705b8a flags:# exclude_featured:flags.0?true q:string hash:long = messages.FoundStickerSets; -messages.getSplitRanges#1cff7e08 = Vector; -messages.markDialogUnread#c286d98f flags:# unread:flags.0?true peer:InputDialogPeer = Bool; -messages.getDialogUnreadMarks#22e24e22 = Vector; -messages.clearAllDrafts#7e58ee9c = Bool; -messages.updatePinnedMessage#d2aaf7ec flags:# silent:flags.0?true unpin:flags.1?true pm_oneside:flags.2?true peer:InputPeer id:int = Updates; -messages.sendVote#10ea6184 peer:InputPeer msg_id:int options:Vector = Updates; -messages.getPollResults#73bb643b peer:InputPeer msg_id:int = Updates; -messages.getOnlines#6e2be050 peer:InputPeer = ChatOnlines; -messages.editChatAbout#def60797 peer:InputPeer about:string = Bool; -messages.editChatDefaultBannedRights#a5866b41 peer:InputPeer banned_rights:ChatBannedRights = Updates; -messages.getEmojiKeywords#35a0e062 lang_code:string = EmojiKeywordsDifference; -messages.getEmojiKeywordsDifference#1508b6af lang_code:string from_version:int = EmojiKeywordsDifference; -messages.getEmojiKeywordsLanguages#4e9963b2 lang_codes:Vector = Vector; -messages.getEmojiURL#d5b10c26 lang_code:string = EmojiURL; -messages.getSearchCounters#ae7cc1 flags:# peer:InputPeer top_msg_id:flags.0?int filters:Vector = Vector; -messages.requestUrlAuth#198fb446 flags:# peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; -messages.acceptUrlAuth#b12c7125 flags:# write_allowed:flags.0?true peer:flags.1?InputPeer msg_id:flags.1?int button_id:flags.1?int url:flags.2?string = UrlAuthResult; -messages.hidePeerSettingsBar#4facb138 peer:InputPeer = Bool; -messages.getScheduledHistory#f516760b peer:InputPeer hash:long = messages.Messages; -messages.getScheduledMessages#bdbb0464 peer:InputPeer id:Vector = messages.Messages; -messages.sendScheduledMessages#bd38850a peer:InputPeer id:Vector = Updates; -messages.deleteScheduledMessages#59ae2b16 peer:InputPeer id:Vector = Updates; -messages.getPollVotes#b86e380e flags:# peer:InputPeer id:int option:flags.0?bytes offset:flags.1?string limit:int = messages.VotesList; -messages.toggleStickerSets#b5052fea flags:# uninstall:flags.0?true archive:flags.1?true unarchive:flags.2?true stickersets:Vector = Bool; -messages.getDialogFilters#f19ed96d = Vector; -messages.getSuggestedDialogFilters#a29cd42c = Vector; -messages.updateDialogFilter#1ad4a04a flags:# id:int filter:flags.0?DialogFilter = Bool; -messages.updateDialogFiltersOrder#c563c1e4 order:Vector = Bool; -messages.getOldFeaturedStickers#7ed094a1 offset:int limit:int hash:long = messages.FeaturedStickers; -messages.getReplies#22ddd30c peer:InputPeer msg_id:int offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages; -messages.getDiscussionMessage#446972fd peer:InputPeer msg_id:int = messages.DiscussionMessage; -messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Bool; -messages.unpinAllMessages#ee22b9a8 flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; -messages.deleteChat#5bd0ee50 chat_id:long = Bool; -messages.deletePhoneCallHistory#f9cbe409 flags:# revoke:flags.0?true = messages.AffectedFoundMessages; -messages.checkHistoryImport#43fe19f3 import_head:string = messages.HistoryImportParsed; -messages.initHistoryImport#34090c3b peer:InputPeer file:InputFile media_count:int = messages.HistoryImport; -messages.uploadImportedMedia#2a862092 peer:InputPeer import_id:long file_name:string media:InputMedia = MessageMedia; -messages.startHistoryImport#b43df344 peer:InputPeer import_id:long = Bool; -messages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites; -messages.getExportedChatInvite#73746f5c peer:InputPeer link:string = messages.ExportedChatInvite; -messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite; -messages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool; -messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool; -messages.getAdminsWithInvites#3920e6ef peer:InputPeer = messages.ChatAdminsWithInvites; -messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters; -messages.setHistoryTTL#b80e5fe4 peer:InputPeer period:int = Updates; -messages.checkHistoryImportPeer#5dc60f03 peer:InputPeer = messages.CheckedHistoryImportPeer; -messages.setChatTheme#e63be13f peer:InputPeer emoticon:string = Updates; -messages.getMessageReadParticipants#31c1c44f peer:InputPeer msg_id:int = Vector; -messages.getSearchResultsCalendar#49f0bde9 peer:InputPeer filter:MessagesFilter offset_id:int offset_date:int = messages.SearchResultsCalendar; -messages.getSearchResultsPositions#6e9583a3 peer:InputPeer filter:MessagesFilter offset_id:int limit:int = messages.SearchResultsPositions; -messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates; -messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates; -messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates; -messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool; -messages.sendReaction#d30d78d4 flags:# big:flags.1?true add_to_recent:flags.2?true peer:InputPeer msg_id:int reaction:flags.0?Vector = Updates; -messages.getMessagesReactions#8bba90e6 peer:InputPeer id:Vector = Updates; -messages.getMessageReactionsList#461b3f48 flags:# peer:InputPeer id:int reaction:flags.0?Reaction offset:flags.1?string limit:int = messages.MessageReactionsList; -messages.setChatAvailableReactions#feb16771 peer:InputPeer available_reactions:ChatReactions = Updates; -messages.getAvailableReactions#18dea0ac hash:int = messages.AvailableReactions; -messages.setDefaultReaction#4f47a016 reaction:Reaction = Bool; -messages.translateText#63183030 flags:# peer:flags.0?InputPeer id:flags.0?Vector text:flags.1?Vector to_lang:string = messages.TranslatedText; -messages.getUnreadReactions#3223495b flags:# peer:InputPeer top_msg_id:flags.0?int offset_id:int add_offset:int limit:int max_id:int min_id:int = messages.Messages; -messages.readReactions#54aa7f8e flags:# peer:InputPeer top_msg_id:flags.0?int = messages.AffectedHistory; -messages.searchSentMedia#107e31a0 q:string filter:MessagesFilter limit:int = messages.Messages; -messages.getAttachMenuBots#16fcc2cb hash:long = AttachMenuBots; -messages.getAttachMenuBot#77216192 bot:InputUser = AttachMenuBotsBot; -messages.toggleBotInAttachMenu#69f59d69 flags:# write_allowed:flags.0?true bot:InputUser enabled:Bool = Bool; -messages.requestWebView#178b480b flags:# from_bot_menu:flags.4?true silent:flags.5?true peer:InputPeer bot:InputUser url:flags.1?string start_param:flags.3?string theme_params:flags.2?DataJSON platform:string reply_to_msg_id:flags.0?int top_msg_id:flags.9?int send_as:flags.13?InputPeer = WebViewResult; -messages.prolongWebView#7ff34309 flags:# silent:flags.5?true peer:InputPeer bot:InputUser query_id:long reply_to_msg_id:flags.0?int top_msg_id:flags.9?int send_as:flags.13?InputPeer = Bool; -messages.requestSimpleWebView#299bec8e flags:# from_switch_webview:flags.1?true bot:InputUser url:string theme_params:flags.0?DataJSON platform:string = SimpleWebViewResult; -messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent; -messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates; -messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio; -messages.rateTranscribedAudio#7f1d072f peer:InputPeer msg_id:int transcription_id:long good:Bool = Bool; -messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector = Vector; -messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers; -messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers; -messages.reportReaction#3f64c076 peer:InputPeer id:int reaction_peer:InputPeer = Bool; -messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions; -messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions; -messages.clearRecentReactions#9dfeefb4 = Bool; -messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector = Updates; -messages.setDefaultHistoryTTL#9eb51445 period:int = Bool; -messages.getDefaultHistoryTTL#658b7188 = DefaultHistoryTTL; -messages.sendBotRequestedPeer#fe38d01b peer:InputPeer msg_id:int button_id:int requested_peer:InputPeer = Updates; -messages.getEmojiGroups#7488ce5b hash:int = messages.EmojiGroups; -messages.getEmojiStatusGroups#2ecd56cd hash:int = messages.EmojiGroups; -messages.getEmojiProfilePhotoGroups#21a548f3 hash:int = messages.EmojiGroups; -messages.searchCustomEmoji#2c11c0d7 emoticon:string hash:long = EmojiList; -messages.togglePeerTranslations#e47cb579 flags:# disabled:flags.0?true peer:InputPeer = Bool; -messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp; -messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult; -messages.setChatWallPaper#8ffacae1 flags:# peer:InputPeer wallpaper:flags.0?InputWallPaper settings:flags.2?WallPaperSettings id:flags.1?int = Updates; - -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#9e82039 flags:# fallback:flags.0?true bot:flags.1?InputUser id:InputPhoto = photos.Photo; -photos.uploadProfilePhoto#388a3b5 flags:# fallback:flags.3?true bot:flags.5?InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.4?VideoSize = photos.Photo; -photos.deletePhotos#87cf7f2f id:Vector = Vector; -photos.getUserPhotos#91cd32a8 user_id:InputUser offset:int max_id:long limit:int = photos.Photos; -photos.uploadContactProfilePhoto#e14c4a71 flags:# suggest:flags.3?true save:flags.4?true user_id:InputUser file:flags.0?InputFile video:flags.1?InputFile video_start_ts:flags.2?double video_emoji_markup:flags.5?VideoSize = photos.Photo; - -upload.saveFilePart#b304a621 file_id:long file_part:int bytes:bytes = Bool; -upload.getFile#be5335be flags:# precise:flags.0?true cdn_supported:flags.1?true location:InputFileLocation offset:long limit:int = upload.File; -upload.saveBigFilePart#de7b673d file_id:long file_part:int file_total_parts:int bytes:bytes = Bool; -upload.getWebFile#24e6818d location:InputWebFileLocation offset:int limit:int = upload.WebFile; -upload.getCdnFile#395f69da file_token:bytes offset:long limit:int = upload.CdnFile; -upload.reuploadCdnFile#9b2754a8 file_token:bytes request_token:bytes = Vector; -upload.getCdnFileHashes#91dc3f31 file_token:bytes offset:long = Vector; -upload.getFileHashes#9156982a location:InputFileLocation offset:long = Vector; - -help.getConfig#c4f9186b = Config; -help.getNearestDc#1fb33026 = NearestDc; -help.getAppUpdate#522d5a7d source:string = help.AppUpdate; -help.getInviteText#4d392343 = help.InviteText; -help.getSupport#9cdf08cd = help.Support; -help.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.getTermsOfServiceUpdate#2ca51fd1 = help.TermsOfServiceUpdate; -help.acceptTermsOfService#ee72f79a id:DataJSON = Bool; -help.getDeepLinkInfo#3fedc75f path:string = help.DeepLinkInfo; -help.getAppConfig#61e3f854 hash:int = help.AppConfig; -help.saveAppLog#6f02f748 events:Vector = Bool; -help.getPassportConfig#c661ad08 hash:int = help.PassportConfig; -help.getSupportName#d360e72c = help.SupportName; -help.getUserInfo#38a08d3 user_id:InputUser = help.UserInfo; -help.editUserInfo#66b91b70 user_id:InputUser message:string entities:Vector = help.UserInfo; -help.getPromoData#c0977421 = help.PromoData; -help.hidePromoData#1e251c95 peer:InputPeer = Bool; -help.dismissSuggestion#f50dbaa1 peer:InputPeer suggestion:string = Bool; -help.getCountriesList#735787a8 lang_code:string hash:int = help.CountriesList; -help.getPremiumPromo#b81b93d4 = help.PremiumPromo; - -channels.readHistory#cc104937 channel:InputChannel max_id:int = Bool; -channels.deleteMessages#84c1fd4e channel:InputChannel id:Vector = messages.AffectedMessages; -channels.reportSpam#f44a8315 channel:InputChannel participant:InputPeer id:Vector = Bool; -channels.getMessages#ad8c9a23 channel:InputChannel id:Vector = messages.Messages; -channels.getParticipants#77ced9d0 channel:InputChannel filter:ChannelParticipantsFilter offset:int limit:int hash:long = channels.ChannelParticipants; -channels.getParticipant#a0ab6cc6 channel:InputChannel participant:InputPeer = channels.ChannelParticipant; -channels.getChannels#a7f6bbb id:Vector = messages.Chats; -channels.getFullChannel#8736a09 channel:InputChannel = messages.ChatFull; -channels.createChannel#91006707 flags:# broadcast:flags.0?true megagroup:flags.1?true for_import:flags.3?true forum:flags.5?true title:string about:string geo_point:flags.2?InputGeoPoint address:flags.2?string ttl_period:flags.4?int = Updates; -channels.editAdmin#d33c8902 channel:InputChannel user_id:InputUser admin_rights:ChatAdminRights rank:string = Updates; -channels.editTitle#566decd0 channel:InputChannel title:string = Updates; -channels.editPhoto#f12e57c9 channel:InputChannel photo:InputChatPhoto = Updates; -channels.checkUsername#10e6bd2c channel:InputChannel username:string = Bool; -channels.updateUsername#3514b3de channel:InputChannel username:string = Bool; -channels.joinChannel#24b524c5 channel:InputChannel = Updates; -channels.leaveChannel#f836aa95 channel:InputChannel = Updates; -channels.inviteToChannel#199f3a6c channel:InputChannel users:Vector = Updates; -channels.deleteChannel#c0111fe3 channel:InputChannel = Updates; -channels.exportMessageLink#e63fadeb flags:# grouped:flags.0?true thread:flags.1?true channel:InputChannel id:int = ExportedMessageLink; -channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; -channels.getAdminedPublicChannels#f8b036af flags:# by_location:flags.0?true check_limit:flags.1?true = messages.Chats; -channels.editBanned#96e6cd81 channel:InputChannel participant:InputPeer banned_rights:ChatBannedRights = Updates; -channels.getAdminLog#33ddf480 flags:# channel:InputChannel q:string events_filter:flags.0?ChannelAdminLogEventsFilter admins:flags.1?Vector max_id:long min_id:long limit:int = channels.AdminLogResults; -channels.setStickers#ea8ca4f9 channel:InputChannel stickerset:InputStickerSet = Bool; -channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector = Bool; -channels.deleteHistory#9baa9647 flags:# for_everyone:flags.0?true channel:InputChannel max_id:int = Updates; -channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates; -channels.getLeftChannels#8341ecc0 offset:int = messages.Chats; -channels.getGroupsForDiscussion#f5dad378 = messages.Chats; -channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool; -channels.editCreator#8f38cd1f channel:InputChannel user_id:InputUser password:InputCheckPasswordSRP = Updates; -channels.editLocation#58e63f6d channel:InputChannel geo_point:InputGeoPoint address:string = Bool; -channels.toggleSlowMode#edd49ef0 channel:InputChannel seconds:int = Updates; -channels.getInactiveChannels#11e831ee = messages.InactiveChats; -channels.convertToGigagroup#b290c69 channel:InputChannel = Updates; -channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool; -channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages; -channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers; -channels.deleteParticipantHistory#367544db channel:InputChannel participant:InputPeer = messages.AffectedHistory; -channels.toggleJoinToSend#e4cb9580 channel:InputChannel enabled:Bool = Updates; -channels.toggleJoinRequest#4c2985b6 channel:InputChannel enabled:Bool = Updates; -channels.reorderUsernames#b45ced1d channel:InputChannel order:Vector = Bool; -channels.toggleUsername#50f24105 channel:InputChannel username:string active:Bool = Bool; -channels.deactivateAllUsernames#a245dd3 channel:InputChannel = Bool; -channels.toggleForum#a4298b29 channel:InputChannel enabled:Bool = Updates; -channels.createForumTopic#f40c0224 flags:# channel:InputChannel title:string icon_color:flags.0?int icon_emoji_id:flags.3?long random_id:long send_as:flags.2?InputPeer = Updates; -channels.getForumTopics#de560d1 flags:# channel:InputChannel q:flags.0?string offset_date:int offset_id:int offset_topic:int limit:int = messages.ForumTopics; -channels.getForumTopicsByID#b0831eb9 channel:InputChannel topics:Vector = messages.ForumTopics; -channels.editForumTopic#f4dfa185 flags:# channel:InputChannel topic_id:int title:flags.0?string icon_emoji_id:flags.1?long closed:flags.2?Bool hidden:flags.3?Bool = Updates; -channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates; -channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; -channels.reorderPinnedForumTopics#2950a18f flags:# force:flags.0?true channel:InputChannel order:Vector = Updates; -channels.toggleAntiSpam#68f3e4eb channel:InputChannel enabled:Bool = Updates; -channels.reportAntiSpamFalsePositive#a850a693 channel:InputChannel msg_id:int = Bool; -channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; - -bots.sendCustomRequest#aa2769ed custom_method:string params:DataJSON = DataJSON; -bots.answerWebhookJSONQuery#e6213f4d query_id:long data:DataJSON = Bool; -bots.setBotCommands#517165a scope:BotCommandScope lang_code:string commands:Vector = Bool; -bots.resetBotCommands#3d8de0f9 scope:BotCommandScope lang_code:string = Bool; -bots.getBotCommands#e34c0dd6 scope:BotCommandScope lang_code:string = Vector; -bots.setBotMenuButton#4504d54f user_id:InputUser button:BotMenuButton = Bool; -bots.getBotMenuButton#9c60eb28 user_id:InputUser = BotMenuButton; -bots.setBotBroadcastDefaultAdminRights#788464e1 admin_rights:ChatAdminRights = Bool; -bots.setBotGroupDefaultAdminRights#925ec9ea admin_rights:ChatAdminRights = Bool; -bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool; -bots.getBotInfo#dcd914fd flags:# bot:flags.0?InputUser lang_code:string = bots.BotInfo; -bots.reorderUsernames#9709b1c2 bot:InputUser order:Vector = Bool; -bots.toggleUsername#53ca973 bot:InputUser username:string active:Bool = Bool; - -payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; -payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; -payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; -payments.sendPaymentForm#2d03522f flags:# form_id:long invoice:InputInvoice requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult; -payments.getSavedInfo#227d824b = payments.SavedInfo; -payments.clearSavedInfo#d83d70c1 flags:# credentials:flags.0?true info:flags.1?true = Bool; -payments.getBankCardData#2e79d779 number:string = payments.BankCardData; -payments.exportInvoice#f91b065 invoice_media:InputMedia = payments.ExportedInvoice; -payments.assignAppStoreTransaction#80ed747d receipt:bytes purpose:InputStorePaymentPurpose = Updates; -payments.assignPlayMarketTransaction#dffd50d3 receipt:DataJSON purpose:InputStorePaymentPurpose = Updates; -payments.canPurchasePremium#9fc19eb6 purpose:InputStorePaymentPurpose = Bool; - -stickers.createStickerSet#9021ab67 flags:# masks:flags.0?true animated:flags.1?true videos:flags.4?true emojis:flags.5?true text_color:flags.6?true user_id:InputUser title:string short_name:string thumb:flags.2?InputDocument stickers:Vector software:flags.3?string = messages.StickerSet; -stickers.removeStickerFromSet#f7760f51 sticker:InputDocument = messages.StickerSet; -stickers.changeStickerPosition#ffb6d4ca sticker:InputDocument position:int = messages.StickerSet; -stickers.addStickerToSet#8653febe stickerset:InputStickerSet sticker:InputStickerSetItem = messages.StickerSet; -stickers.setStickerSetThumb#a76a5392 flags:# stickerset:InputStickerSet thumb:flags.0?InputDocument thumb_document_id:flags.1?long = messages.StickerSet; -stickers.checkShortName#284b3639 short_name:string = Bool; -stickers.suggestShortName#4dafc503 title:string = stickers.SuggestedShortName; -stickers.changeSticker#f5537ebc flags:# sticker:InputDocument emoji:flags.0?string mask_coords:flags.1?MaskCoords keywords:flags.2?string = messages.StickerSet; -stickers.renameStickerSet#124b1c00 stickerset:InputStickerSet title:string = messages.StickerSet; -stickers.deleteStickerSet#87704394 stickerset:InputStickerSet = Bool; - -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; -phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool; -phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates; -phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates; -phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates; -phone.inviteToGroupCall#7b393160 call:InputGroupCall users:Vector = Updates; -phone.discardGroupCall#7a777135 call:InputGroupCall = Updates; -phone.toggleGroupCallSettings#74bbb43d flags:# reset_invite_hash:flags.1?true call:InputGroupCall join_muted:flags.0?Bool = Updates; -phone.getGroupCall#41845db call:InputGroupCall limit:int = phone.GroupCall; -phone.getGroupParticipants#c558d8ab call:InputGroupCall ids:Vector sources:Vector offset:string limit:int = phone.GroupParticipants; -phone.checkGroupCall#b59cf977 call:InputGroupCall sources:Vector = Vector; -phone.toggleGroupCallRecord#f128c708 flags:# start:flags.0?true video:flags.2?true call:InputGroupCall title:flags.1?string video_portrait:flags.2?Bool = Updates; -phone.editGroupCallParticipant#a5273abf flags:# call:InputGroupCall participant:InputPeer muted:flags.0?Bool volume:flags.1?int raise_hand:flags.2?Bool video_stopped:flags.3?Bool video_paused:flags.4?Bool presentation_paused:flags.5?Bool = Updates; -phone.editGroupCallTitle#1ca6ac0a call:InputGroupCall title:string = Updates; -phone.getGroupCallJoinAs#ef7c213a peer:InputPeer = phone.JoinAsPeers; -phone.exportGroupCallInvite#e6aa647f flags:# can_self_unmute:flags.0?true call:InputGroupCall = phone.ExportedGroupCallInvite; -phone.toggleGroupCallStartSubscription#219c34e6 call:InputGroupCall subscribed:Bool = Updates; -phone.startScheduledGroupCall#5680e342 call:InputGroupCall = Updates; -phone.saveDefaultGroupCallJoinAs#575e1f8c peer:InputPeer join_as:InputPeer = Bool; -phone.joinGroupCallPresentation#cbea6bc4 call:InputGroupCall params:DataJSON = Updates; -phone.leaveGroupCallPresentation#1c50d144 call:InputGroupCall = Updates; -phone.getGroupCallStreamChannels#1ab21940 call:InputGroupCall = phone.GroupCallStreamChannels; -phone.getGroupCallStreamRtmpUrl#deb3abbf peer:InputPeer revoke:Bool = phone.GroupCallStreamRtmpUrl; -phone.saveCallLog#41248786 peer:InputPhoneCall file:InputFile = Bool; - -langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference; -langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector = Vector; -langpack.getDifference#cd984aa5 lang_pack:string lang_code:string from_version:int = LangPackDifference; -langpack.getLanguages#42c6978f lang_pack:string = Vector; -langpack.getLanguage#6a596502 lang_pack:string lang_code:string = LangPackLanguage; - -folders.editPeerFolders#6847d0ab folder_peers:Vector = Updates; - -stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel = stats.BroadcastStats; -stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph; -stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats; -stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages; -stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats; - -chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector = chatlists.ExportedChatlistInvite; -chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool; -chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector = ExportedChatlistInvite; -chatlists.getExportedInvites#ce03da83 chatlist:InputChatlist = chatlists.ExportedInvites; -chatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite; -chatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector = Updates; -chatlists.getChatlistUpdates#89419521 chatlist:InputChatlist = chatlists.ChatlistUpdates; -chatlists.joinChatlistUpdates#e089f8f5 chatlist:InputChatlist peers:Vector = Updates; -chatlists.hideChatlistUpdates#66e486fb chatlist:InputChatlist = Bool; -chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector; -chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector = Updates; - -// LAYER 158 diff --git a/telethon_generator/data/errors.csv b/telethon_generator/data/errors.csv deleted file mode 100644 index 374362bc..00000000 --- a/telethon_generator/data/errors.csv +++ /dev/null @@ -1,523 +0,0 @@ -name,codes,description -2FA_CONFIRM_WAIT_X,420,The account is 2FA protected so it will be deleted in a week. Otherwise it can be reset in {seconds} -ABOUT_TOO_LONG,400,The provided bio is too long -ACCESS_TOKEN_EXPIRED,400,Bot token expired -ACCESS_TOKEN_INVALID,400,The provided token is not valid -ACTIVE_USER_REQUIRED,401,The method is only available to already activated users -ADMINS_TOO_MUCH,400,Too many admins -ADMIN_ID_INVALID,400,The specified admin ID is invalid -ADMIN_RANK_EMOJI_NOT_ALLOWED,400,Emoji are not allowed in admin titles or ranks -ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly larger than 16 characters) -ALBUM_PHOTOS_TOO_MANY,400,Too many photos were included in the album -API_ID_INVALID,400,The api_id/api_hash combination is invalid -API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now" -ARTICLE_TITLE_EMPTY,400,The title of the article is empty -AUDIO_CONTENT_URL_EMPTY,400,The remote URL specified in the content field is empty -AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty -AUTH_BYTES_INVALID,400,The provided authorization is invalid -AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions" -AUTH_KEY_INVALID,401,The key is invalid -AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization key, not bound to permanent" -AUTH_KEY_UNREGISTERED,401,The key is not registered in the system -AUTH_RESTART,500,Restart the authorization process -AUTH_TOKEN_ALREADY_ACCEPTED,400,The authorization token was already used -AUTH_TOKEN_EXCEPTION,400,An error occurred while importing the auth token -AUTH_TOKEN_EXPIRED,400,The provided authorization token has expired and the updated QR-code must be re-scanned -AUTH_TOKEN_INVALID,400,An invalid authorization token was provided -AUTH_TOKEN_INVALID2,400,An invalid authorization token was provided -AUTH_TOKEN_INVALIDX,400,The specified auth token is invalid -AUTOARCHIVE_NOT_AVAILABLE,400,You cannot use this feature yet -BANK_CARD_NUMBER_INVALID,400,Incorrect credit card number -BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default" -BASE_PORT_LOC_INVALID,400,Base port location invalid -BOTS_TOO_MUCH,400,There are too many bots in this chat/channel -BOT_CHANNELS_NA,400,Bots can't edit admin privileges -BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used" -BOT_COMMAND_INVALID,400,The specified command is invalid -BOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather -BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat -BOT_GROUPS_BLOCKED,400,This bot can't be added to groups -BOT_INLINE_DISABLED,400,This bot can't be used in inline mode -BOT_INVALID,400,This is not a valid bot -BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot -BOT_MISSING,400,This method can only be run by a bot -BOT_ONESIDE_NOT_AVAIL,400,Bots can't pin messages in PM just for themselves -BOT_PAYMENTS_DISABLED,400,This method can only be run by a bot -BOT_POLLS_DISABLED,400,You cannot create polls under a bot account -BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time -BOT_SCORE_NOT_MODIFIED,400,The score wasn't modified -BROADCAST_CALLS_DISABLED,400, -BROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels -BROADCAST_ID_INVALID,400,The channel is invalid -BROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public -BROADCAST_REQUIRED,400,The request can only be used with a broadcast channel -BUTTON_DATA_INVALID,400,The provided button data is invalid -BUTTON_TEXT_INVALID,400,The specified button text is invalid -BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid -BUTTON_URL_INVALID,400,Button URL invalid -BUTTON_USER_PRIVACY_RESTRICTED,400,The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button -CALL_ALREADY_ACCEPTED,400,The call was already accepted -CALL_ALREADY_DECLINED,400,The call was already declined -CALL_OCCUPY_FAILED,500,The call failed because the user is already making another call -CALL_PEER_INVALID,400,The provided call peer object is invalid -CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid -CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods -CDN_UPLOAD_TIMEOUT,500,A server-side timeout occurred while reuploading the file to the CDN DC -CHANNELS_ADMIN_LOCATED_TOO_MUCH,400,The user has reached the limit of public geogroups -CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel" -CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups -CHANNEL_BANNED,400,The channel is banned -CHANNEL_ID_INVALID,400,The specified supergroup ID is invalid -CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited" -CHANNEL_PARICIPANT_MISSING,400,The current user is not in the channel -CHANNEL_PRIVATE,400 406,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it -CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available -CHANNEL_TOO_BIG,400, -CHANNEL_TOO_LARGE,400 406,Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change) -CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed -CHAT_ABOUT_TOO_LONG,400,Chat about too long -CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this -CHAT_ADMIN_REQUIRED,400 403,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group" -CHAT_DISCUSSION_UNALLOWED,400, -CHAT_FORBIDDEN,403,You cannot write in this chat -CHAT_FORWARDS_RESTRICTED,400 406,You can't forward messages from a protected chat -CHAT_GUEST_SEND_FORBIDDEN,403,"You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info" -CHAT_ID_EMPTY,400,The provided chat ID is empty -CHAT_ID_GENERATE_FAILED,500,Failure while generating the chat ID -CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead" -CHAT_INVALID,400,The chat is invalid for this request -CHAT_INVITE_PERMANENT,400,You can't set an expiration date on permanent invite links -CHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request -CHAT_NOT_MODIFIED,400,"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)" -CHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request -CHAT_REVOKE_DATE_UNSUPPORTED,400,`min_date` and `max_date` are not available for using with non-user peers -CHAT_SEND_GAME_FORBIDDEN,403,You can't send a game to this chat -CHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat -CHAT_SEND_INLINE_FORBIDDEN,400 403,You cannot send inline results in this chat -CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat -CHAT_SEND_POLL_FORBIDDEN,403,You can't send polls in this chat -CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat -CHAT_TITLE_EMPTY,400,No chat title provided -CHAT_TOO_BIG,400,"This method is not available for groups with more than `chat_read_mark_size_threshold` members, [see client configuration](https://core.telegram.org/api/config#client-configuration)" -CHAT_WRITE_FORBIDDEN,403,You can't write in this chat -CHP_CALL_FAIL,500,The statistics cannot be retrieved at this time -CODE_EMPTY,400,The provided code is empty -CODE_HASH_INVALID,400,Code hash invalid -CODE_INVALID,400,Code invalid (i.e. from email) -CONNECTION_API_ID_INVALID,400,The provided API id is invalid -CONNECTION_APP_VERSION_EMPTY,400,App version is empty -CONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty -CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty" -CONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest -CONNECTION_NOT_INITED,400,Connection not initialized -CONNECTION_SYSTEM_EMPTY,400,Connection system empty -CONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection -CONTACT_ADD_MISSING,400,Contact to add is missing -CONTACT_ID_INVALID,400,The provided contact ID is invalid -CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty -CONTACT_REQ_MISSING,400,Missing contact request -CREATE_CALL_FAILED,400,An error occurred while creating the call -CURRENCY_TOTAL_AMOUNT_INVALID,400,The total amount of all prices is invalid -DATA_INVALID,400,Encrypted data invalid -DATA_JSON_INVALID,400,The provided JSON data is invalid -DATA_TOO_LONG,400,Data too long -DATE_EMPTY,400,Date empty -DC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to -DH_G_A_INVALID,400,g_a invalid -DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode -EDIT_BOT_INVITE_FORBIDDEN,403,Normal users can't edit invites that were created by bots -EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it -EMAIL_INVALID,400,The given email is invalid -EMAIL_UNCONFIRMED,400,Email unconfirmed -EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}" -EMAIL_VERIFY_EXPIRED,400,The verification email has expired -EMOJI_INVALID,400,The specified theme emoji is valid -EMOJI_NOT_MODIFIED,400,The theme wasn't changed -EMOTICON_EMPTY,400,The emoticon field cannot be empty -EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon -EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing -ENCRYPTED_MESSAGE_INVALID,400,Encrypted message invalid -ENCRYPTION_ALREADY_ACCEPTED,400,Secret chat already accepted -ENCRYPTION_ALREADY_DECLINED,400,The secret chat was already declined -ENCRYPTION_DECLINED,400,The secret chat was declined -ENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid -ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420 -ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs) -ENTITY_BOUNDS_INVALID,400,Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string) -ENTITY_MENTION_USER_INVALID,400,You can't use this entity -ERROR_TEXT_EMPTY,400,The provided error message is empty -EXPIRE_DATE_INVALID,400,The specified expiration date is invalid -EXPIRE_FORBIDDEN,400, -EXPORT_CARD_INVALID,400,Provided card is invalid -EXTERNAL_URL_INVALID,400,External URL invalid -FIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing -FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid -FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again -FILE_CONTENT_TYPE_INVALID,400,File content-type is invalid -FILE_EMTPY,400,An empty file was provided -FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)" -FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc} -FILE_PARTS_INVALID,400,The number of file parts is invalid -FILE_PART_0_MISSING,400,File part 0 missing -FILE_PART_EMPTY,400,The provided file part is empty -FILE_PART_INVALID,400,The file part number is invalid -FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid -FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload -FILE_PART_SIZE_INVALID,400,The provided file part size is invalid -FILE_PART_TOO_BIG,400,The uploaded file part is too big -FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage -FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty -FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent -FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message -FILE_TITLE_EMPTY,400,An empty file title was specified -FILTER_ID_INVALID,400,The specified filter ID is invalid -FILTER_INCLUDE_EMPTY,400,The include_peers vector of the filter is empty -FILTER_NOT_SUPPORTED,400,The specified filter cannot be used in this context -FILTER_TITLE_EMPTY,400,The title field of the filter is empty -FIRSTNAME_INVALID,400,The first name is invalid -FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers -FLOOD_WAIT_X,420,A wait of {seconds} seconds is required -FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty -FOLDER_ID_INVALID,400,The folder you tried to use was not valid -FRESH_CHANGE_ADMINS_FORBIDDEN,400 406,Recently logged-in users cannot add or change admins -FRESH_CHANGE_PHONE_FORBIDDEN,406,Recently logged-in users cannot use this request -FRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet -FROM_MESSAGE_BOT_DISABLED,400,Bots can't use fromMessage min constructors -FROM_PEER_INVALID,400,The given from_user peer cannot be used for the parameter -GAME_BOT_INVALID,400,You cannot send that game with the current bot -GEO_POINT_INVALID,400,Invalid geoposition provided -GIF_CONTENT_TYPE_INVALID,400,GIF content-type invalid -GIF_ID_INVALID,400,The provided GIF ID is invalid -GRAPH_EXPIRED_RELOAD,400,"This graph has expired, please obtain a new graph token" -GRAPH_INVALID_RELOAD,400,"Invalid graph token provided, please reload the stats and provide the updated token" -GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated" -GROUPCALL_ADD_PARTICIPANTS_FAILED,500, -GROUPCALL_ALREADY_DISCARDED,400,The group call was already discarded -GROUPCALL_ALREADY_STARTED,403,"The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)" -GROUPCALL_FORBIDDEN,403,The group call has already ended -GROUPCALL_INVALID,400,The specified group call is invalid -GROUPCALL_JOIN_MISSING,400,You haven't joined this group call -GROUPCALL_NOT_MODIFIED,400,Group call settings weren't modified -GROUPCALL_SSRC_DUPLICATE_MUCH,400,The app needs to retry joining the group call with a new SSRC value -GROUPED_MEDIA_INVALID,400,Invalid grouped media -GROUP_CALL_INVALID,400,Group call invalid -HASH_INVALID,400,The provided hash is invalid -HIDE_REQUESTER_MISSING,400,The join request was missing or was already handled -HISTORY_GET_FAILED,500,Fetching of history failed -IMAGE_PROCESS_FAILED,400,Failure while processing image -IMPORT_FILE_INVALID,400,The file is too large to be imported -IMPORT_FORMAT_UNRECOGNIZED,400,Unknown import format -IMPORT_ID_INVALID,400,The specified import ID is invalid -INLINE_BOT_REQUIRED,403,The action must be performed through an inline bot callback -INLINE_RESULT_EXPIRED,400,The inline query expired -INPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid -INPUT_FETCH_ERROR,400,An error occurred while deserializing TL parameters -INPUT_FETCH_FAIL,400,Failed deserializing TL payload -INPUT_FILTER_INVALID,400,The search query filter is invalid -INPUT_LAYER_INVALID,400,The provided layer is invalid -INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed -INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message) -INPUT_TEXT_EMPTY,400,The specified text is empty -INPUT_USER_DEACTIVATED,400,The specified user was deleted -INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc} -INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc} -INVITE_FORBIDDEN_WITH_JOINAS,400,"If the user has anonymously joined a group call as a channel, they can't invite other users to the group call because that would cause deanonymization, because the invite would be sent using the original user ID, not the anonymized channel ID" -INVITE_HASH_EMPTY,400,The invite hash is empty -INVITE_HASH_EXPIRED,400 406,The chat the user tried to join has expired and is not valid anymore -INVITE_HASH_INVALID,400,The invite hash is invalid -INVITE_REQUEST_SENT,400,You have successfully requested to join this chat or channel -INVITE_REVOKED_MISSING,400,The specified invite link was already revoked or is invalid -INVOICE_PAYLOAD_INVALID,400,The specified invoice payload is invalid -JOIN_AS_PEER_INVALID,400,The specified peer cannot be used to join a group call -LANG_CODE_INVALID,400,The specified language code is invalid -LANG_CODE_NOT_SUPPORTED,400,The specified language code is not supported -LANG_PACK_INVALID,400,The provided language pack is invalid -LASTNAME_INVALID,400,The last name is invalid -LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files -LINK_NOT_MODIFIED,400,The channel is already linked to this group -LOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files -MAX_DATE_INVALID,400,The specified maximum date is invalid -MAX_ID_INVALID,400,The provided max ID is invalid -MAX_QTS_INVALID,400,The provided QTS were invalid -MD5_CHECKSUM_INVALID,400,The MD5 check-sums do not match -MEDIA_CAPTION_TOO_LONG,400,The caption is too long -MEDIA_EMPTY,400,The provided media object is invalid or the current account may not be able to send it (such as games as users) -MEDIA_GROUPED_INVALID,400,You tried to send media of different types in an album -MEDIA_INVALID,400,Media invalid -MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes) -MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes) -MEDIA_TTL_INVALID,400, -MEGAGROUP_ID_INVALID,400,The group is invalid -MEGAGROUP_PREHISTORY_HIDDEN,400,You can't set this discussion group because it's history is hidden -MEGAGROUP_REQUIRED,400,The request can only be used with a megagroup channel -MEMBER_NO_LOCATION,500,An internal failure occurred while fetching user info (couldn't find location) -MEMBER_OCCUPY_PRIMARY_LOC_FAILED,500,Occupation of primary member location failed -MESSAGE_AUTHOR_REQUIRED,403,Message author required -MESSAGE_DELETE_FORBIDDEN,403,"You can't delete one of the messages you tried to delete, most likely because it is a service message." -MESSAGE_EDIT_TIME_EXPIRED,400,"You can't edit this message anymore, too much time has passed since its creation." -MESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent -MESSAGE_IDS_EMPTY,400,No message ids were provided -MESSAGE_ID_INVALID,400,The specified message ID is invalid or you can't do that operation on such message -MESSAGE_NOT_MODIFIED,400,Content of the message was not modified -MESSAGE_POLL_CLOSED,400,The poll was closed and can no longer be voted on -MESSAGE_TOO_LONG,400,Message was too long -METHOD_INVALID,400,The API method is invalid and cannot be used -MIN_DATE_INVALID,400,The specified minimum date is invalid -MSGID_DECREASE_RETRY,500,The request should be retried with a lower message ID -MSG_ID_INVALID,400,The message ID used in the peer was invalid -MSG_TOO_OLD,400,"[`chat_read_mark_expire_period` seconds](https://core.telegram.org/api/config#chat-read-mark-expire-period) have passed since the message was sent, read receipts were deleted" -MSG_WAIT_FAILED,400,A waiting call returned an error -MT_SEND_QUEUE_TOO_LONG,500, -MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album -NEED_CHAT_INVALID,500,The provided chat is invalid -NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size) -NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc} -NEW_SALT_INVALID,400,The new salt is invalid -NEW_SETTINGS_EMPTY,400,"No password is set on the current account, and no new password was specified in `new_settings`" -NEW_SETTINGS_INVALID,400,The new settings are invalid -NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long -NOT_ALLOWED,403, -OFFSET_INVALID,400,"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files" -OFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid -OPTIONS_TOO_MUCH,400,You defined too many options for the poll -OPTION_INVALID,400,The option specified is invalid and does not exist in the target poll -PACK_SHORT_NAME_INVALID,400,"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in ""_by_""." -PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists -PACK_TITLE_INVALID,400,The stickerpack title is invalid -PARTICIPANTS_TOO_FEW,400,Not enough participants -PARTICIPANT_CALL_FAILED,500,Failure while making call -PARTICIPANT_ID_INVALID,400,The specified participant ID is invalid -PARTICIPANT_JOIN_MISSING,400 403,"Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)" -PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls -PASSWORD_EMPTY,400,The provided password is empty -PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid -PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used -PASSWORD_RECOVERY_EXPIRED,400,The recovery code has expired -PASSWORD_RECOVERY_NA,400,"No email was set, can't recover password via email" -PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used -PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method -PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid -PEER_FLOOD,400,Too many requests -PEER_HISTORY_EMPTY,400, -PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)" -PEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported -PERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty -PERSISTENT_TIMESTAMP_INVALID,400,Persistent timestamp invalid -PERSISTENT_TIMESTAMP_OUTDATED,500,Persistent timestamp outdated -PHONE_CODE_EMPTY,400,The phone code is missing -PHONE_CODE_EXPIRED,400,The confirmation code has expired -PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing -PHONE_CODE_INVALID,400,The phone code entered was invalid -PHONE_HASH_EXPIRED,400,An invalid or expired `phone_code_hash` was provided -PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc} -PHONE_NOT_OCCUPIED,400,No user is associated to the specified phone number -PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app -PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam -PHONE_NUMBER_FLOOD,400,You asked for the code too many times. -PHONE_NUMBER_INVALID,400 406,The phone number is invalid -PHONE_NUMBER_OCCUPIED,400,The phone number is already in use -PHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used -PHONE_PASSWORD_FLOOD,406,You have tried logging in too many times -PHONE_PASSWORD_PROTECTED,400,This phone is password protected -PHOTO_CONTENT_TYPE_INVALID,400,Photo mime-type invalid -PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error -PHOTO_CROP_FILE_MISSING,400,Photo crop file missing -PHOTO_CROP_SIZE_SMALL,400,Photo is too small -PHOTO_EXT_INVALID,400,The extension of the photo is invalid -PHOTO_FILE_MISSING,400,Profile photo file missing -PHOTO_ID_INVALID,400,Photo id is invalid -PHOTO_INVALID,400,Photo invalid -PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images) -PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally -PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error -PINNED_DIALOGS_TOO_MUCH,400,Too many pinned dialogs -PIN_RESTRICTED,400,You can't pin messages in private chats with other people -POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many -POLL_ANSWER_INVALID,400,One of the poll answers is not acceptable -POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll -POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long) -POLL_QUESTION_INVALID,400,The poll question was either empty or too long -POLL_UNSUPPORTED,400,This layer does not support polls in the issued method -POLL_VOTE_REQUIRED,403,Cast a vote in the poll before calling this method -POSTPONED_TIMEOUT,500,The postponed call has timed out -PREMIUM_ACCOUNT_REQUIRED,403,A premium account is required to execute this action -PREMIUM_CURRENTLY_UNAVAILABLE,406, -PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes" -PRIVACY_KEY_INVALID,400,The privacy key is invalid -PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request -PRIVACY_VALUE_INVALID,400,The privacy value is invalid -PTS_CHANGE_EMPTY,500,No PTS change -PUBLIC_CHANNEL_MISSING,403,You can only export group call invite links for public chats or channels -PUBLIC_KEY_REQUIRED,400,A public key is required -QUERY_ID_EMPTY,400,The query ID is empty -QUERY_ID_INVALID,400,The query ID is invalid -QUERY_TOO_SHORT,400,The query string is too short -QUIZ_ANSWER_MISSING,400,You can forward a quiz while hiding the original author only after choosing an option in the quiz -QUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer -QUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer -QUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer -QUIZ_MULTIPLE_INVALID,400,A poll cannot be both multiple choice and quiz -RANDOM_ID_DUPLICATE,500,You provided a random ID that was already used -RANDOM_ID_EMPTY,400,Random ID empty -RANDOM_ID_INVALID,400,A provided random ID is invalid -RANDOM_LENGTH_INVALID,400,Random length invalid -RANGES_INVALID,400,Invalid range provided -REACTIONS_TOO_MANY,400,"The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)" -REACTION_EMPTY,400,No reaction provided -REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed) -REFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server -REG_ID_GENERATE_FAILED,500,Failure while generating registration ID -REPLY_MARKUP_BUY_EMPTY,400,Reply markup for buy button empty -REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty -REPLY_MARKUP_INVALID,400,The provided reply markup is invalid -REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much -RESET_REQUEST_MISSING,400,No password reset is in progress -RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit" -RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs -RESULT_ID_EMPTY,400,Result ID empty -RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot -RESULT_TYPE_INVALID,400,Result type invalid -REVOTE_NOT_ALLOWED,400,You cannot change your vote -RIGHTS_NOT_MODIFIED,400,"The new admin rights are equal to the old rights, no change was made" -RIGHT_FORBIDDEN,403,Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa) -RPC_CALL_FAIL,500,"Telegram is having internal issues, please try again later." -RPC_MCGET_FAIL,500,"Telegram is having internal issues, please try again later." -RSA_DECRYPT_FAILED,400,Internal RSA decryption failed -SCHEDULE_BOT_NOT_ALLOWED,400,Bots are not allowed to schedule messages -SCHEDULE_DATE_INVALID,400,Invalid schedule date provided -SCHEDULE_DATE_TOO_LATE,400,The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours) -SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information -SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat) -SCORE_INVALID,400,The specified game score is invalid -SEARCH_QUERY_EMPTY,400,The search query is empty -SEARCH_WITH_LINK_NOT_SUPPORTED,400,You cannot provide a search query and an invite link at the same time -SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)" -SEND_AS_PEER_INVALID,400,You can't send messages as the specified peer -SEND_CODE_UNAVAILABLE,406,"Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)" -SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified -SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid -SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time -SESSION_EXPIRED,401,The authorization has expired -SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required -SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions" -SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method -SETTINGS_INVALID,400,Invalid settings were provided -SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid -SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name -SHORT_NAME_INVALID,400,The specified short name is invalid -SHORT_NAME_OCCUPIED,400,The specified short name is already in use -SIGN_IN_FAILED,500,Failure while signing in -SLOWMODE_MULTI_MSGS_DISABLED,400,"Slowmode is enabled, you cannot forward multiple messages to this group" -SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat -SMS_CODE_CREATE_FAILED,400,An error occurred while creating the SMS code -SRP_ID_INVALID,400,Invalid SRP ID provided -SRP_PASSWORD_CHANGED,400,Password has changed -START_PARAM_EMPTY,400,The start parameter is empty -START_PARAM_INVALID,400,Start parameter invalid -START_PARAM_TOO_LONG,400,Start parameter is too long -STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc} -STICKERPACK_STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more" -STICKERSET_INVALID,400 406,The provided sticker set is invalid -STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins -STICKERS_EMPTY,400,No sticker provided -STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more" -STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)" -STICKER_EMOJI_INVALID,400,Sticker emoji invalid -STICKER_FILE_INVALID,400,Sticker file invalid -STICKER_GIF_DIMENSIONS,400,The specified video sticker has invalid dimensions -STICKER_ID_INVALID,400,The provided sticker ID is invalid -STICKER_INVALID,400,The provided sticker is invalid -STICKER_MIME_INVALID,400,Make sure to pass a valid image file for the right InputFile parameter -STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid -STICKER_PNG_NOPNG,400,Stickers must be a png file but the used image was not a png -STICKER_TGS_NODOC,400,You must send the animated sticker as a document -STICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs -STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png -STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs -STICKER_VIDEO_BIG,400,The specified video sticker is too big -STICKER_VIDEO_NODOC,400,You must send the video sticker as a document -STICKER_VIDEO_NOWEBM,400,The specified video sticker is not in webm format -STORAGE_CHECK_FAILED,500,Server storage check failed -STORE_INVALID_SCALAR_TYPE,500, -SWITCH_PM_TEXT_EMPTY,400,The switch_pm.text field was empty -TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout -TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session -TAKEOUT_REQUIRED,400 403,You must initialize a takeout request first -TEMP_AUTH_KEY_ALREADY_BOUND,400,The passed temporary key is already bound to another **perm_auth_key_id** -TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided -THEME_FILE_INVALID,400,Invalid theme file provided -THEME_FORMAT_INVALID,400,Invalid theme format provided -THEME_INVALID,400,Theme invalid -THEME_MIME_INVALID,400,"You cannot create this theme, the mime-type is invalid" -THEME_TITLE_INVALID,400,The specified theme title is invalid -TIMEOUT,500,A timeout occurred while fetching data from the worker -TITLE_INVALID,400,The specified stickerpack title is invalid -TMP_PASSWORD_DISABLED,400,The temporary password is disabled -TMP_PASSWORD_INVALID,400,Password auth needs to be regenerated -TOKEN_INVALID,400,The provided token is invalid -TOPIC_DELETED,400,The topic was deleted -TO_LANG_INVALID,400,The specified destination language is invalid -TTL_DAYS_INVALID,400,The provided TTL is invalid -TTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL -TTL_PERIOD_INVALID,400,The provided TTL Period is invalid -TYPES_EMPTY,400,The types field is empty -TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid -Timeout,-503,Timeout while fetching data -UNKNOWN_ERROR,400, -UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs -UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None) -UPDATE_APP_TO_LOGIN,406, -URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL) -USAGE_LIMIT_INVALID,400,The specified usage limit is invalid -USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]""" -USERNAME_NOT_MODIFIED,400,The username is not different from the current username -USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet -USERNAME_OCCUPIED,400,The username is already taken -USERNAME_PURCHASE_AVAILABLE,400, -USERPIC_PRIVACY_REQUIRED,406,You need to disable privacy settings for your profile picture in order to make your geolocation public -USERPIC_UPLOAD_REQUIRED,400 406,You must have a profile picture before using this method -USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)" -USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)" -USER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote -USER_ALREADY_INVITED,400,You have already invited this user -USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat -USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels -USER_BLOCKED,400,User blocked -USER_BOT,400,Bots can only be admins in channels. -USER_BOT_INVALID,400 403,This method can only be called by a bot -USER_BOT_REQUIRED,400,This method can only be called by a bot -USER_CHANNELS_TOO_MUCH,400 403,One of the users you tried to add is already in too many channels/supergroups -USER_CREATOR,400,"You can't leave this channel, because you're its creator" -USER_DEACTIVATED,401,The user has been deleted/deactivated -USER_DEACTIVATED_BAN,401,The user has been deleted/deactivated -USER_DELETED,403,You can't send this secret message because the other participant deleted their account -USER_ID_INVALID,400,"Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited" -USER_INVALID,400 403,The given user was invalid -USER_IS_BLOCKED,400 403,User is blocked -USER_IS_BOT,400,Bots can't send messages to other bots -USER_KICKED,400,This user was kicked from this supergroup/channel -USER_MIGRATE_X,303,The user whose identity is being used to execute queries is associated with DC {new_dc} -USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact -USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel -USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this -USER_RESTRICTED,403 406,"You're spamreported, you can't create channels or chats." -USER_VOLUME_INVALID,400,The specified user volume is invalid -VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming) -VIDEO_FILE_INVALID,400,The given video cannot be used -VIDEO_TITLE_EMPTY,400,The specified video title is empty -VOICE_MESSAGES_FORBIDDEN,400,This user's privacy settings forbid you from sending voice messages -WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper -WALLPAPER_INVALID,400,The input wallpaper was not valid -WALLPAPER_MIME_INVALID,400,The specified wallpaper MIME type is invalid -WC_CONVERT_URL_INVALID,400,WC convert URL invalid -WEBDOCUMENT_INVALID,400,Invalid webdocument URL provided -WEBDOCUMENT_MIME_INVALID,400,Invalid webdocument mime type provided -WEBDOCUMENT_SIZE_TOO_BIG,400,Webdocument is too big! -WEBDOCUMENT_URL_INVALID,400,The given URL cannot be used -WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL -WEBPAGE_MEDIA_EMPTY,400,Webpage media empty -WEBPUSH_AUTH_INVALID,400,The specified web push authentication secret is invalid -WEBPUSH_KEY_INVALID,400,The specified web push elliptic curve Diffie-Hellman public key is invalid -WEBPUSH_TOKEN_INVALID,400,The specified web push token is invalid -WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately -YOU_BLOCKED_USER,400,You blocked this user diff --git a/telethon_generator/data/friendly.csv b/telethon_generator/data/friendly.csv deleted file mode 100644 index 338d1668..00000000 --- a/telethon_generator/data/friendly.csv +++ /dev/null @@ -1,26 +0,0 @@ -ns,friendly,raw -account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession -auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization -auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode -auth.AuthMethods,log_out,auth.logOut -auth.AuthMethods,edit_2fa,account.updatePasswordSettings -bots.BotMethods,inline_query,messages.getInlineBotResults -chats.ChatMethods,action,messages.setTyping -chats.ChatMethods,edit_admin,channels.editAdmin messages.editChatAdmin -chats.ChatMethods,edit_permissions,channels.editBanned messages.editChatDefaultBannedRights -chats.ChatMethods,iter_participants,channels.getParticipants -chats.ChatMethods,iter_admin_log,channels.getAdminLog -dialogs.DialogMethods,iter_dialogs,messages.getDialogs -dialogs.DialogMethods,iter_drafts,messages.getAllDrafts -dialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders -downloads.DownloadMethods,download_media,upload.getFile -messages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages -messages.MessageMethods,send_message,messages.sendMessage -messages.MessageMethods,forward_messages,messages.forwardMessages -messages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage -messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages -messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory -updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference -uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia -uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart -users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername diff --git a/telethon_generator/data/html/404.html b/telethon_generator/data/html/404.html deleted file mode 100644 index 8eb3d37d..00000000 --- a/telethon_generator/data/html/404.html +++ /dev/null @@ -1,44 +0,0 @@ - - - Oopsie! | Telethon - - - - - - - - - - diff --git a/telethon_generator/data/html/core.html b/telethon_generator/data/html/core.html deleted file mode 100644 index daa8500f..00000000 --- a/telethon_generator/data/html/core.html +++ /dev/null @@ -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 deleted file mode 100644 index b240a9e9..00000000 --- a/telethon_generator/data/html/css/docs.dark.css +++ /dev/null @@ -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 deleted file mode 100644 index af3cb210..00000000 --- a/telethon_generator/data/html/css/docs.h4x0r.css +++ /dev/null @@ -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 deleted file mode 100644 index 2d0e95d7..00000000 --- a/telethon_generator/data/html/css/docs.light.css +++ /dev/null @@ -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 deleted file mode 100644 index 1e131224..00000000 --- a/telethon_generator/data/html/img/arrow.svg +++ /dev/null @@ -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 deleted file mode 100644 index a67ffabc..00000000 --- a/telethon_generator/data/html/js/search.js +++ /dev/null @@ -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 deleted file mode 100644 index 8c5d0284..00000000 --- a/telethon_generator/data/methods.csv +++ /dev/null @@ -1,365 +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.createTheme,user,THEME_MIME_INVALID -account.declinePasswordReset,user,RESET_REQUEST_MISSING -account.deleteAccount,user,2FA_CONFIRM_WAIT_X -account.deleteSecureValue,user, -account.finishTakeoutSession,user, -account.getAccountTTL,user, -account.getAllSecureValues,user, -account.getAuthorizationForm,user,PUBLIC_KEY_REQUIRED -account.getAuthorizations,user, -account.getAutoDownloadSettings,user, -account.getContactSignUpNotification,user, -account.getContentSettings,user, -account.getMultiWallPapers,user, -account.getNotifyExceptions,user, -account.getNotifySettings,user,PEER_ID_INVALID -account.getPassword,user, -account.getPasswordSettings,user,PASSWORD_HASH_INVALID -account.getPrivacy,user,PRIVACY_KEY_INVALID -account.getSecureValue,user, -account.getTheme,user, -account.getThemes,user, -account.getTmpPassword,user,PASSWORD_HASH_INVALID TMP_PASSWORD_DISABLED -account.getWallPaper,user,WALLPAPER_INVALID -account.getWallPapers,user, -account.getWebAuthorizations,user, -account.initTakeoutSession,user, -account.installTheme,user, -account.installWallPaper,user,WALLPAPER_INVALID -account.registerDevice,user,TOKEN_INVALID -account.reportPeer,user,PEER_ID_INVALID -account.resendPasswordEmail,user, -account.resetAuthorization,user,HASH_INVALID -account.resetNotifySettings,user, -account.resetWallPapers,user, -account.resetWebAuthorization,user, -account.resetWebAuthorizations,user, -account.saveAutoDownloadSettings,user, -account.saveSecureValue,user,PASSWORD_REQUIRED -account.saveTheme,user, -account.saveWallPaper,user,WALLPAPER_INVALID -account.sendChangePhoneCode,user,FRESH_CHANGE_PHONE_FORBIDDEN PHONE_NUMBER_INVALID -account.sendConfirmPhoneCode,user,HASH_INVALID -account.sendVerifyEmailCode,user,EMAIL_INVALID -account.sendVerifyPhoneCode,user, -account.setAccountTTL,user,TTL_DAYS_INVALID -account.setContactSignUpNotification,user, -account.setContentSettings,user,SENSITIVE_CHANGE_FORBIDDEN -account.setGlobalPrivacySettings,user,AUTOARCHIVE_NOT_AVAILABLE -account.setPrivacy,user,PRIVACY_KEY_INVALID PRIVACY_TOO_LONG -account.unregisterDevice,user,TOKEN_INVALID -account.updateDeviceLocked,user, -account.updateNotifySettings,user,PEER_ID_INVALID -account.updatePasswordSettings,user,EMAIL_UNCONFIRMED_X NEW_SALT_INVALID NEW_SETTINGS_INVALID PASSWORD_HASH_INVALID -account.updateProfile,user,ABOUT_TOO_LONG FIRSTNAME_INVALID -account.updateStatus,user,SESSION_PASSWORD_NEEDED -account.updateTheme,user,THEME_INVALID -account.updateUsername,user,USERNAME_INVALID USERNAME_NOT_MODIFIED USERNAME_OCCUPIED -account.uploadTheme,user, -account.uploadWallPaper,user,WALLPAPER_FILE_INVALID WALLPAPER_MIME_INVALID -account.verifyEmail,user,EMAIL_INVALID -account.verifyPhone,user, -auth.acceptLoginToken,user, -auth.bindTempAuthKey,both,ENCRYPTED_MESSAGE_INVALID INPUT_REQUEST_TOO_LONG TEMP_AUTH_KEY_EMPTY TIMEOUT -auth.cancelCode,user,PHONE_NUMBER_INVALID -auth.checkPassword,user,PASSWORD_HASH_INVALID -auth.checkRecoveryPassword,user,PASSWORD_RECOVERY_EXPIRED -auth.dropTempAuthKeys,both, -auth.exportAuthorization,both,DC_ID_INVALID -auth.exportLoginToken,user, -auth.importAuthorization,both,AUTH_BYTES_INVALID USER_ID_INVALID -auth.importBotAuthorization,both,ACCESS_TOKEN_EXPIRED ACCESS_TOKEN_INVALID API_ID_INVALID -auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_TOKEN_INVALID -auth.logOut,both, -auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID -auth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA -auth.resendCode,user,PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE -auth.resetAuthorizations,user,TIMEOUT -auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED -auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED -auth.signUp,user,FIRSTNAME_INVALID MEMBER_OCCUPY_PRIMARY_LOC_FAILED PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_NUMBER_OCCUPIED REG_ID_GENERATE_FAILED -bots.answerWebhookJSONQuery,bot,QUERY_ID_INVALID USER_BOT_INVALID -bots.sendCustomRequest,bot,USER_BOT_INVALID -bots.setBotCommands,bot,BOT_COMMAND_DESCRIPTION_INVALID BOT_COMMAND_INVALID LANG_CODE_INVALID -channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID -channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW -channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED -channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE -channels.deleteHistory,user,CHANNEL_TOO_BIG -channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN -channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED -channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -channels.editBanned,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ADMIN_INVALID USER_ID_INVALID -channels.editCreator,user,PASSWORD_MISSING PASSWORD_TOO_FRESH_X SESSION_TOO_FRESH_X SRP_ID_INVALID -channels.editLocation,user, -channels.editPhoto,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED FILE_REFERENCE_INVALID PHOTO_INVALID -channels.editTitle,both,CHANNEL_INVALID CHAT_ADMIN_REQUIRED CHAT_NOT_MODIFIED -channels.exportMessageLink,user,CHANNEL_INVALID -channels.getAdminLog,user,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED -channels.getAdminedPublicChannels,user, -channels.getChannels,both,CHANNEL_INVALID CHANNEL_PRIVATE NEED_CHAT_INVALID -channels.getFullChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA TIMEOUT -channels.getGroupsForDiscussion,user, -channels.getInactiveChannels,user, -channels.getLeftChannels,user, -channels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY -channels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT -channels.getParticipants,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID TIMEOUT -channels.inviteToChannel,user,BOTS_TOO_MUCH BOT_GROUPS_BLOCKED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_INVALID CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED USERS_TOO_MUCH USER_BANNED_IN_CHANNEL USER_BLOCKED USER_BOT USER_CHANNELS_TOO_MUCH USER_ID_INVALID USER_KICKED USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -channels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE INVITE_REQUEST_SENT -channels.leaveChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT -channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE -channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE -channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED -channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN -channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS -channels.toggleForum,user,CHAT_DISCUSSION_UNALLOWED -channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS -channels.toggleSignatures,user,CHANNEL_INVALID -channels.toggleSlowMode,user,SECONDS_INVALID -channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED USERNAME_PURCHASE_AVAILABLE -channels.viewSponsoredMessage,user,UNKNOWN_ERROR -contacts.acceptContact,user, -contacts.addContact,user,CONTACT_NAME_EMPTY -contacts.block,user,CONTACT_ID_INVALID -contacts.deleteByPhones,user, -contacts.deleteContacts,user,NEED_MEMBER_INVALID TIMEOUT -contacts.getBlocked,user, -contacts.getContactIDs,user, -contacts.getContacts,user, -contacts.getLocated,user,USERPIC_UPLOAD_REQUIRED -contacts.getSaved,user,TAKEOUT_REQUIRED -contacts.getStatuses,user, -contacts.getTopPeers,user,TYPES_EMPTY -contacts.importContacts,user, -contacts.resetSaved,user, -contacts.resetTopPeerRating,user,PEER_ID_INVALID -contacts.resolveUsername,both,AUTH_KEY_PERM_EMPTY SESSION_PASSWORD_NEEDED USERNAME_INVALID USERNAME_NOT_OCCUPIED -contacts.search,user,QUERY_TOO_SHORT SEARCH_QUERY_EMPTY TIMEOUT -contacts.toggleTopPeers,user, -contacts.unblock,user,CONTACT_ID_INVALID -folders.deleteFolder,user,FOLDER_ID_EMPTY -folders.editPeerFolders,user,FOLDER_ID_INVALID -getFutureSalts,both, -help.acceptTermsOfService,user, -help.editUserInfo,user,ENTITY_BOUNDS_INVALID USER_INVALID -help.getAppChangelog,user, -help.getAppConfig,user, -help.getAppUpdate,user, -help.getCdnConfig,both,AUTH_KEY_PERM_EMPTY TIMEOUT -help.getConfig,both,AUTH_KEY_DUPLICATED TIMEOUT -help.getDeepLinkInfo,user, -help.getInviteText,user, -help.getNearestDc,user, -help.getPassportConfig,user, -help.getProxyData,user, -help.getRecentMeUrls,user, -help.getSupport,user, -help.getSupportName,user,USER_INVALID -help.getTermsOfServiceUpdate,user, -help.getUserInfo,user,USER_INVALID -help.saveAppLog,user, -help.setBotUpdatesStatus,both, -initConnection,both,CONNECTION_LAYER_INVALID INPUT_FETCH_FAIL -invokeAfterMsg,both, -invokeAfterMsgs,both, -invokeWithLayer,both,AUTH_BYTES_INVALID AUTH_KEY_DUPLICATED CDN_METHOD_INVALID CHAT_WRITE_FORBIDDEN CONNECTION_API_ID_INVALID CONNECTION_DEVICE_MODEL_EMPTY CONNECTION_LANG_PACK_INVALID CONNECTION_NOT_INITED CONNECTION_SYSTEM_EMPTY INPUT_LAYER_INVALID INVITE_HASH_EXPIRED NEED_MEMBER_INVALID TIMEOUT -invokeWithMessagesRange,both, -invokeWithTakeout,both, -invokeWithoutUpdates,both, -langpack.getDifference,user,LANG_PACK_INVALID -langpack.getLangPack,user,LANG_PACK_INVALID -langpack.getLanguage,user, -langpack.getLanguages,user,LANG_PACK_INVALID -langpack.getStrings,user,LANG_PACK_INVALID -messages.acceptEncryption,user,CHAT_ID_INVALID ENCRYPTION_ALREADY_ACCEPTED ENCRYPTION_ALREADY_DECLINED ENCRYPTION_OCCUPY_FAILED -messages.acceptUrlAuth,user, -messages.addChatUser,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID USERS_TOO_MUCH USER_ALREADY_PARTICIPANT USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED -messages.checkChatInvite,user,INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID -messages.clearAllDrafts,user, -messages.clearRecentStickers,user, -messages.createChat,user,USERS_TOO_FEW USER_RESTRICTED -messages.deleteChatUser,both,CHAT_ID_INVALID PEER_ID_INVALID USER_NOT_PARTICIPANT -messages.deleteHistory,user,PEER_ID_INVALID -messages.deleteMessages,both,MESSAGE_DELETE_FORBIDDEN -messages.deleteScheduledMessages,user, -messages.discardEncryption,user,CHAT_ID_EMPTY ENCRYPTION_ALREADY_DECLINED ENCRYPTION_ID_INVALID -messages.editChatAbout,both, -messages.editChatAdmin,user,CHAT_ID_INVALID -messages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID -messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID -messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID -messages.editInlineBotMessage,both,ENTITY_BOUNDS_INVALID MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED -messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID -messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID -messages.faveSticker,user,STICKER_ID_INVALID -messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER -messages.getAllChats,user, -messages.getAllDrafts,user, -messages.getAllStickers,user, -messages.getArchivedStickers,user, -messages.getAttachedStickers,user, -messages.getBotCallbackAnswer,user,BOT_RESPONSE_TIMEOUT CHANNEL_INVALID DATA_INVALID MESSAGE_ID_INVALID PEER_ID_INVALID TIMEOUT -messages.getChats,both,CHAT_ID_INVALID PEER_ID_INVALID -messages.getCommonChats,user,USER_ID_INVALID -messages.getDhConfig,user,RANDOM_LENGTH_INVALID -messages.getDialogFilters,user, -messages.getDialogUnreadMarks,user, -messages.getDialogs,user,INPUT_CONSTRUCTOR_INVALID OFFSET_PEER_ID_INVALID SESSION_PASSWORD_NEEDED TIMEOUT -messages.getDocumentByHash,both,SHA256_HASH_INVALID -messages.getEmojiKeywords,user, -messages.getEmojiKeywordsDifference,user, -messages.getEmojiKeywordsLanguages,user, -messages.getEmojiURL,user, -messages.getFavedStickers,user, -messages.getFeaturedStickers,user, -messages.getFullChat,both,CHAT_ID_INVALID PEER_ID_INVALID -messages.getGameHighScores,bot,PEER_ID_INVALID USER_BOT_REQUIRED -messages.getHistory,user,AUTH_KEY_DUPLICATED AUTH_KEY_PERM_EMPTY CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID TIMEOUT -messages.getInlineBotResults,user,BOT_INLINE_DISABLED BOT_INVALID CHANNEL_PRIVATE TIMEOUT -messages.getInlineGameHighScores,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED -messages.getMaskStickers,user, -messages.getMessageEditData,user,MESSAGE_AUTHOR_REQUIRED PEER_ID_INVALID -messages.getMessages,both, -messages.getMessagesReadParticipants,user,CHAT_TOO_BIG MESSAGE_ID_INVALID -messages.getMessagesViews,user,CHANNEL_PRIVATE CHAT_ID_INVALID PEER_ID_INVALID -messages.getOnlines,user, -messages.getPeerDialogs,user,CHANNEL_PRIVATE PEER_ID_INVALID -messages.getPeerSettings,user,CHANNEL_INVALID PEER_ID_INVALID -messages.getPinnedDialogs,user, -messages.getPollResults,user, -messages.getPollVotes,user,BROADCAST_FORBIDDEN POLL_VOTE_REQUIRED -messages.getRecentLocations,user, -messages.getRecentStickers,user, -messages.getSavedGifs,user, -messages.getScheduledHistory,user, -messages.getScheduledMessages,user, -messages.getSearchCounters,user, -messages.getSplitRanges,user, -messages.getStatsURL,user, -messages.getStickerSet,both,EMOTICON_STICKERPACK_MISSING STICKERSET_INVALID -messages.getStickers,user,EMOTICON_EMPTY -messages.getSuggestedDialogFilters,user, -messages.getUnreadMentions,user,PEER_ID_INVALID -messages.getWebPage,user,WC_CONVERT_URL_INVALID -messages.getWebPagePreview,user,ENTITY_BOUNDS_INVALID -messages.hideAllChatJoinRequests,user,HIDE_REQUESTER_MISSING -messages.hidePeerSettingsBar,user, -messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID INVITE_REQUEST_SENT SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT -messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT -messages.installStickerSet,user,STICKERSET_INVALID -messages.markDialogUnread,user, -messages.migrateChat,user,CHAT_ADMIN_REQUIRED CHAT_ID_INVALID PEER_ID_INVALID -messages.readEncryptedHistory,user,MSG_WAIT_FAILED -messages.readFeaturedStickers,user, -messages.readHistory,user,PEER_ID_INVALID TIMEOUT -messages.readMentions,user, -messages.readMessageContents,user, -messages.receivedMessages,user, -messages.receivedQueue,user,MAX_QTS_INVALID MSG_WAIT_FAILED -messages.reorderPinnedDialogs,user,PEER_ID_INVALID -messages.reorderStickerSets,user, -messages.report,user, -messages.reportEncryptedSpam,user,CHAT_ID_INVALID -messages.reportSpam,user,PEER_ID_INVALID -messages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID -messages.requestUrlAuth,user, -messages.saveDraft,user,ENTITY_BOUNDS_INVALID PEER_ID_INVALID -messages.saveGif,user,GIF_ID_INVALID -messages.saveRecentSticker,user,STICKER_ID_INVALID -messages.search,user,CHAT_ADMIN_REQUIRED FROM_PEER_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FILTER_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID -messages.searchGifs,user,METHOD_INVALID SEARCH_QUERY_EMPTY -messages.searchGlobal,user,SEARCH_QUERY_EMPTY -messages.searchStickerSets,user, -messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED -messages.sendEncryptedFile,user,MSG_WAIT_FAILED -messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED -messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID ENTITY_BOUNDS_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID POSTPONED_TIMEOUT QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY -messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_BOUNDS_INVALID ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER -messages.sendMultiMedia,both,ENTITY_BOUNDS_INVALID MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED -messages.sendScheduledMessages,user, -messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID -messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID -messages.setBotPrecheckoutResults,both,ERROR_TEXT_EMPTY -messages.setBotShippingResults,both,QUERY_ID_INVALID -messages.setChatTheme,user,EMOJI_INVALID EMOJI_NOT_MODIFIED PEER_ID_INVALID -messages.setEncryptedTyping,user,CHAT_ID_INVALID -messages.setGameScore,bot,PEER_ID_INVALID USER_BOT_REQUIRED -messages.setHistoryTTL,user,CHAT_NOT_MODIFIED TTL_PERIOD_INVALID -messages.setInlineBotResults,bot,ARTICLE_TITLE_EMPTY AUDIO_CONTENT_URL_EMPTY AUDIO_TITLE_EMPTY BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID DOCUMENT_INVALID FILE_CONTENT_TYPE_INVALID FILE_TITLE_EMPTY GIF_CONTENT_TYPE_INVALID MESSAGE_EMPTY NEXT_OFFSET_INVALID PHOTO_CONTENT_TYPE_INVALID PHOTO_CONTENT_URL_EMPTY PHOTO_THUMB_URL_EMPTY QUERY_ID_INVALID REPLY_MARKUP_INVALID RESULT_TYPE_INVALID SEND_MESSAGE_MEDIA_INVALID SEND_MESSAGE_TYPE_INVALID START_PARAM_INVALID STICKER_DOCUMENT_INVALID USER_BOT_INVALID VIDEO_TITLE_EMPTY WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_URL_INVALID -messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED -messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT -messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID -messages.startHistoryImport,user,IMPORT_ID_INVALID -messages.toggleDialogPin,user,PEER_HISTORY_EMPTY PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH -messages.toggleStickerSets,user, -messages.uninstallStickerSet,user,STICKERSET_INVALID -messages.updateDialogFilter,user, -messages.updateDialogFiltersOrder,user, -messages.updatePinnedMessage,both,BOT_ONESIDE_NOT_AVAIL -messages.uploadEncryptedFile,user, -messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID POSTPONED_TIMEOUT -payments.clearSavedInfo,user, -payments.getBankCardData,user,BANK_CARD_NUMBER_INVALID -payments.getPaymentForm,user,MESSAGE_ID_INVALID -payments.getPaymentReceipt,user,MESSAGE_ID_INVALID -payments.getSavedInfo,user, -payments.sendPaymentForm,user,MESSAGE_ID_INVALID -payments.validateRequestedInfo,user,MESSAGE_ID_INVALID -phone.acceptCall,user,CALL_ALREADY_ACCEPTED CALL_ALREADY_DECLINED CALL_OCCUPY_FAILED CALL_PEER_INVALID CALL_PROTOCOL_FLAGS_INVALID -phone.confirmCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID -phone.createGroupCall,user,SCHEDULE_DATE_INVALID -phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID -phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED -phone.editGroupCallParticipant,user,USER_VOLUME_INVALID -phone.getCallConfig,user, -phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN INVITE_FORBIDDEN_WITH_JOINAS USER_ALREADY_INVITED -phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH -phone.joinGroupCallPresentation,user,PARTICIPANT_JOIN_MISSING -phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID -phone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED -phone.saveCallDebug,user,CALL_PEER_INVALID DATA_JSON_INVALID -phone.setCallRating,user,CALL_PEER_INVALID -phone.toggleGroupCallSettings,user,GROUPCALL_NOT_MODIFIED -photos.deletePhotos,user, -photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID -photos.updateProfilePhoto,user,PHOTO_ID_INVALID -photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID STICKER_MIME_INVALID VIDEO_FILE_INVALID -ping,both, -reqDHParams,both, -reqPq,both, -reqPqMulti,both, -rpcDropAnswer,both, -setClientDHParams,both, -stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X -stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X -stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD -stickers.addStickerToSet,both,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS -stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID -stickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED -stickers.createStickerSet,both,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID -stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID -stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS -stickers.suggestShortName,user,TITLE_INVALID -updates.getChannelDifference,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA HISTORY_GET_FAILED PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID PERSISTENT_TIMESTAMP_OUTDATED RANGES_INVALID TIMEOUT -updates.getDifference,both,AUTH_KEY_PERM_EMPTY CDN_METHOD_INVALID DATE_EMPTY NEED_MEMBER_INVALID PERSISTENT_TIMESTAMP_EMPTY PERSISTENT_TIMESTAMP_INVALID SESSION_PASSWORD_NEEDED STORE_INVALID_SCALAR_TYPE TIMEOUT -updates.getState,both,AUTH_KEY_DUPLICATED MSGID_DECREASE_RETRY SESSION_PASSWORD_NEEDED TIMEOUT -upload.getCdnFile,user,UNKNOWN_METHOD -upload.getCdnFileHashes,both,CDN_METHOD_INVALID RSA_DECRYPT_FAILED -upload.getFile,both,AUTH_KEY_PERM_EMPTY FILE_ID_INVALID INPUT_FETCH_FAIL LIMIT_INVALID LOCATION_INVALID OFFSET_INVALID TIMEOUT -upload.getFileHashes,both, -upload.getWebFile,user,LOCATION_INVALID -upload.reuploadCdnFile,both,RSA_DECRYPT_FAILED -upload.saveBigFilePart,both,FILE_PARTS_INVALID FILE_PART_EMPTY FILE_PART_INVALID FILE_PART_SIZE_CHANGED FILE_PART_SIZE_INVALID TIMEOUT -upload.saveFilePart,both,FILE_PART_EMPTY FILE_PART_INVALID INPUT_FETCH_FAIL SESSION_PASSWORD_NEEDED -users.getFullUser,both,TIMEOUT USER_ID_INVALID -users.getUsers,both,AUTH_KEY_PERM_EMPTY MEMBER_NO_LOCATION NEED_MEMBER_INVALID SESSION_PASSWORD_NEEDED TIMEOUT -users.setSecureValueErrors,bot, diff --git a/telethon_generator/data/mtproto.tl b/telethon_generator/data/mtproto.tl deleted file mode 100644 index 203df276..00000000 --- a/telethon_generator/data/mtproto.tl +++ /dev/null @@ -1,116 +0,0 @@ -// Core types (no need to gen) - -//vector#1cb5c415 {t:Type} # [ t ] = Vector t; - -/////////////////////////////// -/// Authorization key creation -/////////////////////////////// - -resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector = ResPQ; - -p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; -p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; -p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; -p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; - -bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner; - -server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params; -server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params; - -server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data; - -client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data; - -dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer; -dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer; -dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer; - -destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes; -destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes; -destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; - ----functions--- - -req_pq#60469778 nonce:int128 = ResPQ; -req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; - -req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params; - -set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer; - -destroy_auth_key#d1435160 = DestroyAuthKeyRes; - -/////////////////////////////// -////////////// System messages -/////////////////////////////// - ----types--- - -msgs_ack#62d6b459 msg_ids:Vector = MsgsAck; - -bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; -bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; - -msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq; -msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo; -msgs_all_info#8cc0d131 msg_ids:Vector info:string = MsgsAllInfo; - -msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; -msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; - -msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq; - -//rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually - -rpc_error#2144ca19 error_code:int error_message:string = RpcError; - -rpc_answer_unknown#5e2ad36e = RpcDropAnswer; -rpc_answer_dropped_running#cd78e586 = RpcDropAnswer; -rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer; - -future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt; -future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; - -pong#347773c5 msg_id:long ping_id:long = Pong; - -destroy_session_ok#e22045fc session_id:long = DestroySessionRes; -destroy_session_none#62d350c9 session_id:long = DestroySessionRes; - -new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; - -//message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually -//msg_container#73f1f8dc messages:vector = MessageContainer; // parsed manually -//msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container -//gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually - -http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; - -//ipPort ipv4:int port:int = IpPort; -//help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; - -ipPort#d433ad73 ipv4:int port:int = IpPort; -ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; -accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; -help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; - -tlsClientHello blocks:vector = TlsClientHello; - -tlsBlockString data:string = TlsBlock; -tlsBlockRandom length:int = TlsBlock; -tlsBlockZero length:int = TlsBlock; -tlsBlockDomain = TlsBlock; -tlsBlockGrease seed:int = TlsBlock; -tlsBlockPublicKey = TlsBlock; -tlsBlockScope entries:Vector = TlsBlock; - ----functions--- - -rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; - -get_future_salts#b921bd04 num:int = FutureSalts; - -ping#7abe77ec ping_id:long = Pong; -ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; - -destroy_session#e7512126 session_id:long = DestroySessionRes; diff --git a/telethon_generator/docswriter.py b/telethon_generator/docswriter.py deleted file mode 100644 index f174d346..00000000 --- a/telethon_generator/docswriter.py +++ /dev/null @@ -1,295 +0,0 @@ -import os -import re - - -class DocsWriter: - """ - Utility class used to write the HTML files used on the documentation. - """ - def __init__(self, filename, type_to_path): - """ - Initializes the writer to the specified output file, - creating the parent directories when used if required. - """ - self.filename = filename - self._parent = str(self.filename.parent) - self.handle = None - self.title = '' - - # Should be set before calling adding items to the menu - self.menu_separator_tag = None - - # Utility functions - self.type_to_path = lambda t: self._rel(type_to_path(t)) - - # Control signals - self.menu_began = False - self.table_columns = 0 - self.table_columns_left = None - self.write_copy_script = False - self._script = '' - - def _rel(self, path): - """ - Get the relative path for the given path from the current - file by working around https://bugs.python.org/issue20012. - """ - return os.path.relpath( - str(path), self._parent).replace(os.path.sep, '/') - - # High level writing - def write_head(self, title, css_path, default_css): - """Writes the head part for the generated document, - with the given title and CSS - """ - self.title = title - self.write( - ''' - - - - {title} - - - - - - -
    ''', - title=title, - rel_css=self._rel(css_path), - def_css=default_css - ) - - def set_menu_separator(self, img): - """Sets the menu separator. - Must be called before adding entries to the menu - """ - if img: - self.menu_separator_tag = '/'.format( - self._rel(img)) - else: - self.menu_separator_tag = None - - def add_menu(self, name, link=None): - """Adds a menu entry, will create it if it doesn't exist yet""" - if self.menu_began: - if self.menu_separator_tag: - self.write(self.menu_separator_tag) - else: - # First time, create the menu tag - self.write('') - - def write_title(self, title, level=1, id=None): - """Writes a title header in the document body, - with an optional depth level - """ - if id: - self.write('{title}', - title=title, lv=level, id=id) - else: - self.write('{title}', - title=title, lv=level) - - def write_code(self, tlobject): - """Writes the code for the given 'tlobject' properly - formatted with hyperlinks - """ - self.write('
    ---{}---\n',
    -                   'functions' if tlobject.is_function else 'types')
    -
    -        # Write the function or type and its ID
    -        if tlobject.namespace:
    -            self.write(tlobject.namespace)
    -            self.write('.')
    -
    -        self.write('{}#{:08x}', tlobject.name, tlobject.id)
    -
    -        # Write all the arguments (or do nothing if there's none)
    -        for arg in tlobject.args:
    -            self.write(' ')
    -            add_link = not arg.generic_definition and not arg.is_generic
    -
    -            # "Opening" modifiers
    -            if arg.generic_definition:
    -                self.write('{')
    -
    -            # Argument name
    -            self.write(arg.name)
    -            self.write(':')
    -
    -            # "Opening" modifiers
    -            if arg.flag:
    -                self.write('{}.{}?', arg.flag, arg.flag_index)
    -
    -            if arg.is_generic:
    -                self.write('!')
    -
    -            if arg.is_vector:
    -                self.write('Vector<',
    -                           self.type_to_path('vector'))
    -
    -            # Argument type
    -            if arg.type:
    -                if add_link:
    -                    self.write('', self.type_to_path(arg.type))
    -                self.write(arg.type)
    -                if add_link:
    -                    self.write('')
    -            else:
    -                self.write('#')
    -
    -            # "Closing" modifiers
    -            if arg.is_vector:
    -                self.write('>')
    -
    -            if arg.generic_definition:
    -                self.write('}')
    -
    -        # Now write the resulting type (result from a function/type)
    -        self.write(' = ')
    -        generic_name = next((arg.name for arg in tlobject.args
    -                             if arg.generic_definition), None)
    -
    -        if tlobject.result == generic_name:
    -            # Generic results cannot have any link
    -            self.write(tlobject.result)
    -        else:
    -            if re.search('^vector<', tlobject.result, re.IGNORECASE):
    -                # Notice that we don't simply make up the "Vector" part,
    -                # because some requests (as of now, only FutureSalts),
    -                # use a lower type name for it (see #81)
    -                vector, inner = tlobject.result.split('<')
    -                inner = inner.strip('>')
    -                self.write('{}<',
    -                           self.type_to_path(vector), vector)
    -
    -                self.write('{}>',
    -                           self.type_to_path(inner), inner)
    -            else:
    -                self.write('{}',
    -                           self.type_to_path(tlobject.result), tlobject.result)
    -
    -        self.write('
    ') - - def begin_table(self, column_count): - """Begins a table with the given 'column_count', required to automatically - create the right amount of columns when adding items to the rows""" - self.table_columns = column_count - self.table_columns_left = 0 - self.write('') - - def add_row(self, text, link=None, bold=False, align=None): - """This will create a new row, or add text to the next column - of the previously created, incomplete row, closing it if complete""" - if not self.table_columns_left: - # Starting a new row - self.write('') - self.table_columns_left = self.table_columns - - self.write('') - - if bold: - self.write('') - if link: - self.write('', self._rel(link)) - - # Finally write the real table data, the given text - self.write(text) - - if link: - self.write('') - if bold: - self.write('') - - self.write('') - - self.table_columns_left -= 1 - if not self.table_columns_left: - self.write('') - - def end_table(self): - # If there was any column left, finish it before closing the table - if self.table_columns_left: - self.write('') - - self.write('
    ') - - def write_text(self, text): - """Writes a paragraph of text""" - self.write('

    {}

    ', text) - - def write_copy_button(self, text, text_to_copy): - """Writes a button with 'text' which can be used - to copy 'text_to_copy' to clipboard when it's clicked.""" - self.write_copy_script = True - self.write('' - .format(text_to_copy, text)) - - def add_script(self, src='', path=None): - if path: - self._script += ''.format( - self._rel(path)) - elif src: - self._script += ''.format(src) - - def end_body(self): - """Ends the whole document. This should be called the last""" - if self.write_copy_script: - self.write( - '' - '' - ) - - self.write('
    {}', self._script) - - # "Low" level writing - def write(self, s, *args, **kwargs): - """Wrapper around handle.write""" - if args or kwargs: - self.handle.write(s.format(*args, **kwargs)) - else: - self.handle.write(s) - - # With block - def __enter__(self): - # Sanity check - self.filename.parent.mkdir(parents=True, exist_ok=True) - self.handle = self.filename.open('w', encoding='utf-8') - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.handle.close() diff --git a/telethon_generator/generators/__init__.py b/telethon_generator/generators/__init__.py deleted file mode 100644 index 156606e0..00000000 --- a/telethon_generator/generators/__init__.py +++ /dev/null @@ -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 deleted file mode 100755 index ccec5679..00000000 --- a/telethon_generator/generators/docs.py +++ /dev/null @@ -1,649 +0,0 @@ -#!/usr/bin/env python3 -import functools -import os -import pathlib -import re -import shutil -from collections import defaultdict -from pathlib import Path - -from ..docswriter import DocsWriter -from ..parsers import TLObject, Usability -from ..utils import snake_to_camel_case - -CORE_TYPES = { - 'int', 'long', 'int128', 'int256', 'double', - 'vector', 'string', 'bool', 'true', 'bytes', 'date' -} - - -def _get_file_name(tlobject): - """``ClassName -> class_name.html``.""" - name = tlobject.name if isinstance(tlobject, TLObject) else tlobject - # Courtesy of http://stackoverflow.com/a/1176023/4759433 - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - result = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() - return '{}.html'.format(result) - - -def get_import_code(tlobject): - """``TLObject -> from ... import ...``.""" - kind = 'functions' if tlobject.is_function else 'types' - ns = '.' + tlobject.namespace if tlobject.namespace else '' - return 'from telethon.tl.{}{} import {}'\ - .format(kind, ns, tlobject.class_name) - - -def _get_path_for(tlobject): - """Returns the path for the given TLObject.""" - out_dir = pathlib.Path('methods' if tlobject.is_function else 'constructors') - if tlobject.namespace: - out_dir /= tlobject.namespace - - return out_dir / _get_file_name(tlobject) - - -def _get_path_for_type(type_): - """Similar to `_get_path_for` but for only type names.""" - if type_.lower() in CORE_TYPES: - return Path('index.html#%s' % type_.lower()) - elif '.' in type_: - namespace, name = type_.split('.') - return Path('types', namespace, _get_file_name(name)) - else: - return Path('types', _get_file_name(type_)) - - -def _find_title(html_file): - """Finds the for the given HTML file, or (Unknown).""" - # TODO Is it necessary to read files like this? - with html_file.open() as f: - for line in f: - if '<title>' in line: - # + 7 to skip len('<title>') - return line[line.index('<title>') + 7:line.index('')] - - return '(Unknown)' - - -def _build_menu(docs): - """ - Builds the menu used for the current ``DocumentWriter``. - """ - - paths = [] - current = docs.filename - top = pathlib.Path('.') - while current != top: - current = current.parent - paths.append(current) - - for path in reversed(paths): - docs.add_menu(path.stem.title() or 'API', link=path / 'index.html') - - if docs.filename.stem != 'index': - docs.add_menu(docs.title, link=docs.filename) - - docs.end_menu() - - -def _generate_index(folder, paths, - bots_index=False, bots_index_paths=()): - """Generates the index file for the specified folder""" - # Determine the namespaces listed here (as sub folders) - # and the files (.html files) that we should link to - namespaces = [] - files = [] - INDEX = 'index.html' - BOT_INDEX = 'botindex.html' - - for item in (bots_index_paths or folder.iterdir()): - if item.is_dir(): - namespaces.append(item) - elif item.name not in (INDEX, BOT_INDEX): - files.append(item) - - # Now that everything is setup, write the index.html file - filename = folder / (BOT_INDEX if bots_index else INDEX) - with DocsWriter(filename, _get_path_for_type) as docs: - # Title should be the current folder name - docs.write_head(str(folder).replace(os.path.sep, '/').title(), - css_path=paths['css'], - default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - docs.write_title(str(filename.parent) - .replace(os.path.sep, '/').title()) - - if bots_index: - docs.write_text('These are the requests that you may be able to ' - 'use as a bot. Click here to ' - 'view them all.'.format(INDEX)) - else: - docs.write_text('Click here to view the requests ' - 'that you can use as a bot.'.format(BOT_INDEX)) - if namespaces: - docs.write_title('Namespaces', level=3) - docs.begin_table(4) - namespaces.sort() - for namespace in namespaces: - # For every namespace, also write the index of it - namespace_paths = [] - if bots_index: - for item in bots_index_paths: - if item.parent == namespace: - namespace_paths.append(item) - - _generate_index(namespace, paths, - bots_index, namespace_paths) - - docs.add_row( - namespace.stem.title(), - link=namespace / (BOT_INDEX if bots_index else INDEX)) - - docs.end_table() - - docs.write_title('Available items') - docs.begin_table(2) - - files = [(f, _find_title(f)) for f in files] - files.sort(key=lambda t: t[1]) - - for file, title in files: - docs.add_row(title, link=file) - - docs.end_table() - docs.end_body() - - -def _get_description(arg): - """Generates a proper description for the given argument.""" - desc = [] - otherwise = False - if arg.can_be_inferred: - desc.append('If left unspecified, it will be inferred automatically.') - otherwise = True - elif arg.flag: - desc.append('This argument defaults to ' - 'None and can be omitted.') - otherwise = True - - if arg.type in {'InputPeer', 'InputUser', 'InputChannel', - 'InputNotifyPeer', 'InputDialogPeer'}: - desc.append( - 'Anything entity-like will work if the library can find its ' - 'Input version (e.g., usernames, Peer, ' - 'User or Channel objects, etc.).' - ) - - if arg.is_vector: - if arg.is_generic: - desc.append('A list of other Requests must be supplied.') - else: - desc.append('A list must be supplied.') - elif arg.is_generic: - desc.append('A different Request must be supplied for this argument.') - else: - otherwise = False # Always reset to false if no other text is added - - if otherwise: - desc.insert(1, 'Otherwise,') - desc[-1] = desc[-1][:1].lower() + desc[-1][1:] - - return ' '.join(desc).replace( - 'list', - 'list' - ) - - -def _copy_replace(src, dst, replacements): - """Copies the src file into dst applying the replacements dict""" - with src.open() as infile, dst.open('w') as outfile: - outfile.write(re.sub( - '|'.join(re.escape(k) for k in replacements), - lambda m: str(replacements[m.group(0)]), - infile.read() - )) - - -def _write_html_pages(tlobjects, methods, layer, input_res): - """ - Generates the documentation HTML files from from ``scheme.tl`` - to ``/methods`` and ``/constructors``, etc. - """ - # Save 'Type: [Constructors]' for use in both: - # * Seeing the return type or constructors belonging to the same type. - # * Generating the types documentation, showing available constructors. - paths = {k: pathlib.Path(v) for k, v in ( - ('css', 'css'), - ('arrow', 'img/arrow.svg'), - ('search.js', 'js/search.js'), - ('404', '404.html'), - ('index_all', 'index.html'), - ('bot_index', 'botindex.html'), - ('index_types', 'types/index.html'), - ('index_methods', 'methods/index.html'), - ('index_constructors', 'constructors/index.html') - )} - paths['default_css'] = 'light' # docs..css, local path - type_to_constructors = defaultdict(list) - type_to_functions = defaultdict(list) - for tlobject in tlobjects: - d = type_to_functions if tlobject.is_function else type_to_constructors - d[tlobject.innermost_result].append(tlobject) - - for t, cs in type_to_constructors.items(): - type_to_constructors[t] = list(sorted(cs, key=lambda c: c.name)) - - methods = {m.name: m for m in methods} - bot_docs_paths = [] - - for tlobject in tlobjects: - filename = _get_path_for(tlobject) - with DocsWriter(filename, _get_path_for_type) as docs: - docs.write_head(title=tlobject.class_name, - css_path=paths['css'], - default_css=paths['default_css']) - - # Create the menu (path to the current TLObject) - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - - # Create the page title - docs.write_title(tlobject.class_name) - - if tlobject.is_function: - if tlobject.usability == Usability.USER: - start = 'Only users can' - elif tlobject.usability == Usability.BOT: - bot_docs_paths.append(filename) - start = 'Only bots can' - elif tlobject.usability == Usability.BOTH: - bot_docs_paths.append(filename) - start = 'Both users and bots can' - else: - bot_docs_paths.append(filename) - start = \ - 'Both users and bots may be able to' - - docs.write_text('{} use this request. ' - 'See code examples.'.format(start)) - - # Write the code definition for this TLObject - docs.write_code(tlobject) - docs.write_copy_button('Copy import to the clipboard', - get_import_code(tlobject)) - - # Write the return type (or constructors belonging to the same type) - docs.write_title('Returns' if tlobject.is_function - else 'Belongs to', level=3) - - generic_arg = next((arg.name for arg in tlobject.args - if arg.generic_definition), None) - - if tlobject.result == generic_arg: - # We assume it's a function returning a generic type - generic_arg = next((arg.name for arg in tlobject.args - if arg.is_generic)) - docs.write_text('This request returns the result of whatever ' - 'the result from invoking the request passed ' - 'through {} is.'.format(generic_arg)) - else: - if re.search('^vector<', tlobject.result, re.IGNORECASE): - docs.write_text('A list of the following type is returned.') - inner = tlobject.innermost_result - else: - inner = tlobject.result - - docs.begin_table(column_count=1) - docs.add_row(inner, link=_get_path_for_type(inner)) - docs.end_table() - - cs = type_to_constructors.get(inner, []) - if not cs: - docs.write_text('This type has no instances available.') - elif len(cs) == 1: - docs.write_text('This type can only be an instance of:') - else: - docs.write_text('This type can be an instance of either:') - - docs.begin_table(column_count=2) - for constructor in cs: - link = _get_path_for(constructor) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - - # Return (or similar types) written. Now parameters/members - docs.write_title( - 'Parameters' if tlobject.is_function else 'Members', level=3 - ) - - # Sort the arguments in the same way they're sorted - # on the generated code (flags go last) - args = [ - a for a in tlobject.sorted_args() - if not a.flag_indicator and not a.generic_definition - ] - - if args: - # Writing parameters - docs.begin_table(column_count=3) - - for arg in args: - # Name row - docs.add_row(arg.name, - bold=True) - - # Type row - friendly_type = 'flag' if arg.type == 'true' else arg.type - if arg.is_generic: - docs.add_row('!' + friendly_type, align='center') - else: - docs.add_row( - friendly_type, align='center', - link=_get_path_for_type(arg.type) - ) - - # Add a description for this argument - docs.add_row(_get_description(arg)) - - docs.end_table() - else: - if tlobject.is_function: - docs.write_text('This request takes no input parameters.') - else: - docs.write_text('This type has no members.') - - if tlobject.is_function: - docs.write_title('Known RPC errors') - method_info = methods.get(tlobject.fullname) - errors = method_info and method_info.errors - if not errors: - docs.write_text("This request can't cause any RPC error " - "as far as we know.") - else: - docs.write_text( - 'This request can cause {} known error{}:'.format( - len(errors), '' if len(errors) == 1 else 's' - )) - docs.begin_table(column_count=2) - for error in errors: - docs.add_row('{}'.format(error.name)) - docs.add_row('{}.'.format(error.description)) - docs.end_table() - docs.write_text('You can import these from ' - 'telethon.errors.') - - docs.write_title('Example', id='examples') - if tlobject.friendly: - ns, friendly = tlobject.friendly - docs.write_text( - 'Please refer to the documentation of client.{1}() ' - 'to learn about the parameters and see several code ' - 'examples on how to use it.' - .format(ns, friendly) - ) - docs.write_text( - 'The method above is the recommended way to do it. ' - 'If you need more control over the parameters or want ' - 'to learn how it is implemented, open the details by ' - 'clicking on the "Details" text.' - ) - docs.write('
    ') - - docs.write('''
    \
    -from telethon.sync import TelegramClient
    -from telethon import functions, types
    -
    -with TelegramClient(name, api_id, api_hash) as client:
    -    result = client(''')
    -                tlobject.as_example(docs, indent=1)
    -                docs.write(')\n')
    -                if tlobject.result.startswith('Vector'):
    -                    docs.write('''\
    -    for x in result:
    -        print(x''')
    -                else:
    -                    docs.write('    print(result')
    -                    if tlobject.result != 'Bool' \
    -                            and not tlobject.result.startswith('Vector'):
    -                        docs.write('.stringify()')
    -
    -                docs.write(')
    ') - if tlobject.friendly: - docs.write('
    ') - - depth = '../' * (2 if tlobject.namespace else 1) - docs.add_script(src='prependPath = "{}";'.format(depth)) - docs.add_script(path=paths['search.js']) - docs.end_body() - - # Find all the available types (which are not the same as the constructors) - # Each type has a list of constructors associated to it, hence is a map - for t, cs in type_to_constructors.items(): - filename = _get_path_for_type(t) - out_dir = filename.parent - if out_dir: - out_dir.mkdir(parents=True, exist_ok=True) - - # Since we don't have access to the full TLObject, split the type - if '.' in t: - namespace, name = t.split('.') - else: - namespace, name = None, t - - with DocsWriter(filename, _get_path_for_type) as docs: - docs.write_head(title=snake_to_camel_case(name), - css_path=paths['css'], - default_css=paths['default_css']) - - docs.set_menu_separator(paths['arrow']) - _build_menu(docs) - - # Main file title - docs.write_title(snake_to_camel_case(name)) - - # List available constructors for this type - docs.write_title('Available constructors', level=3) - if not cs: - docs.write_text('This type has no constructors available.') - elif len(cs) == 1: - docs.write_text('This type has one constructor available.') - else: - docs.write_text('This type has %d constructors available.' % - len(cs)) - - docs.begin_table(2) - for constructor in cs: - # Constructor full name - link = _get_path_for(constructor) - docs.add_row(constructor.class_name, link=link) - docs.end_table() - - # List all the methods which return this type - docs.write_title('Requests returning this type', level=3) - functions = type_to_functions.get(t, []) - if not functions: - docs.write_text('No request returns this type.') - elif len(functions) == 1: - docs.write_text('Only the following request returns this type.') - else: - docs.write_text( - 'The following %d requests return this type as a result.' % - len(functions) - ) - - docs.begin_table(2) - for func in functions: - link = _get_path_for(func) - docs.add_row(func.class_name, link=link) - docs.end_table() - - # List all the methods which take this type as input - docs.write_title('Requests accepting this type as input', level=3) - other_methods = sorted( - (u for u in tlobjects - if any(a.type == t for a in u.args) and u.is_function), - key=lambda u: u.name - ) - if not other_methods: - docs.write_text( - 'No request accepts this type as an input parameter.') - elif len(other_methods) == 1: - docs.write_text( - 'Only this request has a parameter with this type.') - else: - docs.write_text( - 'The following %d requests accept this type as an input ' - 'parameter.' % len(other_methods)) - - docs.begin_table(2) - for ot in other_methods: - link = _get_path_for(ot) - docs.add_row(ot.class_name, link=link) - docs.end_table() - - # List every other type which has this type as a member - docs.write_title('Other types containing this type', level=3) - other_types = sorted( - (u for u in tlobjects - if any(a.type == t for a in u.args) and not u.is_function), - key=lambda u: u.name - ) - - if not other_types: - docs.write_text( - 'No other types have a member of this type.') - elif len(other_types) == 1: - docs.write_text( - 'You can find this type as a member of this other type.') - else: - docs.write_text( - 'You can find this type as a member of any of ' - 'the following %d types.' % len(other_types)) - - docs.begin_table(2) - for ot in other_types: - link = _get_path_for(ot) - docs.add_row(ot.class_name, link=link) - docs.end_table() - docs.end_body() - - # After everything's been written, generate an index.html per folder. - # This will be done automatically and not taking into account any extra - # information that we have available, simply a file listing all the others - # accessible by clicking on their title - for folder in ['types', 'methods', 'constructors']: - _generate_index(pathlib.Path(folder), paths) - - _generate_index(pathlib.Path('methods'), paths, True, - bot_docs_paths) - - # Write the final core index, the main index for the rest of files - types = set() - methods = [] - cs = [] - for tlobject in tlobjects: - if tlobject.is_function: - methods.append(tlobject) - else: - cs.append(tlobject) - - if not tlobject.result.lower() in CORE_TYPES: - if re.search('^vector<', tlobject.result, re.IGNORECASE): - types.add(tlobject.innermost_result) - else: - types.add(tlobject.result) - - types = sorted(types) - methods = sorted(methods, key=lambda m: m.name) - cs = sorted(cs, key=lambda c: c.name) - - shutil.copy(str(input_res / '404.html'), str(paths['404'])) - _copy_replace(input_res / 'core.html', paths['index_all'], { - '{type_count}': len(types), - '{method_count}': len(methods), - '{constructor_count}': len(tlobjects) - len(methods), - '{layer}': layer, - }) - - def fmt(xs): - zs = {} # create a dict to hold those which have duplicated keys - for x in xs: - zs[x.class_name] = x.class_name in zs - return ', '.join( - '"{}.{}"'.format(x.namespace, x.class_name) - if zs[x.class_name] and x.namespace - else '"{}"'.format(x.class_name) for x in xs - ) - - request_names = fmt(methods) - constructor_names = fmt(cs) - - def fmt(xs, formatter): - return ', '.join('"{}"'.format( - formatter(x)).replace(os.path.sep, '/') for x in xs) - - type_names = fmt(types, formatter=lambda x: x) - - request_urls = fmt(methods, _get_path_for) - type_urls = fmt(types, _get_path_for_type) - constructor_urls = fmt(cs, _get_path_for) - - paths['search.js'].parent.mkdir(parents=True, exist_ok=True) - _copy_replace(input_res / 'js/search.js', paths['search.js'], { - '{request_names}': request_names, - '{type_names}': type_names, - '{constructor_names}': constructor_names, - '{request_urls}': request_urls, - '{type_urls}': type_urls, - '{constructor_urls}': constructor_urls - }) - - -def _copy_resources(res_dir): - for dirname, files in [('css', ['docs.light.css', 'docs.dark.css']), - ('img', ['arrow.svg'])]: - dirpath = pathlib.Path(dirname) - dirpath.mkdir(parents=True, exist_ok=True) - for file in files: - shutil.copy(str(res_dir / dirname / file), str(dirpath)) - - -def _create_structure(tlobjects): - """ - Pre-create the required directory structure. - """ - types_ns = set() - method_ns = set() - for obj in tlobjects: - if obj.namespace: - if obj.is_function: - method_ns.add(obj.namespace) - else: - types_ns.add(obj.namespace) - - output_dir = pathlib.Path('.') - type_dir = output_dir / 'types' - type_dir.mkdir(exist_ok=True) - - cons_dir = output_dir / 'constructors' - cons_dir.mkdir(exist_ok=True) - for ns in types_ns: - (type_dir / ns).mkdir(exist_ok=True) - (cons_dir / ns).mkdir(exist_ok=True) - - meth_dir = output_dir / 'methods' - meth_dir.mkdir(exist_ok=True) - for ns in types_ns: - (meth_dir / ns).mkdir(exist_ok=True) - - -def generate_docs(tlobjects, methods_info, layer, input_res): - _create_structure(tlobjects) - _write_html_pages(tlobjects, methods_info, layer, input_res) - _copy_resources(input_res) diff --git a/telethon_generator/generators/errors.py b/telethon_generator/generators/errors.py deleted file mode 100644 index 386575be..00000000 --- a/telethon_generator/generators/errors.py +++ /dev/null @@ -1,60 +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 '.format(error.name, error.subclass)) - - if error.has_captures: - f.write('def __init__(self, request, capture=0):\n ' - ' self.request = request\n ') - f.write(' self.{} = int(capture)\n ' - .format(error.capture_name)) - else: - f.write('def __init__(self, request):\n ' - ' self.request = request\n ') - - f.write('super(Exception, self).__init__(' - '{}'.format(repr(error.description))) - - if error.has_captures: - f.write('.format({0}=self.{0})'.format(error.capture_name)) - - f.write(' + self._fmt_request(self.request))\n\n') - f.write(' def __reduce__(self):\n ') - if error.has_captures: - f.write('return type(self), (self.request, self.{})\n'.format(error.capture_name)) - else: - f.write('return type(self), (self.request,)\n') - - # Create the actual {CODE: ErrorClassName} dict once classes are defined - f.write('\n\nrpc_errors_dict = {\n') - for error in exact_match: - f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) - f.write('}\n\nrpc_errors_re = (\n') - for error in regex_match: - f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) - f.write(')\n') diff --git a/telethon_generator/generators/tlobject.py b/telethon_generator/generators/tlobject.py deleted file mode 100644 index 535ff38d..00000000 --- a/telethon_generator/generators/tlobject.py +++ /dev/null @@ -1,716 +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({})', - 'InputGroupCall': 'utils.get_input_group_call({})', -} - -NAMED_AUTO_CASTS = { - ('chat_id', 'int'): 'await client.get_peer_id({}, add_mark=False)' -} - -# Secret chats have a chat_id which may be negative. -# With the named auto-cast above, we would break it. -# However there are plenty of other legit requests -# with `chat_id:int` where it is useful. -# -# NOTE: This works because the auto-cast is not recursive. -# There are plenty of types that would break if we -# did recurse into them to resolve them. -NAMED_BLACKLIST = { - 'messages.discardEncryption' -} - -BASE_TYPES = ('string', 'bytes', 'int', 'long', 'int128', - 'int256', 'double', 'Bool', 'true', 'date') - - -def _write_modules( - out_dir, depth, kind, namespace_tlobjects, type_constructors): - # namespace_tlobjects: {'namespace', [TLObject]} - out_dir.mkdir(parents=True, exist_ok=True) - for ns, tlobjects in namespace_tlobjects.items(): - file = out_dir / '{}.py'.format(ns or '__init__') - with file.open('w') as f, SourceBuilder(f) as builder: - builder.writeln(AUTO_GEN_NOTICE) - - builder.writeln('from {}.tl.tlobject import TLObject', '.' * depth) - if kind != 'TLObject': - builder.writeln( - 'from {}.tl.tlobject import {}', '.' * depth, kind) - - builder.writeln('from typing import Optional, List, ' - 'Union, TYPE_CHECKING') - - # Add the relative imports to the namespaces, - # unless we already are in a namespace. - if not ns: - builder.writeln('from . import {}', ', '.join(sorted( - x for x in namespace_tlobjects.keys() if x - ))) - - # Import 'os' for those needing access to 'os.urandom()' - # Currently only 'random_id' needs 'os' to be imported, - # for all those TLObjects with arg.can_be_inferred. - builder.writeln('import os') - - # Import struct for the .__bytes__(self) serialization - builder.writeln('import struct') - - # Import datetime for type hinting - builder.writeln('from datetime import datetime') - - tlobjects.sort(key=lambda x: x.name) - - type_names = set() - type_defs = [] - - # Find all the types in this file and generate type definitions - # based on the types. The type definitions are written to the - # file at the end. - for t in tlobjects: - if not t.is_function: - type_name = t.result - if '.' in type_name: - type_name = type_name[type_name.rindex('.'):] - if type_name in type_names: - continue - type_names.add(type_name) - constructors = type_constructors[type_name] - if not constructors: - pass - elif len(constructors) == 1: - type_defs.append('Type{} = {}'.format( - type_name, constructors[0].class_name)) - else: - type_defs.append('Type{} = Union[{}]'.format( - type_name, ','.join(c.class_name - for c in constructors))) - - imports = {} - primitives = {'int', 'long', 'int128', 'int256', 'double', - 'string', 'date', 'bytes', 'Bool', 'true'} - # Find all the types in other files that are used in this file - # and generate the information required to import those types. - for t in tlobjects: - for arg in t.args: - name = arg.type - if not name or name in primitives: - continue - - import_space = '{}.tl.types'.format('.' * depth) - if '.' in name: - namespace = name.split('.')[0] - name = name.split('.')[1] - import_space += '.{}'.format(namespace) - - if name not in type_names: - type_names.add(name) - if name == 'date': - imports['datetime'] = ['datetime'] - continue - elif import_space not in imports: - imports[import_space] = set() - imports[import_space].add('Type{}'.format(name)) - - # Add imports required for type checking - if imports: - builder.writeln('if TYPE_CHECKING:') - for namespace, names in imports.items(): - builder.writeln('from {} import {}', - namespace, ', '.join(sorted(names))) - - builder.end_block() - - # Generate the class for every TLObject - for t in tlobjects: - _write_source_code(t, kind, builder, type_constructors) - builder.current_indent = 0 - - # Write the type definitions generated earlier. - builder.writeln() - for line in type_defs: - builder.writeln(line) - - -def _write_source_code(tlobject, kind, builder, type_constructors): - """ - Writes the source code corresponding to the given TLObject - by making use of the ``builder`` `SourceBuilder`. - - Additional information such as file path depth and - the ``Type: [Constructors]`` must be given for proper - importing and documentation strings. - """ - _write_class_init(tlobject, kind, type_constructors, builder) - _write_resolve(tlobject, builder) - _write_to_dict(tlobject, builder) - _write_to_bytes(tlobject, builder) - _write_from_reader(tlobject, builder) - _write_read_result(tlobject, builder) - - -def _write_class_init(tlobject, kind, type_constructors, builder): - builder.writeln() - builder.writeln() - builder.writeln('class {}({}):', tlobject.class_name, kind) - - # Class-level variable to store its Telegram's constructor ID - builder.writeln('CONSTRUCTOR_ID = {:#x}', tlobject.id) - builder.writeln('SUBCLASS_OF_ID = {:#x}', - crc32(tlobject.result.encode('ascii'))) - builder.writeln() - - # Convert the args to string parameters, those with flag having =None - args = ['{}: {}{}'.format( - a.name, a.type_hint(), '=None' if a.flag or a.can_be_inferred else '') - for a in tlobject.real_args - ] - - # Write the __init__ function if it has any argument - if not tlobject.real_args: - return - - if any(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.flag: - builder.writeln('if self.{}:', arg.name) - - if arg.is_vector: - builder.writeln('_tmp = []') - builder.writeln('for _x in self.{0}:', arg.name) - builder.writeln('_tmp.append({})', ac.format('_x')) - builder.end_block() - builder.writeln('self.{} = _tmp', arg.name) - else: - builder.writeln('self.{} = {}', arg.name, - ac.format('self.' + arg.name)) - - if arg.flag: - builder.end_block() - builder.end_block() - - -def _write_to_dict(tlobject, builder): - builder.writeln('def to_dict(self):') - builder.writeln('return {') - builder.current_indent += 1 - - builder.write("'_': '{}'", tlobject.class_name) - for arg in tlobject.real_args: - builder.writeln(',') - builder.write("'{}': ", arg.name) - if arg.type in BASE_TYPES: - if arg.is_vector: - builder.write('[] if self.{0} is None else self.{0}[:]', - arg.name) - else: - builder.write('self.{}', arg.name) - else: - if arg.is_vector: - builder.write( - '[] if self.{0} is None else [x.to_dict() ' - 'if isinstance(x, TLObject) else x for x in self.{0}]', - arg.name - ) - else: - builder.write( - 'self.{0}.to_dict() ' - 'if isinstance(self.{0}, TLObject) else self.{0}', - arg.name - ) - - builder.writeln() - builder.current_indent -= 1 - builder.writeln("}") - - builder.end_block() - - -def _write_to_bytes(tlobject, builder): - builder.writeln('def _bytes(self):') - - # Some objects require more than one flag parameter to be set - # at the same time. In this case, add an assertion. - repeated_args = defaultdict(list) - for arg in tlobject.args: - if arg.flag: - repeated_args[(arg.flag, arg.flag_index)].append(arg) - - for ra in repeated_args.values(): - if len(ra) > 1: - cnd1 = ('(self.{0} or self.{0} is not None)' - .format(a.name) for a in ra) - cnd2 = ('(self.{0} is None or self.{0} is False)' - .format(a.name) for a in ra) - builder.writeln( - "assert ({}) or ({}), '{} parameters must all " - "be False-y (like None) or all me True-y'", - ' and '.join(cnd1), ' and '.join(cnd2), - ', '.join(a.name for a in ra) - ) - - builder.writeln("return b''.join((") - builder.current_indent += 1 - - # First constructor code, we already know its bytes - builder.writeln('{},', repr(struct.pack('/Vector. - # If this weren't the case, we should check upper case after - # max(index('<'), index('.')) (and if it is, it's boxed, so return). - m = re.match(r'Vector<(int|long)>', tlobject.result) - if not m: - return - - builder.end_block() - builder.writeln('@staticmethod') - builder.writeln('def read_result(reader):') - builder.writeln('reader.read_int() # Vector ID') - builder.writeln('return [reader.read_{}() ' - 'for _ in range(reader.read_int())]', m.group(1)) - - -def _write_arg_to_bytes(builder, arg, tlobject, name=None): - """ - Writes the .__bytes__() code for the given argument - :param builder: The source code builder - :param arg: The argument to write - :param tlobject: The parent TLObject - :param name: The name of the argument. Defaults to "self.argname" - This argument is an option because it's required when - writing Vectors<> - """ - if arg.generic_definition: - return # Do nothing, this only specifies a later type - - if name is None: - name = 'self.{}'.format(arg.name) - - # The argument may be a flag, only write if it's not None AND - # if it's not a True type. - # True types are not actually sent, but instead only used to - # determine the flags. - if arg.flag: - if arg.type == 'true': - return # Exit, since True type is never written - elif arg.is_vector: - # Vector flags are special since they consist of 3 values, - # so we need an extra join here. Note that empty vector flags - # should NOT be sent either! - builder.write("b'' if {0} is None or {0} is False " - "else b''.join((", name) - elif 'Bool' == arg.type: - # `False` is a valid value for this type, so only check for `None`. - builder.write("b'' if {0} is None else (", name) - else: - builder.write("b'' if {0} is None or {0} is False " - "else (", name) - - if arg.is_vector: - if arg.use_vector_id: - # vector code, unsigned 0x1cb5c415 as little endian - builder.write(r"b'\x15\xc4\xb5\x1c',") - - builder.write("struct.pack('3.5 feature, so add another join. - builder.write("b''.join(") - - # Temporary disable .is_vector, not to enter this if again - # Also disable .flag since it's not needed per element - old_flag, arg.flag = arg.flag, None - arg.is_vector = False - _write_arg_to_bytes(builder, arg, tlobject, name='x') - arg.is_vector = True - arg.flag = old_flag - - builder.write(' for x in {})', name) - - elif arg.flag_indicator: - # Calculate the flags with those items which are not None - if not any(f.flag for f in tlobject.args): - # There's a flag indicator, but no flag arguments so it's 0 - builder.write(r"b'\0\0\0\0'") - else: - def fmt_flag_arg(a): - if a.type == 'Bool': - fmt = '(0 if {0} is None else {1})' - else: - fmt = '(0 if {0} is None or {0} is False else {1})' - return fmt.format('self.{}'.format(a.name), 1 << a.flag_index) - - builder.write("struct.pack(' - """ - - if arg.generic_definition: - return # Do nothing, this only specifies a later type - - # The argument may be a flag, only write that flag was given! - old_flag = None - if arg.flag: - # Treat 'true' flags as a special case, since they're true if - # they're set, and nothing else needs to actually be read. - if 'true' == arg.type: - builder.writeln('{} = bool({} & {})', - name, arg.flag, 1 << arg.flag_index) - return - - builder.writeln('if {} & {}:', arg.flag, 1 << arg.flag_index) - # Temporary disable .flag not to enter this if - # again when calling the method recursively - old_flag, arg.flag = arg.flag, None - - if arg.is_vector: - if arg.use_vector_id: - # We have to read the vector's constructor ID - builder.writeln("reader.read_int()") - - builder.writeln('{} = []', name) - builder.writeln('for _ in range(reader.read_int()):') - # Temporary disable .is_vector, not to enter this if again - arg.is_vector = False - _write_arg_read_code(builder, arg, tlobject, name='_x') - builder.writeln('{}.append(_x)', name) - arg.is_vector = True - - elif arg.flag_indicator: - # Read the flags, which will indicate what items we should read next - builder.writeln('{} = reader.read_int()', arg.name) - builder.writeln() - - elif 'int' == arg.type: - # User IDs are becoming larger than 2³¹ - 1, which would translate - # into reading a negative ID, which we would treat as a chat. So - # special case them to read unsigned. See https://t.me/BotNews/57. - if arg.name == 'user_id' or (arg.name == 'id' and tlobject.result == 'User'): - builder.writeln('{} = reader.read_int(signed=False)', name) - else: - builder.writeln('{} = reader.read_int()', name) - - elif 'long' == arg.type: - builder.writeln('{} = reader.read_long()', name) - - elif 'int128' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=128)', name) - - elif 'int256' == arg.type: - builder.writeln('{} = reader.read_large_int(bits=256)', name) - - elif 'double' == arg.type: - builder.writeln('{} = reader.read_double()', name) - - elif 'string' == arg.type: - builder.writeln('{} = reader.tgread_string()', name) - - elif 'Bool' == arg.type: - builder.writeln('{} = reader.tgread_bool()', name) - - elif 'true' == arg.type: - # Arbitrary not-None value, don't actually read "true" flags - builder.writeln('{} = True', name) - - elif 'bytes' == arg.type: - builder.writeln('{} = reader.tgread_bytes()', name) - - elif 'date' == arg.type: # Custom format - builder.writeln('{} = reader.tgread_date()', name) - - else: - # Else it may be a custom type - if not arg.skip_constructor_id: - builder.writeln('{} = reader.tgread_object()', name) - else: - # Import the correct type inline to avoid cyclic imports. - # There may be better solutions so that we can just access - # all the types before the files have been parsed, but I - # don't know of any. - sep_index = arg.type.find('.') - if sep_index == -1: - ns, t = '.', arg.type - else: - ns, t = '.' + arg.type[:sep_index], arg.type[sep_index+1:] - class_name = snake_to_camel_case(t) - - # There would be no need to import the type if we're in the - # file with the same namespace, but since it does no harm - # and we don't have information about such thing in the - # method we just ignore that case. - builder.writeln('from {} import {}', ns, class_name) - builder.writeln('{} = {}.from_reader(reader)', - name, class_name) - - # End vector and flag blocks if required (if we opened them before) - if arg.is_vector: - builder.end_block() - - if old_flag: - builder.current_indent -= 1 - builder.writeln('else:') - builder.writeln('{} = None', name) - builder.current_indent -= 1 - # Restore .flag - arg.flag = old_flag - - -def _write_all_tlobjects(tlobjects, layer, builder): - builder.writeln(AUTO_GEN_NOTICE) - builder.writeln() - - builder.writeln('from . import types, functions') - builder.writeln() - - # Create a constant variable to indicate which layer this is - builder.writeln('LAYER = {}', layer) - builder.writeln() - - # Then create the dictionary containing constructor_id: class - builder.writeln('tlobjects = {') - builder.current_indent += 1 - - # Fill the dictionary (0x1a2b3c4f: tl.full.type.path.Class) - for tlobject in tlobjects: - builder.write('{:#010x}: ', tlobject.id) - builder.write('functions' if tlobject.is_function else 'types') - - if tlobject.namespace: - builder.write('.{}', tlobject.namespace) - - builder.writeln('.{},', tlobject.class_name) - - builder.current_indent -= 1 - builder.writeln('}') - - -def generate_tlobjects(tlobjects, layer, import_depth, output_dir): - # Group everything by {namespace: [tlobjects]} to generate __init__.py - namespace_functions = defaultdict(list) - namespace_types = defaultdict(list) - - # Group {type: [constructors]} to generate the documentation - type_constructors = defaultdict(list) - for tlobject in tlobjects: - if tlobject.is_function: - namespace_functions[tlobject.namespace].append(tlobject) - else: - namespace_types[tlobject.namespace].append(tlobject) - type_constructors[tlobject.result].append(tlobject) - - _write_modules(output_dir / 'functions', import_depth, 'TLRequest', - namespace_functions, type_constructors) - _write_modules(output_dir / 'types', import_depth, 'TLObject', - namespace_types, type_constructors) - - filename = output_dir / 'alltlobjects.py' - with filename.open('w') as file: - with SourceBuilder(file) as builder: - _write_all_tlobjects(tlobjects, layer, builder) - - -def clean_tlobjects(output_dir): - for d in ('functions', 'types'): - d = output_dir / d - if d.is_dir(): - shutil.rmtree(str(d)) - - tl = output_dir / 'alltlobjects.py' - if tl.is_file(): - tl.unlink() diff --git a/telethon_generator/parsers/__init__.py b/telethon_generator/parsers/__init__.py deleted file mode 100644 index a8c9a7b7..00000000 --- a/telethon_generator/parsers/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index b049ef52..00000000 --- a/telethon_generator/parsers/errors.py +++ /dev/null @@ -1,85 +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') - ) - - if error_code.startswith('2'): - error_code = re.sub(r'2', 'TWO_', error_code, count=1) - - if re.match(r'\d+', error_code): - raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code)) - - return snake_to_camel_case( - error_code.replace('FIRSTNAME', 'FIRST_NAME')\ - .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error') - - -class Error: - def __init__(self, codes, name, description): - # TODO Some errors have the same name but different integer codes - # Should these be split into different files or doesn't really matter? - # Telegram isn't exactly consistent with returned errors anyway. - self.int_code = codes[0] - self.int_codes = codes - self.str_code = name - self.subclass = _get_class_name(codes[0]) - self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES - self.description = description - - self.has_captures = '_X' in name - if self.has_captures: - self.name = _get_class_name(name.replace('_X', '_')) - self.pattern = name.replace('_X', r'_(\d+)') - self.capture_name = re.search(r'{(\w+)}', description).group(1) - else: - self.name = _get_class_name(name) - self.pattern = name - self.capture_name = None - - -def parse_errors(csv_file): - """ - Parses the input CSV file with columns (name, error codes, description) - and yields `Error` instances as a result. - """ - with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, tup in enumerate(f, start=2): - try: - name, codes, description = tup - except ValueError: - raise ValueError('Columns count mismatch, unquoted comma in ' - 'desc? (line {})'.format(line)) from None - - try: - codes = [int(x) for x in codes.split()] or [400] - except ValueError: - raise ValueError('Not all codes are integers ' - '(line {})'.format(line)) from None - - yield Error([int(x) for x in codes], name, description) diff --git a/telethon_generator/parsers/methods.py b/telethon_generator/parsers/methods.py deleted file mode 100644 index 22f77b1e..00000000 --- a/telethon_generator/parsers/methods.py +++ /dev/null @@ -1,67 +0,0 @@ -import csv -import enum -import warnings - - -class Usability(enum.Enum): - UNKNOWN = 0 - USER = 1 - BOT = 2 - BOTH = 4 - - @property - def key(self): - return { - Usability.UNKNOWN: 'unknown', - Usability.USER: 'user', - Usability.BOT: 'bot', - Usability.BOTH: 'both', - }[self] - - -class MethodInfo: - def __init__(self, name, usability, errors, friendly): - self.name = name - self.errors = errors - self.friendly = friendly - try: - self.usability = { - 'unknown': Usability.UNKNOWN, - 'user': Usability.USER, - 'bot': Usability.BOT, - 'both': Usability.BOTH, - }[usability.lower()] - except KeyError: - raise ValueError('Usability must be either user, bot, both or ' - 'unknown, not {}'.format(usability)) from None - - -def parse_methods(csv_file, friendly_csv_file, errors_dict): - """ - Parses the input CSV file with columns (method, usability, errors) - and yields `MethodInfo` instances as a result. - """ - raw_to_friendly = {} - with friendly_csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for ns, friendly, raw_list in f: - for raw in raw_list.split(): - raw_to_friendly[raw] = (ns, friendly) - - with csv_file.open(newline='') as f: - f = csv.reader(f) - next(f, None) # header - for line, (method, usability, errors) in enumerate(f, start=2): - try: - errors = [errors_dict[x] for x in errors.split()] - except KeyError: - raise ValueError('Method {} references unknown errors {}' - .format(method, errors)) from None - - friendly = raw_to_friendly.pop(method, None) - yield MethodInfo(method, usability, errors, friendly) - - if raw_to_friendly: - warnings.warn('note: unknown raw methods in friendly mapping: {}' - .format(', '.join(raw_to_friendly))) diff --git a/telethon_generator/parsers/tlobject/__init__.py b/telethon_generator/parsers/tlobject/__init__.py deleted file mode 100644 index e1f432b7..00000000 --- a/telethon_generator/parsers/tlobject/__init__.py +++ /dev/null @@ -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 deleted file mode 100644 index 5aa0fc9a..00000000 --- a/telethon_generator/parsers/tlobject/parser.py +++ /dev/null @@ -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 deleted file mode 100644 index 9e1bee2a..00000000 --- a/telethon_generator/parsers/tlobject/tlarg.py +++ /dev/null @@ -1,246 +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)', - ('video', 'InputFile'): "client.upload_file('/path/to/file.mp4')", -} - -KNOWN_TYPED_EXAMPLES = { - 'int128': "int.from_bytes(os.urandom(16), 'big')", - 'bytes': "b'arbitrary\\x7f data \\xfa here'", - 'long': "-12398745604826", - 'string': "'some string here'", - 'int': '42', - 'date': 'datetime.datetime(2018, 6, 25)', - 'double': '7.13', - 'Bool': 'False', - 'true': 'True', - 'InputChatPhoto': "client.upload_file('/path/to/photo.jpg')", - 'InputFile': "client.upload_file('/path/to/file.jpg')", - 'InputPeer': "'username'" -} - -_fmt_strings(KNOWN_NAMED_EXAMPLES, KNOWN_TYPED_EXAMPLES) - -SYNONYMS = { - 'InputUser': 'InputPeer', - 'InputChannel': 'InputPeer', - 'InputDialogPeer': 'InputPeer', - 'InputNotifyPeer': 'InputPeer', - 'InputMessage': 'int' -} - -# These are flags that are cleaner to leave off -OMITTED_EXAMPLES = { - 'silent', - 'background', - 'clear_draft', - 'reply_to_msg_id', - 'random_id', - 'reply_markup', - 'entities', - 'embed_links', - 'hash', - 'min_id', - 'max_id', - 'add_offset', - 'grouped', - 'broadcast', - 'admins', - 'edit', - 'delete' -} - - -class TLArg: - def __init__(self, name, arg_type, generic_definition): - """ - Initializes a new .tl argument - :param name: The name of the .tl argument - :param arg_type: The type of the .tl argument - :param generic_definition: Is the argument a generic definition? - (i.e. {X:Type}) - """ - self.name = 'is_self' if name == 'self' else name - - # Default values - self.is_vector = False - self.flag = None # name of the flag to check if self is present - self.skip_constructor_id = False - self.flag_index = -1 # bit index of the flag to check if self is present - self.cls = None - - # Special case: some types can be inferred, which makes it - # less annoying to type. Currently the only type that can - # be inferred is if the name is 'random_id', to which a - # random ID will be assigned if left as None (the default) - self.can_be_inferred = name == 'random_id' - - # The type can be an indicator that other arguments will be flags - if arg_type == '#': - self.flag_indicator = True - self.type = None - self.is_generic = False - else: - self.flag_indicator = False - self.is_generic = arg_type.startswith('!') - # Strip the exclamation mark always to have only the name - self.type = arg_type.lstrip('!') - - # The type may be a flag (FLAGS.IDX?REAL_TYPE) - # FLAGS can be any name, but it should have appeared previously. - flag_match = re.match(r'(\w+).(\d+)\?([\w<>.]+)', self.type) - if flag_match: - self.flag = flag_match.group(1) - self.flag_index = int(flag_match.group(2)) - # Update the type to match the exact type, not the "flagged" one - self.type = flag_match.group(3) - - # Then check if the type is a Vector - vector_match = re.match(r'[Vv]ector<([\w\d.]+)>', self.type) - if vector_match: - self.is_vector = True - - # If the type's first letter is not uppercase, then - # it is a constructor and we use (read/write) its ID - # as pinpointed on issue #81. - self.use_vector_id = self.type[0] == 'V' - - # Update the type to match the one inside the vector - self.type = vector_match.group(1) - - # See use_vector_id. An example of such case is ipPort in - # help.configSpecial - if self.type.split('.')[-1][0].islower(): - self.skip_constructor_id = True - - # The name may contain "date" in it, if this is the case and - # the type is "int", we can safely assume that this should be - # treated as a "date" object. Note that this is not a valid - # Telegram object, but it's easier to work with - if self.type == 'int' and ( - re.search(r'(\b|_)(date|until|since)(\b|_)', name) or - name in ('expires', 'expires_at', 'was_online')): - self.type = 'date' - - self.generic_definition = generic_definition - - def type_hint(self): - cls = self.type - if '.' in cls: - cls = cls.split('.')[1] - result = { - 'int': 'int', - 'long': 'int', - 'int128': 'int', - 'int256': 'int', - 'double': 'float', - 'string': 'str', - 'date': 'Optional[datetime]', # None date = 0 timestamp - 'bytes': 'bytes', - 'Bool': 'bool', - 'true': 'bool', - }.get(cls, "'Type{}'".format(cls)) - if self.is_vector: - result = 'List[{}]'.format(result) - if self.flag and cls != 'date': - result = 'Optional[{}]'.format(result) - - return result - - def real_type(self): - # Find the real type representation by updating it as required - real_type = self.type - if self.flag_indicator: - real_type = '#' - - if self.is_vector: - if self.use_vector_id: - real_type = 'Vector<{}>'.format(real_type) - else: - real_type = 'vector<{}>'.format(real_type) - - if self.is_generic: - real_type = '!{}'.format(real_type) - - if self.flag: - real_type = '{}.{}?{}'.format(self.flag, self.flag_index, real_type) - - return real_type - - def __str__(self): - 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.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 deleted file mode 100644 index d30b1b4a..00000000 --- a/telethon_generator/parsers/tlobject/tlobject.py +++ /dev/null @@ -1,155 +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)) - - @property - def innermost_result(self): - index = self.result.find('<') - if index == -1: - return self.result - else: - return self.result[index + 1:-1] - - def sorted_args(self): - """Returns the arguments properly sorted and ready to plug-in - into a Python's method header (i.e., flags and those which - can be inferred will go last so they can default =None) - """ - return sorted(self.args, - key=lambda x: bool(x.flag) or x.can_be_inferred) - - def __repr__(self, ignore_id=False): - if self.id is None or ignore_id: - hex_id = '' - else: - hex_id = '#{:08x}'.format(self.id) - - if self.args: - args = ' ' + ' '.join([repr(arg) for arg in self.args]) - else: - args = '' - - return '{}{}{} = {}'.format(self.fullname, hex_id, args, self.result) - - def infer_id(self): - representation = self.__repr__(ignore_id=True) - representation = representation\ - .replace(':bytes ', ':string ')\ - .replace('?bytes ', '?string ')\ - .replace('<', ' ').replace('>', '')\ - .replace('{', '').replace('}', '') - - # Remove optional empty values (special-cased to the true type) - representation = re.sub( - r' \w+:\w+\.\d+\?true', - r'', - representation - ) - return zlib.crc32(representation.encode('ascii')) - - def to_dict(self): - return { - 'id': - str(struct.unpack('i', struct.pack('I', self.id))[0]), - 'method' if self.is_function else 'predicate': - self.fullname, - 'params': - [x.to_dict() for x in self.args if not x.generic_definition], - 'type': - self.result - } - - def is_good_example(self): - return not self.class_name.endswith('Empty') - - def as_example(self, f, indent=0): - f.write('functions' if self.is_function else 'types') - if self.namespace: - f.write('.') - f.write(self.namespace) - - f.write('.') - f.write(self.class_name) - f.write('(') - - args = [arg for arg in self.real_args if not arg.omit_example()] - if not args: - f.write(')') - return - - f.write('\n') - indent += 1 - remaining = len(args) - for arg in args: - remaining -= 1 - f.write(' ' * indent) - f.write(arg.name) - f.write('=') - if arg.is_vector: - f.write('[') - arg.as_example(f, indent) - if arg.is_vector: - f.write(']') - if remaining: - f.write(',') - f.write('\n') - - indent -= 1 - f.write(' ' * indent) - f.write(')') diff --git a/telethon_generator/sourcebuilder.py b/telethon_generator/sourcebuilder.py deleted file mode 100644 index 9fb61593..00000000 --- a/telethon_generator/sourcebuilder.py +++ /dev/null @@ -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/syncerrors.py b/telethon_generator/syncerrors.py deleted file mode 100644 index c91a5bc3..00000000 --- a/telethon_generator/syncerrors.py +++ /dev/null @@ -1,55 +0,0 @@ -# Should be fed with the JSON obtained from https://core.telegram.org/api/errors#error-database -import re -import csv -import sys -import json -from pathlib import Path - -sys.path.insert(0, '..') - -from telethon_generator.parsers.errors import parse_errors, Error -from telethon_generator.parsers.methods import parse_methods, MethodInfo - -ERRORS = Path('data/errors.csv') -METHODS = Path('data/methods.csv') -FRIENDLY = Path('data/friendly.csv') - - -def main(): - new_errors = [] - new_methods = [] - - self_errors = {e.str_code: e for e in parse_errors(ERRORS)} - self_methods = {m.name: m for m in parse_methods(METHODS, FRIENDLY, self_errors)} - - tg_data = json.load(sys.stdin) - - def get_desc(code): - return re.sub(r'\s*&\w+;\s*', '', (tg_data['descriptions'].get(code) or '').rstrip('.')) - - for int_code, errors in tg_data['errors'].items(): - int_code = int(int_code) # json does not support non-string keys - for code, methods in errors.items(): - if not re.match(r'\w+', code): - continue # skip, full code is unknown (contains asterisk or is multiple words) - str_code = code.replace('%d', 'X') - if error := self_errors.get(str_code): - error.int_codes.append(int_code) # de-duplicated once later - if not error.description: # prefer our descriptions - if not error.has_captures: # need descriptions with specific text if error has captures - error.description = get_desc(code) - else: - self_errors[str_code] = Error([int_code], str_code, get_desc(code)) - - new_errors.extend((e.str_code, ' '.join(map(str, sorted(set(e.int_codes)))), e.description) for e in self_errors.values()) - new_methods.extend((m.name, m.usability.key, ' '.join(sorted(e.str_code for e in m.errors))) for m in self_methods.values()) - - csv.register_dialect('plain', lineterminator='\n') - with ERRORS.open('w', encoding='utf-8', newline='') as fd: - csv.writer(fd, 'plain').writerows((('name', 'codes', 'description'), *sorted(new_errors))) - with METHODS.open('w', encoding='utf-8', newline='') as fd: - csv.writer(fd, 'plain').writerows((('method', 'usability', 'errors'), *sorted(new_methods))) - - -if __name__ == '__main__': - main() diff --git a/telethon_generator/utils.py b/telethon_generator/utils.py deleted file mode 100644 index 9889803f..00000000 --- a/telethon_generator/utils.py +++ /dev/null @@ -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 diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/readthedocs/__init__.py b/tests/readthedocs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/readthedocs/conftest.py b/tests/readthedocs/conftest.py deleted file mode 100644 index 9aad9580..00000000 --- a/tests/readthedocs/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -import pathlib - -import pytest - - -@pytest.fixture -def docs_dir(): - return pathlib.Path('readthedocs') diff --git a/tests/readthedocs/quick_references/__init__.py b/tests/readthedocs/quick_references/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/readthedocs/quick_references/test_client_reference.py b/tests/readthedocs/quick_references/test_client_reference.py deleted file mode 100644 index ad720d54..00000000 --- a/tests/readthedocs/quick_references/test_client_reference.py +++ /dev/null @@ -1,14 +0,0 @@ -import re - -from telethon import TelegramClient - - -def test_all_methods_present(docs_dir): - with (docs_dir / 'quick-references/client-reference.rst').open(encoding='utf-8') as fd: - present_methods = set(map(str.lstrip, re.findall(r'^ {4}\w+$', fd.read(), re.MULTILINE))) - - assert len(present_methods) > 0 - for name in dir(TelegramClient): - attr = getattr(TelegramClient, name) - if callable(attr) and not name.startswith('_'): - assert name in present_methods diff --git a/tests/telethon/__init__.py b/tests/telethon/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/client/__init__.py b/tests/telethon/client/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/client/test_messages.py b/tests/telethon/client/test_messages.py deleted file mode 100644 index d47fcf45..00000000 --- a/tests/telethon/client/test_messages.py +++ /dev/null @@ -1,40 +0,0 @@ -import inspect - -import pytest - -from telethon import TelegramClient - - -@pytest.mark.asyncio -async def test_send_message_with_file_forwards_args(): - arguments = {} - sentinel = object() - - for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters): - if name in {'self', 'entity', 'file'}: - continue # positional - - if name in {'message'}: - continue # renamed - - if name in {'link_preview'}: - continue # make no sense in send_file - - arguments[name] = value - - class MockedClient(TelegramClient): - # noinspection PyMissingConstructor - def __init__(self): - pass - - async def send_file(self, entity, file, **kwargs): - assert entity == 'a' - assert file == 'b' - for k, v in arguments.items(): - assert k in kwargs - assert kwargs[k] == v - - return sentinel - - client = MockedClient() - assert (await client.send_message('a', file='b', **arguments)) == sentinel diff --git a/tests/telethon/crypto/__init__.py b/tests/telethon/crypto/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/crypto/test_rsa.py b/tests/telethon/crypto/test_rsa.py deleted file mode 100644 index 251acd2c..00000000 --- a/tests/telethon/crypto/test_rsa.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Tests for `telethon.crypto.rsa`. -""" -import pytest - -from telethon.crypto import rsa - - -@pytest.fixture -def server_key_fp(): - """Factory to return a key, old if so chosen.""" - def _server_key_fp(old: bool): - for fp, data in rsa._server_keys.items(): - _, old_key = data - if old_key == old: - return fp - - return _server_key_fp - - -def test_encryption_inv_key(): - """Test for #1324.""" - assert rsa.encrypt("invalid", b"testdata") is None - - -def test_encryption_old_key(server_key_fp): - """Test for #1324.""" - assert rsa.encrypt(server_key_fp(old=True), b"testdata") is None - - -def test_encryption_allowed_old_key(server_key_fp): - data = rsa.encrypt(server_key_fp(old=True), b"testdata", use_old=True) - # We can't verify the data is actually valid because we don't have - # the decryption keys - assert data is not None and len(data) == 256 - - -def test_encryption_current_key(server_key_fp): - data = rsa.encrypt(server_key_fp(old=False), b"testdata") - # We can't verify the data is actually valid because we don't have - # the decryption keys - assert data is not None and len(data) == 256 diff --git a/tests/telethon/events/__init__.py b/tests/telethon/events/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/events/test_chataction.py b/tests/telethon/events/test_chataction.py deleted file mode 100644 index 24f60596..00000000 --- a/tests/telethon/events/test_chataction.py +++ /dev/null @@ -1,67 +0,0 @@ -import pytest - -from telethon import TelegramClient, events, types, utils - - -def get_client(): - return TelegramClient(None, 1, '1') - - -def get_user_456(): - return types.User( - id=456, - access_hash=789, - first_name='User 123' - ) - - -@pytest.mark.asyncio -async def test_get_input_users_no_action_message_no_entities(): - event = events.ChatAction.build(types.UpdateChatParticipantDelete( - chat_id=123, - user_id=456, - version=1 - )) - event._set_client(get_client()) - - assert await event.get_input_users() == [] - - -@pytest.mark.asyncio -async def test_get_input_users_no_action_message(): - user = get_user_456() - event = events.ChatAction.build(types.UpdateChatParticipantDelete( - chat_id=123, - user_id=456, - version=1 - )) - event._set_client(get_client()) - event._entities[user.id] = user - - assert await event.get_input_users() == [utils.get_input_peer(user)] - - -@pytest.mark.asyncio -async def test_get_users_no_action_message_no_entities(): - event = events.ChatAction.build(types.UpdateChatParticipantDelete( - chat_id=123, - user_id=456, - version=1 - )) - event._set_client(get_client()) - - assert await event.get_users() == [] - - -@pytest.mark.asyncio -async def test_get_users_no_action_message(): - user = get_user_456() - event = events.ChatAction.build(types.UpdateChatParticipantDelete( - chat_id=123, - user_id=456, - version=1 - )) - event._set_client(get_client()) - event._entities[user.id] = user - - assert await event.get_users() == [user] diff --git a/tests/telethon/extensions/__init__.py b/tests/telethon/extensions/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/extensions/test_html.py b/tests/telethon/extensions/test_html.py deleted file mode 100644 index 59d96e0d..00000000 --- a/tests/telethon/extensions/test_html.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Tests for `telethon.extensions.html`. -""" -from telethon.extensions import html -from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl - - -def test_entity_edges(): - """ - Test that entities at the edges (start and end) don't crash. - """ - text = 'Hello, world' - entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] - result = html.unparse(text, entities) - assert result == 'Hello, world' - - -def test_malformed_entities(): - """ - Test that malformed entity offsets from bad clients - don't crash and produce the expected results. - """ - text = '🏆Telegram Official Android Challenge is over🏆.' - entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] - result = html.unparse(text, entities) - assert result == '🏆Telegram Official Android Challenge is over🏆.' - - -def test_trailing_malformed_entities(): - """ - Similar to `test_malformed_entities`, but for the edge - case where the malformed entity offset is right at the end - (note the lack of a trailing dot in the text string). - """ - text = '🏆Telegram Official Android Challenge is over🏆' - entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] - result = html.unparse(text, entities) - assert result == '🏆Telegram Official Android Challenge is over🏆' - - -def test_entities_together(): - """ - Test that an entity followed immediately by a different one behaves well. - """ - original = '⚙️Settings' - stripped = '⚙️Settings' - - text, entities = html.parse(original) - assert text == stripped - assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] - - text = html.unparse(text, entities) - assert text == original - - -def test_offset_at_emoji(): - """ - Tests that an entity starting at a emoji preserves the emoji. - """ - text = 'Hi\n👉 See example' - entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] - parsed = 'Hi\n👉 See example' - - assert html.parse(parsed) == (text, entities) - assert html.unparse(text, entities) == parsed diff --git a/tests/telethon/extensions/test_markdown.py b/tests/telethon/extensions/test_markdown.py deleted file mode 100644 index bd78e4d8..00000000 --- a/tests/telethon/extensions/test_markdown.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Tests for `telethon.extensions.markdown`. -""" -from telethon.extensions import markdown -from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl - - -def test_entity_edges(): - """ - Test that entities at the edges (start and end) don't crash. - """ - text = 'Hello, world' - entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] - result = markdown.unparse(text, entities) - assert result == '**Hello**, **world**' - - -def test_malformed_entities(): - """ - Test that malformed entity offsets from bad clients - don't crash and produce the expected results. - """ - text = '🏆Telegram Official Android Challenge is over🏆.' - entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] - result = markdown.unparse(text, entities) - assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆." - - -def test_trailing_malformed_entities(): - """ - Similar to `test_malformed_entities`, but for the edge - case where the malformed entity offset is right at the end - (note the lack of a trailing dot in the text string). - """ - text = '🏆Telegram Official Android Challenge is over🏆' - entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] - result = markdown.unparse(text, entities) - assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆" - - -def test_entities_together(): - """ - Test that an entity followed immediately by a different one behaves well. - """ - original = '**⚙️**__Settings__' - stripped = '⚙️Settings' - - text, entities = markdown.parse(original) - assert text == stripped - assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] - - text = markdown.unparse(text, entities) - assert text == original - - -def test_offset_at_emoji(): - """ - Tests that an entity starting at a emoji preserves the emoji. - """ - text = 'Hi\n👉 See example' - entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] - parsed = '**Hi**\n__👉__ See **example**' - - assert markdown.parse(parsed) == (text, entities) - assert markdown.unparse(text, entities) == parsed diff --git a/tests/telethon/test_helpers.py b/tests/telethon/test_helpers.py deleted file mode 100644 index 47705ca2..00000000 --- a/tests/telethon/test_helpers.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -tests for telethon.helpers -""" - -from base64 import b64decode - -import pytest - -from telethon import helpers -from telethon.utils import get_inner_text -from telethon.tl.types import MessageEntityUnknown as Meu - - -def test_strip_text(): - text = ' text ' - text_stripped = 'text' - entities_before_and_after = ( - ([], []), - ([Meu(i, 0) for i in range(10)], []), # del '' - ([Meu(0, 0), Meu(0, 1), Meu(5, 1)], []), # del '', ' ', ' ' - ([Meu(0, 3)], [Meu(0, 2)]), # ' te' -> 'te' - ([Meu(3, 1)], [Meu(2, 1)]), # 'x' - ([Meu(3, 2)], [Meu(2, 2)]), # 'xt' - ([Meu(3, 3)], [Meu(2, 2)]), # 'xt ' -> 'xt' - ([Meu(0, 6)], [Meu(0, 4)]), # ' text ' -> 'text' - ) - for entities_before, entities_expected in entities_before_and_after: - entities_for_test = [Meu(meu.offset, meu.length) for meu in entities_before] # deep copy - text_after = helpers.strip_text(text, entities_for_test) - assert text_after == text_stripped - assert sorted((e.offset, e.length) for e in entities_for_test) \ - == sorted((e.offset, e.length) for e in entities_expected) - inner_text_before = get_inner_text(text, entities_before) - inner_text_before_stripped = [t.strip() for t in inner_text_before] - inner_text_after = get_inner_text(text_after, entities_for_test) - for t in inner_text_after: - assert t in inner_text_before_stripped - - -class TestSyncifyAsyncContext: - class NoopContextManager: - def __init__(self, loop): - self.count = 0 - self.loop = loop - - async def __aenter__(self): - self.count += 1 - return self - - async def __aexit__(self, exc_type, *args): - assert exc_type is None - self.count -= 1 - - __enter__ = helpers._sync_enter - __exit__ = helpers._sync_exit - - def test_sync_acontext(self, event_loop): - contm = self.NoopContextManager(event_loop) - assert contm.count == 0 - - with contm: - assert contm.count == 1 - - assert contm.count == 0 - - @pytest.mark.asyncio - async def test_async_acontext(self, event_loop): - contm = self.NoopContextManager(event_loop) - assert contm.count == 0 - - async with contm: - assert contm.count == 1 - - assert contm.count == 0 - - -def test_generate_key_data_from_nonce(): - gkdfn = helpers.generate_key_data_from_nonce - - key_expect = b64decode(b'NFwRFB8Knw/kAmvPWjtrQauWysHClVfQh0UOAaABqZA=') - nonce_expect = b64decode(b'1AgjhU9eDvJRjFik73bjR2zZEATzL/jLu9yodYfWEgA=') - assert gkdfn(123456789, 1234567) == (key_expect, nonce_expect) diff --git a/tests/telethon/test_pickle.py b/tests/telethon/test_pickle.py deleted file mode 100644 index 4854c66f..00000000 --- a/tests/telethon/test_pickle.py +++ /dev/null @@ -1,35 +0,0 @@ -import pickle - -from telethon.errors import RPCError, BadRequestError, FileIdInvalidError, NetworkMigrateError - - -def _assert_equality(error, unpickled_error): - assert error.code == unpickled_error.code - assert error.message == unpickled_error.message - assert type(error) == type(unpickled_error) - assert str(error) == str(unpickled_error) - - -def test_base_rpcerror_pickle(): - error = RPCError("request", "message", 123) - unpickled_error = pickle.loads(pickle.dumps(error)) - _assert_equality(error, unpickled_error) - - -def test_rpcerror_pickle(): - error = BadRequestError("request", "BAD_REQUEST", 400) - unpickled_error = pickle.loads(pickle.dumps(error)) - _assert_equality(error, unpickled_error) - - -def test_fancy_rpcerror_pickle(): - error = FileIdInvalidError("request") - unpickled_error = pickle.loads(pickle.dumps(error)) - _assert_equality(error, unpickled_error) - - -def test_fancy_rpcerror_capture_pickle(): - error = NetworkMigrateError(request="request", capture=5) - unpickled_error = pickle.loads(pickle.dumps(error)) - _assert_equality(error, unpickled_error) - assert error.new_dc == unpickled_error.new_dc diff --git a/tests/telethon/test_utils.py b/tests/telethon/test_utils.py deleted file mode 100644 index b0cfb33f..00000000 --- a/tests/telethon/test_utils.py +++ /dev/null @@ -1,57 +0,0 @@ -import io -import pathlib - -import pytest - -from telethon import utils -from telethon.tl.types import ( - MessageMediaGame, Game, PhotoEmpty -) - - -def test_game_input_media_memory_error(): - large_long = 2**62 - media = MessageMediaGame(Game( - id=large_long, # <- key to trigger `MemoryError` - access_hash=large_long, - short_name='short_name', - title='title', - description='description', - photo=PhotoEmpty(large_long), - )) - input_media = utils.get_input_media(media) - bytes(input_media) # <- shouldn't raise `MemoryError` - - -def test_private_get_extension(): - # Positive cases - png_header = bytes.fromhex('89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52') - png_buffer = io.BytesIO(png_header) - - class CustomFd: - def __init__(self, name): - self.name = name - - assert utils._get_extension('foo.bar.baz') == '.baz' - assert utils._get_extension(pathlib.Path('foo.bar.baz')) == '.baz' - assert utils._get_extension(png_header) == '.png' - assert utils._get_extension(png_buffer) == '.png' - assert utils._get_extension(png_buffer) == '.png' # make sure it did seek back - assert utils._get_extension(CustomFd('foo.bar.baz')) == '.baz' - - # Negative cases - null_header = bytes.fromhex('00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00') - null_buffer = io.BytesIO(null_header) - - empty_header = bytes() - empty_buffer = io.BytesIO(empty_header) - - assert utils._get_extension('foo') == '' - assert utils._get_extension(pathlib.Path('foo')) == '' - assert utils._get_extension(null_header) == '' - assert utils._get_extension(null_buffer) == '' - assert utils._get_extension(null_buffer) == '' # make sure it did seek back - assert utils._get_extension(empty_header) == '' - assert utils._get_extension(empty_buffer) == '' - assert utils._get_extension(empty_buffer) == '' # make sure it did seek back - assert utils._get_extension(CustomFd('foo')) == '' diff --git a/tests/telethon/tl/__init__.py b/tests/telethon/tl/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/telethon/tl/test_serialization.py b/tests/telethon/tl/test_serialization.py deleted file mode 100644 index 7dbb067d..00000000 --- a/tests/telethon/tl/test_serialization.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest - -from telethon.tl import types, functions - - -def test_nested_invalid_serialization(): - large_long = 2**62 - request = functions.account.SetPrivacyRequest( - key=types.InputPrivacyKeyChatInvite(), - rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])] - ) - with pytest.raises(TypeError): - bytes(request) diff --git a/update-docs.sh b/update-docs.sh deleted file mode 100644 index d3f6c2b0..00000000 --- a/update-docs.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -e -python setup.py gen docs -rm -rf /tmp/docs -mv docs/ /tmp/docs -git checkout gh-pages -# there's probably better ways but we know none has spaces -rm -rf $(ls /tmp/docs) -mv /tmp/docs/* . -git add constructors/ types/ methods/ index.html js/search.js css/ img/ -git commit --amend -m "Update documentation" -git push --force -git checkout v1