├── tests ├── __init__.py ├── telethon │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ └── test_messages.py │ ├── crypto │ │ ├── __init__.py │ │ └── test_rsa.py │ ├── events │ │ ├── __init__.py │ │ └── test_chataction.py │ ├── tl │ │ ├── __init__.py │ │ └── test_serialization.py │ ├── extensions │ │ ├── __init__.py │ │ ├── test_markdown.py │ │ └── test_html.py │ ├── test_pickle.py │ ├── test_utils.py │ └── test_helpers.py └── readthedocs │ ├── __init__.py │ ├── quick_references │ ├── __init__.py │ └── test_client_reference.py │ └── conftest.py ├── requirements.txt ├── telethon_generator ├── __init__.py ├── parsers │ ├── tlobject │ │ └── __init__.py │ ├── __init__.py │ ├── methods.py │ └── errors.py ├── generators │ ├── __init__.py │ └── errors.py ├── utils.py ├── data │ ├── html │ │ ├── img │ │ │ └── arrow.svg │ │ ├── 404.html │ │ └── css │ │ │ ├── docs.light.css │ │ │ └── docs.dark.css │ └── friendly.csv ├── sourcebuilder.py └── syncerrors.py ├── telethon ├── custom.py ├── types.py ├── functions.py ├── tl │ ├── __init__.py │ ├── custom │ │ ├── inputsizedfile.py │ │ ├── __init__.py │ │ ├── forward.py │ │ ├── inlineresults.py │ │ ├── sendergetter.py │ │ └── qrlogin.py │ ├── patched │ │ └── __init__.py │ └── core │ │ ├── __init__.py │ │ ├── tlmessage.py │ │ ├── rpcresult.py │ │ ├── gzippacked.py │ │ └── messagecontainer.py ├── version.py ├── sessions │ ├── __init__.py │ └── string.py ├── _updates │ ├── __init__.py │ └── entitycache.py ├── extensions │ ├── __init__.py │ └── messagepacker.py ├── crypto │ ├── __init__.py │ ├── aesctr.py │ ├── factorization.py │ ├── authkey.py │ ├── aes.py │ └── cdndecrypter.py ├── __init__.py ├── network │ ├── connection │ │ ├── __init__.py │ │ ├── tcpabridged.py │ │ ├── http.py │ │ ├── tcpintermediate.py │ │ ├── tcpfull.py │ │ └── tcpobfuscated.py │ ├── __init__.py │ ├── requeststate.py │ └── mtprotoplainsender.py ├── client │ ├── telegramclient.py │ ├── __init__.py │ ├── bots.py │ └── buttons.py ├── errors │ ├── __init__.py │ └── rpcbaseerrors.py ├── events │ ├── raw.py │ ├── messageedited.py │ └── messagedeleted.py ├── hints.py └── sync.py ├── readthedocs ├── requirements.txt ├── modules │ ├── helpers.rst │ ├── utils.rst │ ├── errors.rst │ ├── sessions.rst │ ├── network.rst │ ├── events.rst │ ├── client.rst │ └── custom.rst ├── examples │ ├── working-with-messages.rst │ ├── word-of-warning.rst │ ├── users.rst │ └── chats-and-channels.rst ├── Makefile ├── developing │ ├── telegram-api-in-other-languages.rst │ ├── tips-for-porting-the-project.rst │ ├── coding-style.rst │ ├── philosophy.rst │ ├── understanding-the-type-language.rst │ ├── test-servers.rst │ ├── project-structure.rst │ └── testing.rst ├── make.bat ├── basic │ ├── next-steps.rst │ ├── installation.rst │ └── quick-start.rst ├── custom_roles.py ├── concepts │ └── strings.rst ├── index.rst └── quick-references │ └── client-reference.rst ├── dev-requirements.txt ├── optional-requirements.txt ├── telethon_examples ├── screenshot-gui.jpg ├── print_updates.py ├── print_messages.py ├── assistant.py └── replier.py ├── .coveragerc ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation-issue.yml │ ├── feature-request.yml │ └── bug-report.yml └── workflows.disabled │ └── python.yml ├── .readthedocs.yaml ├── logo.svg ├── update-docs.sh ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── pyproject.toml └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/telethon/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/readthedocs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/telethon/client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/telethon/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/telethon/events/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/telethon/tl/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyaes 2 | rsa 3 | -------------------------------------------------------------------------------- /telethon_generator/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/telethon/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telethon/custom.py: -------------------------------------------------------------------------------- 1 | from .tl.custom import * 2 | -------------------------------------------------------------------------------- /telethon/types.py: -------------------------------------------------------------------------------- 1 | from .tl.types import * 2 | -------------------------------------------------------------------------------- /tests/readthedocs/quick_references/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /telethon/functions.py: -------------------------------------------------------------------------------- 1 | from .tl.functions import * 2 | -------------------------------------------------------------------------------- /readthedocs/requirements.txt: -------------------------------------------------------------------------------- 1 | ./ 2 | sphinx-rtd-theme~=1.3.0 3 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-asyncio 4 | -------------------------------------------------------------------------------- /telethon/tl/__init__.py: -------------------------------------------------------------------------------- 1 | from .tlobject import TLObject, TLRequest 2 | -------------------------------------------------------------------------------- /optional-requirements.txt: -------------------------------------------------------------------------------- 1 | cryptg 2 | pysocks 3 | python-socks[asyncio] 4 | hachoir 5 | pillow 6 | -------------------------------------------------------------------------------- /telethon_examples/screenshot-gui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pibico/Telethon/v1/telethon_examples/screenshot-gui.jpg -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | parallel = true 4 | source = 5 | telethon 6 | 7 | [report] 8 | precision = 2 9 | -------------------------------------------------------------------------------- /telethon/version.py: -------------------------------------------------------------------------------- 1 | # Versions should comply with PEP440. 2 | # This line is parsed in setup.py: 3 | __version__ = '1.40.0' 4 | -------------------------------------------------------------------------------- /telethon_generator/parsers/tlobject/__init__.py: -------------------------------------------------------------------------------- 1 | from .tlarg import TLArg 2 | from .tlobject import TLObject 3 | from .parser import parse_tl, find_layer 4 | -------------------------------------------------------------------------------- /tests/readthedocs/conftest.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def docs_dir(): 8 | return pathlib.Path('readthedocs') 9 | -------------------------------------------------------------------------------- /readthedocs/modules/helpers.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Helpers 3 | ======= 4 | 5 | .. automodule:: telethon.helpers 6 | :members: 7 | :undoc-members: 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /telethon/sessions/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract import Session 2 | from .memory import MemorySession 3 | from .sqlite import SQLiteSession 4 | from .string import StringSession 5 | -------------------------------------------------------------------------------- /telethon_generator/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import generate_errors 2 | from .tlobject import generate_tlobjects, clean_tlobjects 3 | from .docs import generate_docs 4 | -------------------------------------------------------------------------------- /telethon_generator/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .errors import Error, parse_errors 2 | from .methods import MethodInfo, Usability, parse_methods 3 | from .tlobject import TLObject, parse_tl, find_layer 4 | -------------------------------------------------------------------------------- /telethon/_updates/__init__.py: -------------------------------------------------------------------------------- 1 | from .entitycache import EntityCache 2 | from .messagebox import MessageBox, GapError, PrematureEndReason 3 | from .session import SessionState, ChannelState, Entity, EntityType 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /readthedocs/modules/utils.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-utils: 2 | 3 | ========= 4 | Utilities 5 | ========= 6 | 7 | These are the utilities that the library has to offer. 8 | 9 | .. automodule:: telethon.utils 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | -------------------------------------------------------------------------------- /telethon/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Several extensions Python is missing, such as a proper class to handle a TCP 3 | communication with support for cancelling the operation, and a utility class 4 | to read arbitrary binary data in a more comfortable way, with int/strings/etc. 5 | """ 6 | from .binaryreader import BinaryReader 7 | -------------------------------------------------------------------------------- /telethon_generator/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def snake_to_camel_case(name, suffix=None): 5 | # Courtesy of http://stackoverflow.com/a/31531797/4759433 6 | result = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name) 7 | result = result[:1].upper() + result[1:].replace('_', '') 8 | return result + suffix if suffix else result 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "3.11" 8 | 9 | sphinx: 10 | configuration: readthedocs/conf.py 11 | 12 | formats: 13 | - pdf 14 | - epub 15 | 16 | python: 17 | install: 18 | - requirements: readthedocs/requirements.txt 19 | -------------------------------------------------------------------------------- /telethon/tl/custom/inputsizedfile.py: -------------------------------------------------------------------------------- 1 | from ..types import InputFile 2 | 3 | 4 | class InputSizedFile(InputFile): 5 | """InputFile class with two extra parameters: md5 (digest) and size""" 6 | def __init__(self, id_, parts, name, md5, size): 7 | super().__init__(id_, parts, name, md5.hexdigest()) 8 | self.md5 = md5.digest() 9 | self.size = size 10 | -------------------------------------------------------------------------------- /telethon/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains several utilities regarding cryptographic purposes, 3 | such as the AES IGE mode used by Telegram, the authorization key bound with 4 | their data centers, and so on. 5 | """ 6 | from .aes import AES 7 | from .aesctr import AESModeCTR 8 | from .authkey import AuthKey 9 | from .factorization import Factorization 10 | from .cdndecrypter import CdnDecrypter 11 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /update-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | python setup.py gen docs 5 | rm -rf /tmp/docs 6 | mv docs/ /tmp/docs 7 | git checkout gh-pages 8 | # there's probably better ways but we know none has spaces 9 | rm -rf $(ls /tmp/docs) 10 | mv /tmp/docs/* . 11 | git add constructors/ types/ methods/ index.html js/search.js css/ img/ 12 | git commit --amend -m "Update documentation" 13 | git push --force 14 | git checkout v1 15 | -------------------------------------------------------------------------------- /tests/telethon/tl/test_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from telethon.tl import types, functions 4 | 5 | 6 | def test_nested_invalid_serialization(): 7 | large_long = 2**62 8 | request = functions.account.SetPrivacyRequest( 9 | key=types.InputPrivacyKeyChatInvite(), 10 | rules=[types.InputPrivacyValueDisallowUsers(users=[large_long])] 11 | ) 12 | with pytest.raises(TypeError): 13 | bytes(request) 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated code 2 | /telethon/tl/functions/ 3 | /telethon/tl/types/ 4 | /telethon/tl/alltlobjects.py 5 | /telethon/errors/rpcerrorlist.py 6 | 7 | # User session 8 | *.session 9 | /usermedia/ 10 | 11 | # Builds and testing 12 | __pycache__/ 13 | /dist/ 14 | /build/ 15 | /*.egg-info/ 16 | /readthedocs/_build/ 17 | /.tox/ 18 | 19 | # API reference docs 20 | /docs/ 21 | 22 | # File used to manually test new changes, contains sensitive data 23 | /example.py 24 | -------------------------------------------------------------------------------- /telethon/__init__.py: -------------------------------------------------------------------------------- 1 | from .client.telegramclient import TelegramClient 2 | from .network import connection 3 | from .tl.custom import Button 4 | from .tl import patched as _ # import for its side-effects 5 | from . import version, events, utils, errors, types, functions, custom 6 | 7 | __version__ = version.__version__ 8 | 9 | __all__ = [ 10 | 'TelegramClient', 'Button', 11 | 'types', 'functions', 'custom', 'errors', 12 | 'events', 'utils', 'connection' 13 | ] 14 | -------------------------------------------------------------------------------- /telethon/network/connection/__init__.py: -------------------------------------------------------------------------------- 1 | from .connection import Connection 2 | from .tcpfull import ConnectionTcpFull 3 | from .tcpintermediate import ConnectionTcpIntermediate 4 | from .tcpabridged import ConnectionTcpAbridged 5 | from .tcpobfuscated import ConnectionTcpObfuscated 6 | from .tcpmtproxy import ( 7 | TcpMTProxy, 8 | ConnectionTcpMTProxyAbridged, 9 | ConnectionTcpMTProxyIntermediate, 10 | ConnectionTcpMTProxyRandomizedIntermediate 11 | ) 12 | from .http import ConnectionHttp 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask questions in StackOverflow 4 | url: https://stackoverflow.com/questions/ask?tags=telethon 5 | about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed 6 | - name: Find about updates and our Telegram groups 7 | url: https://t.me/s/TelethonUpdates 8 | about: Be notified of updates, chat with other people about the library or ask questions in these groups 9 | -------------------------------------------------------------------------------- /telethon/client/telegramclient.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, 3 | BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, 4 | MessageParseMethods, UserMethods, TelegramBaseClient 5 | ) 6 | 7 | 8 | class TelegramClient( 9 | AccountMethods, AuthMethods, DownloadMethods, DialogMethods, ChatMethods, 10 | BotMethods, MessageMethods, UploadMethods, ButtonMethods, UpdateMethods, 11 | MessageParseMethods, UserMethods, TelegramBaseClient 12 | ): 13 | pass 14 | -------------------------------------------------------------------------------- /readthedocs/examples/working-with-messages.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Working with messages 3 | ===================== 4 | 5 | .. note:: 6 | 7 | These examples assume you have read :ref:`full-api`. 8 | 9 | This section has been `moved to the wiki`_, where it can be easily edited as new 10 | features arrive and the API changes. Please refer to the linked page to learn how 11 | to send spoilers, custom emoji, stickers, react to messages, and more things. 12 | 13 | .. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages 14 | -------------------------------------------------------------------------------- /readthedocs/modules/errors.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-errors: 2 | 3 | ========== 4 | API Errors 5 | ========== 6 | 7 | These are the base errors that Telegram's API may raise. 8 | 9 | See :ref:`rpc-errors` for a more in-depth explanation on how to handle all 10 | known possible errors and learning to determine what a method may raise. 11 | 12 | .. automodule:: telethon.errors.common 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | .. automodule:: telethon.errors.rpcbaseerrors 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | -------------------------------------------------------------------------------- /telethon/tl/custom/__init__.py: -------------------------------------------------------------------------------- 1 | from .adminlogevent import AdminLogEvent 2 | from .draft import Draft 3 | from .dialog import Dialog 4 | from .inputsizedfile import InputSizedFile 5 | from .messagebutton import MessageButton 6 | from .forward import Forward 7 | from .message import Message 8 | from .button import Button 9 | from .inlinebuilder import InlineBuilder 10 | from .inlineresult import InlineResult 11 | from .inlineresults import InlineResults 12 | from .conversation import Conversation 13 | from .qrlogin import QRLogin 14 | from .participantpermissions import ParticipantPermissions 15 | -------------------------------------------------------------------------------- /tests/readthedocs/quick_references/test_client_reference.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from telethon import TelegramClient 4 | 5 | 6 | def test_all_methods_present(docs_dir): 7 | with (docs_dir / 'quick-references/client-reference.rst').open(encoding='utf-8') as fd: 8 | present_methods = set(map(str.lstrip, re.findall(r'^ {4}\w+$', fd.read(), re.MULTILINE))) 9 | 10 | assert len(present_methods) > 0 11 | for name in dir(TelegramClient): 12 | attr = getattr(TelegramClient, name) 13 | if callable(attr) and not name.startswith('_') and name != 'sign_up': 14 | assert name in present_methods 15 | -------------------------------------------------------------------------------- /telethon/network/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains several classes regarding network, low level connection 3 | with Telegram's servers and the protocol used (TCP full, abridged, etc.). 4 | """ 5 | from .mtprotoplainsender import MTProtoPlainSender 6 | from .authenticator import do_authentication 7 | from .mtprotosender import MTProtoSender 8 | from .connection import ( 9 | Connection, 10 | ConnectionTcpFull, ConnectionTcpIntermediate, ConnectionTcpAbridged, 11 | ConnectionTcpObfuscated, ConnectionTcpMTProxyAbridged, 12 | ConnectionTcpMTProxyIntermediate, 13 | ConnectionTcpMTProxyRandomizedIntermediate, ConnectionHttp, TcpMTProxy 14 | ) 15 | -------------------------------------------------------------------------------- /telethon/tl/patched/__init__.py: -------------------------------------------------------------------------------- 1 | from .. import types, alltlobjects 2 | from ..custom.message import Message as _Message 3 | 4 | class MessageEmpty(_Message, types.MessageEmpty): 5 | pass 6 | 7 | types.MessageEmpty = MessageEmpty 8 | alltlobjects.tlobjects[MessageEmpty.CONSTRUCTOR_ID] = MessageEmpty 9 | 10 | class MessageService(_Message, types.MessageService): 11 | pass 12 | 13 | types.MessageService = MessageService 14 | alltlobjects.tlobjects[MessageService.CONSTRUCTOR_ID] = MessageService 15 | 16 | class Message(_Message, types.Message): 17 | pass 18 | 19 | types.Message = Message 20 | alltlobjects.tlobjects[Message.CONSTRUCTOR_ID] = Message 21 | -------------------------------------------------------------------------------- /readthedocs/modules/sessions.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-sessions: 2 | 3 | ======== 4 | Sessions 5 | ======== 6 | 7 | These are the different built-in session storage that you may subclass. 8 | 9 | .. automodule:: telethon.sessions.abstract 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | 14 | .. automodule:: telethon.sessions.memory 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | 19 | .. automodule:: telethon.sessions.sqlite 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | .. automodule:: telethon.sessions.string 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | -------------------------------------------------------------------------------- /readthedocs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Telethon 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /readthedocs/developing/telegram-api-in-other-languages.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | Telegram API in Other Languages 3 | =============================== 4 | 5 | Telethon was made for **Python**, and it has inspired other libraries such as 6 | `gramjs `__ (JavaScript) and `grammers 7 | `__ (Rust). But there is a lot more beyond 8 | those, made independently by different developers. 9 | 10 | If you're looking for something like Telethon but in a different programming 11 | language, head over to `Telegram API in Other Languages in the official wiki 12 | `__ 13 | for a (mostly) up-to-date list. 14 | -------------------------------------------------------------------------------- /telethon/network/requeststate.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | 4 | class RequestState: 5 | """ 6 | This request state holds several information relevant to sent messages, 7 | in particular the message ID assigned to the request, the container ID 8 | it belongs to, the request itself, the request as bytes, and the future 9 | result that will eventually be resolved. 10 | """ 11 | __slots__ = ('container_id', 'msg_id', 'request', 'data', 'future', 'after') 12 | 13 | def __init__(self, request, after=None): 14 | self.container_id = None 15 | self.msg_id = None 16 | self.request = request 17 | self.data = bytes(request) 18 | self.future = asyncio.Future() 19 | self.after = after 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: 7539d8bd1a00a3c1bfd34cdb606d3a6372e83469 3 | hooks: 4 | - id: check-added-large-files 5 | - id: check-case-conflict 6 | - id: check-merge-conflict 7 | - id: check-symlinks 8 | - id: check-yaml 9 | - id: double-quote-string-fixer 10 | - id: end-of-file-fixer 11 | - id: name-tests-test 12 | - id: trailing-whitespace 13 | - repo: git://github.com/pre-commit/mirrors-yapf 14 | sha: v0.11.1 15 | hooks: 16 | - id: yapf 17 | - repo: git://github.com/FalconSocial/pre-commit-python-sorter 18 | sha: 1.0.4 19 | hooks: 20 | - id: python-import-sorter 21 | args: 22 | - --silent-overwrite 23 | -------------------------------------------------------------------------------- /.github/workflows.disabled/python.yml: -------------------------------------------------------------------------------- 1 | name: Python Library 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.5", "3.6", "3.7", "3.8"] 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Set up env 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install tox 22 | - name: Lint with flake8 23 | run: | 24 | tox -e flake 25 | - name: Test with pytest 26 | run: | 27 | # use "py", which is the default python version 28 | tox -e py 29 | -------------------------------------------------------------------------------- /readthedocs/developing/tips-for-porting-the-project.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Tips for Porting the Project 3 | ============================ 4 | 5 | 6 | If you're going to use the code on this repository to guide you, please 7 | be kind and don't forget to mention it helped you! 8 | 9 | You should start by reading the source code on the `first 10 | release `__ of 11 | the project, and start creating a ``MTProtoSender``. Once this is made, 12 | you should write by hand the code to authenticate on the Telegram's 13 | server, which are some steps required to get the key required to talk to 14 | them. Save it somewhere! Then, simply mimic, or reinvent other parts of 15 | the code, and it will be ready to go within a few days. 16 | 17 | Good luck! 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-issue.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Issue 2 | description: Report a problem with the documentation. 3 | labels: [documentation] 4 | body: 5 | 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Describe the problem in detail. 11 | placeholder: This part is unclear... 12 | 13 | - type: checkboxes 14 | id: checklist 15 | attributes: 16 | label: Checklist 17 | description: Read this carefully, we will close and ignore your issue if you skimmed through this. 18 | options: 19 | - label: This is a documentation problem, not a question or a bug report. 20 | required: true 21 | - label: I have searched for this issue before posting it and there isn't a duplicate. 22 | required: true 23 | -------------------------------------------------------------------------------- /readthedocs/examples/word-of-warning.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | A Word of Warning 3 | ================= 4 | 5 | Full API is **not** how you are intended to use the library. You **should** 6 | always prefer the :ref:`client-ref`. However, not everything is implemented 7 | as a friendly method, so full API is your last resort. 8 | 9 | If you select a method in :ref:`client-ref`, you will most likely find an 10 | example for that method. This is how you are intended to use the library. 11 | 12 | Full API **will** break between different minor versions of the library, 13 | since Telegram changes very often. The friendly methods will be kept 14 | compatible between major versions. 15 | 16 | If you need to see real-world examples, please refer to the 17 | `wiki page of projects using Telethon `__. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest ideas, changes or other enhancements for the library. 3 | labels: [enhancement] 4 | body: 5 | 6 | - type: textarea 7 | id: feature-description 8 | attributes: 9 | label: Describe your suggested feature 10 | description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works? 11 | placeholder: "It should work like this..." 12 | validations: 13 | required: true 14 | 15 | - type: checkboxes 16 | id: checklist 17 | attributes: 18 | label: Checklist 19 | description: Read this carefully, we will close and ignore your issue if you skimmed through this. 20 | options: 21 | - label: I have searched for this issue before posting it and there isn't a duplicate. 22 | required: true 23 | -------------------------------------------------------------------------------- /readthedocs/modules/network.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-network: 2 | 3 | ================ 4 | Connection Modes 5 | ================ 6 | 7 | The only part about network that you should worry about are 8 | the different connection modes, which are the following: 9 | 10 | .. automodule:: telethon.network.connection.tcpfull 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | .. automodule:: telethon.network.connection.tcpabridged 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | .. automodule:: telethon.network.connection.tcpintermediate 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | .. automodule:: telethon.network.connection.tcpobfuscated 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | .. automodule:: telethon.network.connection.http 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | -------------------------------------------------------------------------------- /readthedocs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Telethon 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /readthedocs/developing/coding-style.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Coding Style 3 | ============ 4 | 5 | 6 | Basically, make it **readable**, while keeping the style similar to the 7 | code of whatever file you're working on. 8 | 9 | Also note that not everyone has 4K screens for their primary monitors, 10 | so please try to stick to the 80-columns limit. This makes it easy to 11 | ``git diff`` changes from a terminal before committing changes. If the 12 | line has to be long, please don't exceed 120 characters. 13 | 14 | For the commit messages, please make them *explanatory*. Not only 15 | they're helpful to troubleshoot when certain issues could have been 16 | introduced, but they're also used to construct the change log once a new 17 | version is ready. 18 | 19 | If you don't know enough Python, I strongly recommend reading `Dive Into 20 | Python 3 `__, available online for 21 | free. For instance, remember to do ``if x is None`` or 22 | ``if x is not None`` instead ``if x == None``! 23 | -------------------------------------------------------------------------------- /telethon/network/connection/tcpabridged.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from .connection import Connection, PacketCodec 4 | 5 | 6 | class AbridgedPacketCodec(PacketCodec): 7 | tag = b'\xef' 8 | obfuscate_tag = b'\xef\xef\xef\xef' 9 | 10 | def encode_packet(self, data): 11 | length = len(data) >> 2 12 | if length < 127: 13 | length = struct.pack('B', length) 14 | else: 15 | length = b'\x7f' + int.to_bytes(length, 3, 'little') 16 | return length + data 17 | 18 | async def read_packet(self, reader): 19 | length = struct.unpack('= 127: 21 | length = struct.unpack( 22 | '`` and ``Vector``). 17 | 3. Those bytes may be gzipped data, which needs to be treated early. 18 | """ 19 | from .tlmessage import TLMessage 20 | from .gzippacked import GzipPacked 21 | from .messagecontainer import MessageContainer 22 | from .rpcresult import RpcResult 23 | 24 | core_objects = {x.CONSTRUCTOR_ID: x for x in ( 25 | GzipPacked, MessageContainer, RpcResult 26 | )} 27 | -------------------------------------------------------------------------------- /readthedocs/developing/philosophy.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Philosophy 3 | ========== 4 | 5 | 6 | The intention of the library is to have an existing MTProto library 7 | existing with hardly any dependencies (indeed, wherever Python is 8 | available, you can run this library). 9 | 10 | Being written in Python means that performance will be nowhere close to 11 | other implementations written in, for instance, Java, C++, Rust, or 12 | pretty much any other compiled language. However, the library turns out 13 | to actually be pretty decent for common operations such as sending 14 | messages, receiving updates, or other scripting. Uploading files may be 15 | notably slower, but if you would like to contribute, pull requests are 16 | appreciated! 17 | 18 | If ``libssl`` is available on your system, the library will make use of 19 | it to speed up some critical parts such as encrypting and decrypting the 20 | messages. Files will notably be sent and downloaded faster. 21 | 22 | The main focus is to keep everything clean and simple, for everyone to 23 | understand how working with MTProto and Telegram works. Don't be afraid 24 | to read the source, the code won't bite you! It may prove useful when 25 | using the library on your own use cases. 26 | -------------------------------------------------------------------------------- /telethon/tl/core/tlmessage.py: -------------------------------------------------------------------------------- 1 | from .. import TLObject 2 | 3 | 4 | class TLMessage(TLObject): 5 | """ 6 | https://core.telegram.org/mtproto/service_messages#simple-container. 7 | 8 | Messages are what's ultimately sent to Telegram: 9 | message msg_id:long seqno:int bytes:int body:bytes = Message; 10 | 11 | Each message has its own unique identifier, and the body is simply 12 | the serialized request that should be executed on the server, or 13 | the response object from Telegram. Since the body is always a valid 14 | object, it makes sense to store the object and not the bytes to 15 | ease working with them. 16 | 17 | There is no need to add serializing logic here since that can be 18 | inlined and is unlikely to change. Thus these are only needed to 19 | encapsulate responses. 20 | """ 21 | SIZE_OVERHEAD = 12 22 | 23 | def __init__(self, msg_id, seq_no, obj): 24 | self.msg_id = msg_id 25 | self.seq_no = seq_no 26 | self.obj = obj 27 | 28 | def to_dict(self): 29 | return { 30 | '_': 'TLMessage', 31 | 'msg_id': self.msg_id, 32 | 'seq_no': self.seq_no, 33 | 'obj': self.obj 34 | } 35 | -------------------------------------------------------------------------------- /telethon_generator/data/html/img/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /telethon/client/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This package defines clients as subclasses of others, and then a single 3 | `telethon.client.telegramclient.TelegramClient` which is subclass of them 4 | all to provide the final unified interface while the methods can live in 5 | different subclasses to be more maintainable. 6 | 7 | The ABC is `telethon.client.telegrambaseclient.TelegramBaseClient` and the 8 | first implementor is `telethon.client.users.UserMethods`, since calling 9 | requests require them to be resolved first, and that requires accessing 10 | entities (users). 11 | """ 12 | from .telegrambaseclient import TelegramBaseClient 13 | from .users import UserMethods # Required for everything 14 | from .messageparse import MessageParseMethods # Required for messages 15 | from .uploads import UploadMethods # Required for messages to send files 16 | from .updates import UpdateMethods # Required for buttons (register callbacks) 17 | from .buttons import ButtonMethods # Required for messages to use buttons 18 | from .messages import MessageMethods 19 | from .chats import ChatMethods 20 | from .dialogs import DialogMethods 21 | from .downloads import DownloadMethods 22 | from .account import AccountMethods 23 | from .auth import AuthMethods 24 | from .bots import BotMethods 25 | from .telegramclient import TelegramClient 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://snarky.ca/what-the-heck-is-pyproject-toml/ 2 | [build-system] 3 | requires = ["setuptools", "wheel"] 4 | build-backend = "setuptools.build_meta" 5 | 6 | # Need to use legacy format for the time being 7 | # https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini 8 | [tool.tox] 9 | legacy_tox_ini = """ 10 | [tox] 11 | envlist = py35,py36,py37,py38 12 | 13 | # run with tox -e py 14 | [testenv] 15 | deps = 16 | -rrequirements.txt 17 | -roptional-requirements.txt 18 | -rdev-requirements.txt 19 | commands = 20 | # NOTE: you can run any command line tool here - not just tests 21 | pytest {posargs} 22 | 23 | # run with tox -e flake 24 | [testenv:flake] 25 | deps = 26 | -rrequirements.txt 27 | -roptional-requirements.txt 28 | -rdev-requirements.txt 29 | flake8 30 | commands = 31 | # stop the build if there are Python syntax errors or undefined names 32 | flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics 35 | 36 | """ 37 | -------------------------------------------------------------------------------- /telethon/tl/core/rpcresult.py: -------------------------------------------------------------------------------- 1 | from .gzippacked import GzipPacked 2 | from .. import TLObject 3 | from ..types import RpcError 4 | 5 | 6 | class RpcResult(TLObject): 7 | CONSTRUCTOR_ID = 0xf35c6d01 8 | 9 | def __init__(self, req_msg_id, body, error): 10 | self.req_msg_id = req_msg_id 11 | self.body = body 12 | self.error = error 13 | 14 | @classmethod 15 | def from_reader(cls, reader): 16 | msg_id = reader.read_long() 17 | inner_code = reader.read_int(signed=False) 18 | if inner_code == RpcError.CONSTRUCTOR_ID: 19 | return RpcResult(msg_id, None, RpcError.from_reader(reader)) 20 | if inner_code == GzipPacked.CONSTRUCTOR_ID: 21 | return RpcResult(msg_id, GzipPacked.from_reader(reader).data, None) 22 | 23 | reader.seek(-4) 24 | # This reader.read() will read more than necessary, but it's okay. 25 | # We could make use of MessageContainer's length here, but since 26 | # it's not necessary we don't need to care about it. 27 | return RpcResult(msg_id, reader.read(), None) 28 | 29 | def to_dict(self): 30 | return { 31 | '_': 'RpcResult', 32 | 'req_msg_id': self.req_msg_id, 33 | 'body': self.body, 34 | 'error': self.error 35 | } 36 | -------------------------------------------------------------------------------- /telethon_generator/data/html/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oopsie! | Telethon 4 | 5 | 6 | 7 | 8 | 36 | 37 | 38 |
39 |

You seem a bit lost…

40 |

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

42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/telethon/test_pickle.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from telethon.errors import RPCError, BadRequestError, FileIdInvalidError, NetworkMigrateError 4 | 5 | 6 | def _assert_equality(error, unpickled_error): 7 | assert error.code == unpickled_error.code 8 | assert error.message == unpickled_error.message 9 | assert type(error) == type(unpickled_error) 10 | assert str(error) == str(unpickled_error) 11 | 12 | 13 | def test_base_rpcerror_pickle(): 14 | error = RPCError("request", "message", 123) 15 | unpickled_error = pickle.loads(pickle.dumps(error)) 16 | _assert_equality(error, unpickled_error) 17 | 18 | 19 | def test_rpcerror_pickle(): 20 | error = BadRequestError("request", "BAD_REQUEST", 400) 21 | unpickled_error = pickle.loads(pickle.dumps(error)) 22 | _assert_equality(error, unpickled_error) 23 | 24 | 25 | def test_fancy_rpcerror_pickle(): 26 | error = FileIdInvalidError("request") 27 | unpickled_error = pickle.loads(pickle.dumps(error)) 28 | _assert_equality(error, unpickled_error) 29 | 30 | 31 | def test_fancy_rpcerror_capture_pickle(): 32 | error = NetworkMigrateError(request="request", capture=5) 33 | unpickled_error = pickle.loads(pickle.dumps(error)) 34 | _assert_equality(error, unpickled_error) 35 | assert error.new_dc == unpickled_error.new_dc 36 | -------------------------------------------------------------------------------- /tests/telethon/crypto/test_rsa.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `telethon.crypto.rsa`. 3 | """ 4 | import pytest 5 | 6 | from telethon.crypto import rsa 7 | 8 | 9 | @pytest.fixture 10 | def server_key_fp(): 11 | """Factory to return a key, old if so chosen.""" 12 | def _server_key_fp(old: bool): 13 | for fp, data in rsa._server_keys.items(): 14 | _, old_key = data 15 | if old_key == old: 16 | return fp 17 | 18 | return _server_key_fp 19 | 20 | 21 | def test_encryption_inv_key(): 22 | """Test for #1324.""" 23 | assert rsa.encrypt("invalid", b"testdata") is None 24 | 25 | 26 | def test_encryption_old_key(server_key_fp): 27 | """Test for #1324.""" 28 | assert rsa.encrypt(server_key_fp(old=True), b"testdata") is None 29 | 30 | 31 | def test_encryption_allowed_old_key(server_key_fp): 32 | data = rsa.encrypt(server_key_fp(old=True), b"testdata", use_old=True) 33 | # We can't verify the data is actually valid because we don't have 34 | # the decryption keys 35 | assert data is not None and len(data) == 256 36 | 37 | 38 | def test_encryption_current_key(server_key_fp): 39 | data = rsa.encrypt(server_key_fp(old=False), b"testdata") 40 | # We can't verify the data is actually valid because we don't have 41 | # the decryption keys 42 | assert data is not None and len(data) == 256 43 | -------------------------------------------------------------------------------- /telethon/crypto/aesctr.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds the AESModeCTR wrapper class. 3 | """ 4 | import pyaes 5 | 6 | 7 | class AESModeCTR: 8 | """Wrapper around pyaes.AESModeOfOperationCTR mode with custom IV""" 9 | # TODO Maybe make a pull request to pyaes to support iv on CTR 10 | 11 | def __init__(self, key, iv): 12 | """ 13 | Initializes the AES CTR mode with the given key/iv pair. 14 | 15 | :param key: the key to be used as bytes. 16 | :param iv: the bytes initialization vector. Must have a length of 16. 17 | """ 18 | # TODO Use libssl if available 19 | assert isinstance(key, bytes) 20 | self._aes = pyaes.AESModeOfOperationCTR(key) 21 | 22 | assert isinstance(iv, bytes) 23 | assert len(iv) == 16 24 | self._aes._counter._counter = list(iv) 25 | 26 | def encrypt(self, data): 27 | """ 28 | Encrypts the given plain text through AES CTR. 29 | 30 | :param data: the plain text to be encrypted. 31 | :return: the encrypted cipher text. 32 | """ 33 | return self._aes.encrypt(data) 34 | 35 | def decrypt(self, data): 36 | """ 37 | Decrypts the given cipher text through AES CTR 38 | 39 | :param data: the cipher text to be decrypted. 40 | :return: the decrypted plain text. 41 | """ 42 | return self._aes.decrypt(data) 43 | -------------------------------------------------------------------------------- /telethon/network/connection/http.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from .connection import Connection, PacketCodec 4 | 5 | 6 | SSL_PORT = 443 7 | 8 | 9 | class HttpPacketCodec(PacketCodec): 10 | tag = None 11 | obfuscate_tag = None 12 | 13 | def encode_packet(self, data): 14 | return ('POST /api HTTP/1.1\r\n' 15 | 'Host: {}:{}\r\n' 16 | 'Content-Type: application/x-www-form-urlencoded\r\n' 17 | 'Connection: keep-alive\r\n' 18 | 'Keep-Alive: timeout=100000, max=10000000\r\n' 19 | 'Content-Length: {}\r\n\r\n' 20 | .format(self._conn._ip, self._conn._port, len(data)) 21 | .encode('ascii') + data) 22 | 23 | async def read_packet(self, reader): 24 | while True: 25 | line = await reader.readline() 26 | if not line or line[-1] != b'\n': 27 | raise asyncio.IncompleteReadError(line, None) 28 | 29 | if line.lower().startswith(b'content-length: '): 30 | await reader.readexactly(2) 31 | length = int(line[16:-2]) 32 | return await reader.readexactly(length) 33 | 34 | 35 | class ConnectionHttp(Connection): 36 | packet_codec = HttpPacketCodec 37 | 38 | async def connect(self, timeout=None, ssl=None): 39 | await super().connect(timeout=timeout, ssl=self._port == SSL_PORT) 40 | -------------------------------------------------------------------------------- /telethon/tl/core/gzippacked.py: -------------------------------------------------------------------------------- 1 | import gzip 2 | import struct 3 | 4 | from .. import TLObject 5 | 6 | 7 | class GzipPacked(TLObject): 8 | CONSTRUCTOR_ID = 0x3072cfa1 9 | 10 | def __init__(self, data): 11 | self.data = data 12 | 13 | @staticmethod 14 | def gzip_if_smaller(content_related, data): 15 | """Calls bytes(request), and based on a certain threshold, 16 | optionally gzips the resulting data. If the gzipped data is 17 | smaller than the original byte array, this is returned instead. 18 | 19 | Note that this only applies to content related requests. 20 | """ 21 | if content_related and len(data) > 512: 22 | gzipped = bytes(GzipPacked(data)) 23 | return gzipped if len(gzipped) < len(data) else data 24 | else: 25 | return data 26 | 27 | def __bytes__(self): 28 | return struct.pack('`__ 7 | (also known as TL, found on ``.tl`` files) is a concise way to define 8 | what other programming languages commonly call classes or structs. 9 | 10 | Every definition is written as follows for a Telegram object is defined 11 | as follows: 12 | 13 | ``name#id argument_name:argument_type = CommonType`` 14 | 15 | This means that in a single line you know what the ``TLObject`` name is. 16 | You know it's unique ID, and you know what arguments it has. It really 17 | isn't that hard to write a generator for generating code to any 18 | platform! 19 | 20 | The generated code should also be able to *encode* the ``TLObject`` (let 21 | this be a request or a type) into bytes, so they can be sent over the 22 | network. This isn't a big deal either, because you know how the 23 | ``TLObject``\ 's are made, and how the types should be serialized. 24 | 25 | You can either write your own code generator, or use the one this 26 | library provides, but please be kind and keep some special mention to 27 | this project for helping you out. 28 | 29 | This is only a introduction. The ``TL`` language is not *that* easy. But 30 | it's not that hard either. You're free to sniff the 31 | ``telethon_generator/`` files and learn how to parse other more complex 32 | lines, such as ``flags`` (to indicate things that may or may not be 33 | written at all) and ``vector``\ 's. 34 | -------------------------------------------------------------------------------- /telethon/network/connection/tcpintermediate.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import random 3 | import os 4 | 5 | from .connection import Connection, PacketCodec 6 | 7 | 8 | class IntermediatePacketCodec(PacketCodec): 9 | tag = b'\xee\xee\xee\xee' 10 | obfuscate_tag = tag 11 | 12 | def encode_packet(self, data): 13 | return struct.pack(' 0: 37 | return packet_with_padding[:-pad_size] 38 | return packet_with_padding 39 | 40 | 41 | class ConnectionTcpIntermediate(Connection): 42 | """ 43 | Intermediate mode between `ConnectionTcpFull` and `ConnectionTcpAbridged`. 44 | Always sends 4 extra bytes for the packet length. 45 | """ 46 | packet_codec = IntermediatePacketCodec 47 | -------------------------------------------------------------------------------- /telethon_examples/print_updates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # A simple script to print all updates received. 3 | # Import modules to access environment, sleep, write to stderr 4 | import os 5 | import sys 6 | import time 7 | 8 | # Import the client 9 | from telethon import TelegramClient 10 | 11 | 12 | # This is a helper method to access environment variables or 13 | # prompt the user to type them in the terminal if missing. 14 | def get_env(name, message, cast=str): 15 | if name in os.environ: 16 | return os.environ[name] 17 | while True: 18 | value = input(message) 19 | try: 20 | return cast(value) 21 | except ValueError as e: 22 | print(e, file=sys.stderr) 23 | time.sleep(1) 24 | 25 | 26 | # Define some variables so the code reads easier 27 | session = os.environ.get('TG_SESSION', 'printer') 28 | api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) 29 | api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') 30 | proxy = None # https://github.com/Anorov/PySocks 31 | 32 | 33 | # This is our update handler. It is called when a new update arrives. 34 | async def handler(update): 35 | print(update) 36 | 37 | 38 | # Use the client in a `with` block. It calls `start/disconnect` automatically. 39 | with TelegramClient(session, api_id, api_hash, proxy=proxy) as client: 40 | # Register the update handler so that it gets called 41 | client.add_event_handler(handler) 42 | 43 | # Run the client until Ctrl+C is pressed, or the client disconnects 44 | print('(Press Ctrl+C to stop this)') 45 | client.run_until_disconnected() 46 | -------------------------------------------------------------------------------- /readthedocs/developing/test-servers.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Test Servers 3 | ============ 4 | 5 | 6 | To run Telethon on a test server, use the following code: 7 | 8 | .. code-block:: python 9 | 10 | client = TelegramClient(None, api_id, api_hash) 11 | client.session.set_dc(dc_id, '149.154.167.40', 80) 12 | 13 | You can check your ``'test ip'`` on https://my.telegram.org. 14 | 15 | You should set `None` session so to ensure you're generating a new 16 | authorization key for it (it would fail if you used a session where you 17 | had previously connected to another data center). 18 | 19 | Note that port 443 might not work, so you can try with 80 instead. 20 | 21 | Once you're connected, you'll likely be asked to either sign in or sign up. 22 | Remember `anyone can access the phone you 23 | choose `__, 24 | so don't store sensitive data here. 25 | 26 | Valid phone numbers are ``99966XYYYY``, where ``X`` is the ``dc_id`` and 27 | ``YYYY`` is any number you want, for example, ``1234`` in ``dc_id = 2`` would 28 | be ``9996621234``. The code sent by Telegram will be ``dc_id`` repeated five 29 | times, in this case, ``22222`` so we can hardcode that: 30 | 31 | .. code-block:: python 32 | 33 | client = TelegramClient(None, api_id, api_hash) 34 | client.session.set_dc(2, '149.154.167.40', 80) 35 | client.start( 36 | phone='9996621234', code_callback=lambda: '22222' 37 | ) 38 | 39 | Note that Telegram has changed the length of login codes multiple times in the 40 | past, so if ``dc_id`` repeated five times does not work, try repeating it six 41 | times. 42 | -------------------------------------------------------------------------------- /telethon_examples/print_messages.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # A simple script to print some messages. 3 | import os 4 | import sys 5 | import time 6 | 7 | from telethon import TelegramClient, events, utils 8 | 9 | 10 | def get_env(name, message, cast=str): 11 | if name in os.environ: 12 | return os.environ[name] 13 | while True: 14 | value = input(message) 15 | try: 16 | return cast(value) 17 | except ValueError as e: 18 | print(e, file=sys.stderr) 19 | time.sleep(1) 20 | 21 | 22 | session = os.environ.get('TG_SESSION', 'printer') 23 | api_id = get_env('TG_API_ID', 'Enter your API ID: ', int) 24 | api_hash = get_env('TG_API_HASH', 'Enter your API hash: ') 25 | proxy = None # https://github.com/Anorov/PySocks 26 | 27 | # Create and start the client so we can make requests (we don't here) 28 | client = TelegramClient(session, api_id, api_hash, proxy=proxy).start() 29 | 30 | 31 | # `pattern` is a regex, see https://docs.python.org/3/library/re.html 32 | # Use https://regexone.com/ if you want a more interactive way of learning. 33 | # 34 | # "(?i)" makes it case-insensitive, and | separates "options". 35 | @client.on(events.NewMessage(pattern=r'(?i).*\b(hello|hi)\b')) 36 | async def handler(event): 37 | sender = await event.get_sender() 38 | name = utils.get_display_name(sender) 39 | print(name, 'said', event.text, '!') 40 | 41 | try: 42 | print('(Press Ctrl+C to stop this)') 43 | client.run_until_disconnected() 44 | finally: 45 | client.disconnect() 46 | 47 | # Note: We used try/finally to show it can be done this way, but using: 48 | # 49 | # with client: 50 | # client.run_until_disconnected() 51 | # 52 | # is almost always a better idea. 53 | -------------------------------------------------------------------------------- /readthedocs/modules/events.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-events: 2 | 3 | ============= 4 | Update Events 5 | ============= 6 | 7 | .. currentmodule:: telethon.events 8 | 9 | Every event (builder) subclasses `common.EventBuilder`, 10 | so all the methods in it can be used from any event builder/event instance. 11 | 12 | .. automodule:: telethon.events.common 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | .. automodule:: telethon.events.newmessage 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | .. automodule:: telethon.events.chataction 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | .. automodule:: telethon.events.userupdate 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | 32 | .. automodule:: telethon.events.messageedited 33 | :members: 34 | :undoc-members: 35 | :show-inheritance: 36 | 37 | .. automodule:: telethon.events.messagedeleted 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | .. automodule:: telethon.events.messageread 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | .. automodule:: telethon.events.callbackquery 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | .. automodule:: telethon.events.inlinequery 53 | :members: 54 | :undoc-members: 55 | :show-inheritance: 56 | 57 | .. automodule:: telethon.events.album 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | .. automodule:: telethon.events.raw 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | 67 | .. automodule:: telethon.events 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /telethon/errors/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds all the base and automatically generated errors that the 3 | Telegram API has. See telethon_generator/errors.json for more. 4 | """ 5 | import re 6 | 7 | from .common import ( 8 | ReadCancelledError, TypeNotFoundError, InvalidChecksumError, 9 | InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError, 10 | AlreadyInConversationError, BadMessageError, MultiError 11 | ) 12 | 13 | # This imports the base errors too, as they're imported there 14 | from .rpcbaseerrors import * 15 | from .rpcerrorlist import * 16 | 17 | 18 | def rpc_message_to_error(rpc_error, request): 19 | """ 20 | Converts a Telegram's RPC Error to a Python error. 21 | 22 | :param rpc_error: the RpcError instance. 23 | :param request: the request that caused this error. 24 | :return: the RPCError as a Python exception that represents this error. 25 | """ 26 | # Try to get the error by direct look-up, otherwise regex 27 | # Case-insensitive, for things like "timeout" which don't conform. 28 | cls = rpc_errors_dict.get(rpc_error.error_message.upper(), None) 29 | if cls: 30 | return cls(request=request) 31 | 32 | for msg_regex, cls in rpc_errors_re: 33 | m = re.match(msg_regex, rpc_error.error_message) 34 | if m: 35 | capture = int(m.group(1)) if m.groups() else None 36 | return cls(request=request, capture=capture) 37 | 38 | # Some errors are negative: 39 | # * -500 for "No workers running", 40 | # * -503 for "Timeout" 41 | # 42 | # We treat them as if they were positive, so -500 will be treated 43 | # as a `ServerError`, etc. 44 | cls = base_errors.get(abs(rpc_error.error_code), RPCError) 45 | return cls(request=request, message=rpc_error.error_message, 46 | code=rpc_error.error_code) 47 | -------------------------------------------------------------------------------- /telethon/events/raw.py: -------------------------------------------------------------------------------- 1 | from .common import EventBuilder 2 | from .. import utils 3 | 4 | 5 | class Raw(EventBuilder): 6 | """ 7 | Raw events are not actual events. Instead, they are the raw 8 | :tl:`Update` object that Telegram sends. You normally shouldn't 9 | need these. 10 | 11 | Args: 12 | types (`list` | `tuple` | `type`, optional): 13 | The type or types that the :tl:`Update` instance must be. 14 | Equivalent to ``if not isinstance(update, types): return``. 15 | 16 | Example 17 | .. code-block:: python 18 | 19 | from telethon import events 20 | 21 | @client.on(events.Raw) 22 | async def handler(update): 23 | # Print all incoming updates 24 | print(update.stringify()) 25 | """ 26 | def __init__(self, types=None, *, func=None): 27 | super().__init__(func=func) 28 | if not types: 29 | self.types = None 30 | elif not utils.is_list_like(types): 31 | if not isinstance(types, type): 32 | raise TypeError('Invalid input type given: {}'.format(types)) 33 | 34 | self.types = types 35 | else: 36 | if not all(isinstance(x, type) for x in types): 37 | raise TypeError('Invalid input types given: {}'.format(types)) 38 | 39 | self.types = tuple(types) 40 | 41 | async def resolve(self, client): 42 | self.resolved = True 43 | 44 | @classmethod 45 | def build(cls, update, others=None, self_id=None): 46 | return update 47 | 48 | def filter(self, event): 49 | if not self.types or isinstance(event, self.types): 50 | if self.func: 51 | # Return the result of func directly as it may need to be awaited 52 | return self.func(event) 53 | return event 54 | -------------------------------------------------------------------------------- /tests/telethon/events/test_chataction.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from telethon import TelegramClient, events, types, utils 4 | 5 | 6 | def get_client(): 7 | return TelegramClient(None, 1, '1') 8 | 9 | 10 | def get_user_456(): 11 | return types.User( 12 | id=456, 13 | access_hash=789, 14 | first_name='User 123' 15 | ) 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_get_input_users_no_action_message_no_entities(): 20 | event = events.ChatAction.build(types.UpdateChatParticipantDelete( 21 | chat_id=123, 22 | user_id=456, 23 | version=1 24 | )) 25 | event._set_client(get_client()) 26 | 27 | assert await event.get_input_users() == [] 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_get_input_users_no_action_message(): 32 | user = get_user_456() 33 | event = events.ChatAction.build(types.UpdateChatParticipantDelete( 34 | chat_id=123, 35 | user_id=456, 36 | version=1 37 | )) 38 | event._set_client(get_client()) 39 | event._entities[user.id] = user 40 | 41 | assert await event.get_input_users() == [utils.get_input_peer(user)] 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_get_users_no_action_message_no_entities(): 46 | event = events.ChatAction.build(types.UpdateChatParticipantDelete( 47 | chat_id=123, 48 | user_id=456, 49 | version=1 50 | )) 51 | event._set_client(get_client()) 52 | 53 | assert await event.get_users() == [] 54 | 55 | 56 | @pytest.mark.asyncio 57 | async def test_get_users_no_action_message(): 58 | user = get_user_456() 59 | event = events.ChatAction.build(types.UpdateChatParticipantDelete( 60 | chat_id=123, 61 | user_id=456, 62 | version=1 63 | )) 64 | event._set_client(get_client()) 65 | event._entities[user.id] = user 66 | 67 | assert await event.get_users() == [user] 68 | -------------------------------------------------------------------------------- /readthedocs/examples/users.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Users 3 | ===== 4 | 5 | 6 | .. note:: 7 | 8 | These examples assume you have read :ref:`full-api`. 9 | 10 | .. contents:: 11 | 12 | 13 | Retrieving full information 14 | =========================== 15 | 16 | If you need to retrieve the bio, biography or about information for a user 17 | you should use :tl:`GetFullUser`: 18 | 19 | 20 | .. code-block:: python 21 | 22 | from telethon.tl.functions.users import GetFullUserRequest 23 | 24 | full = await client(GetFullUserRequest(user)) 25 | # or even 26 | full = await client(GetFullUserRequest('username')) 27 | 28 | bio = full.full_user.about 29 | 30 | 31 | See :tl:`UserFull` to know what other fields you can access. 32 | 33 | 34 | Updating your name and/or bio 35 | ============================= 36 | 37 | The first name, last name and bio (about) can all be changed with the same 38 | request. Omitted fields won't change after invoking :tl:`UpdateProfile`: 39 | 40 | .. code-block:: python 41 | 42 | from telethon.tl.functions.account import UpdateProfileRequest 43 | 44 | await client(UpdateProfileRequest( 45 | about='This is a test from Telethon' 46 | )) 47 | 48 | 49 | Updating your username 50 | ====================== 51 | 52 | You need to use :tl:`account.UpdateUsername`: 53 | 54 | .. code-block:: python 55 | 56 | from telethon.tl.functions.account import UpdateUsernameRequest 57 | 58 | await client(UpdateUsernameRequest('new_username')) 59 | 60 | 61 | Updating your profile photo 62 | =========================== 63 | 64 | The easiest way is to upload a new file and use that as the profile photo 65 | through :tl:`UploadProfilePhoto`: 66 | 67 | 68 | .. code-block:: python 69 | 70 | from telethon.tl.functions.photos import UploadProfilePhotoRequest 71 | 72 | await client(UploadProfilePhotoRequest( 73 | await client.upload_file('/path/to/some/file') 74 | )) 75 | -------------------------------------------------------------------------------- /telethon/crypto/factorization.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds a fast Factorization class. 3 | """ 4 | from random import randint 5 | 6 | 7 | class Factorization: 8 | """ 9 | Simple module to factorize large numbers really quickly. 10 | """ 11 | @classmethod 12 | def factorize(cls, pq): 13 | """ 14 | Factorizes the given large integer. 15 | 16 | Implementation from https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/. 17 | 18 | :param pq: the prime pair pq. 19 | :return: a tuple containing the two factors p and q. 20 | """ 21 | if pq % 2 == 0: 22 | return 2, pq // 2 23 | 24 | y, c, m = randint(1, pq - 1), randint(1, pq - 1), randint(1, pq - 1) 25 | g = r = q = 1 26 | x = ys = 0 27 | 28 | while g == 1: 29 | x = y 30 | for i in range(r): 31 | y = (pow(y, 2, pq) + c) % pq 32 | 33 | k = 0 34 | while k < r and g == 1: 35 | ys = y 36 | for i in range(min(m, r - k)): 37 | y = (pow(y, 2, pq) + c) % pq 38 | q = q * (abs(x - y)) % pq 39 | 40 | g = cls.gcd(q, pq) 41 | k += m 42 | 43 | r *= 2 44 | 45 | if g == pq: 46 | while True: 47 | ys = (pow(ys, 2, pq) + c) % pq 48 | g = cls.gcd(abs(x - ys), pq) 49 | if g > 1: 50 | break 51 | 52 | p, q = g, pq // g 53 | return (p, q) if p < q else (q, p) 54 | 55 | @staticmethod 56 | def gcd(a, b): 57 | """ 58 | Calculates the Greatest Common Divisor. 59 | 60 | :param a: the first number. 61 | :param b: the second number. 62 | :return: GCD(a, b) 63 | """ 64 | while b: 65 | a, b = b, a % b 66 | 67 | return a 68 | -------------------------------------------------------------------------------- /telethon/tl/core/messagecontainer.py: -------------------------------------------------------------------------------- 1 | from .tlmessage import TLMessage 2 | from ..tlobject import TLObject 3 | 4 | 5 | class MessageContainer(TLObject): 6 | CONSTRUCTOR_ID = 0x73f1f8dc 7 | 8 | # Maximum size in bytes for the inner payload of the container. 9 | # Telegram will close the connection if the payload is bigger. 10 | # The overhead of the container itself is subtracted. 11 | MAXIMUM_SIZE = 1044456 - 8 12 | 13 | # Maximum amount of messages that can't be sent inside a single 14 | # container, inclusive. Beyond this limit Telegram will respond 15 | # with BAD_MESSAGE 64 (invalid container). 16 | # 17 | # This limit is not 100% accurate and may in some cases be higher. 18 | # However, sending up to 100 requests at once in a single container 19 | # is a reasonable conservative value, since it could also depend on 20 | # other factors like size per request, but we cannot know this. 21 | MAXIMUM_LENGTH = 100 22 | 23 | def __init__(self, messages): 24 | self.messages = messages 25 | 26 | def to_dict(self): 27 | return { 28 | '_': 'MessageContainer', 29 | 'messages': 30 | [] if self.messages is None else [ 31 | None if x is None else x.to_dict() for x in self.messages 32 | ], 33 | } 34 | 35 | @classmethod 36 | def from_reader(cls, reader): 37 | # This assumes that .read_* calls are done in the order they appear 38 | messages = [] 39 | for _ in range(reader.read_int()): 40 | msg_id = reader.read_long() 41 | seq_no = reader.read_int() 42 | length = reader.read_int() 43 | before = reader.tell_position() 44 | obj = reader.tgread_object() # May over-read e.g. RpcResult 45 | reader.set_position(before + length) 46 | messages.append(TLMessage(msg_id, seq_no, obj)) 47 | return MessageContainer(messages) 48 | -------------------------------------------------------------------------------- /telethon_generator/data/friendly.csv: -------------------------------------------------------------------------------- 1 | ns,friendly,raw 2 | account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession 3 | auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization 4 | auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode 5 | auth.AuthMethods,log_out,auth.logOut 6 | auth.AuthMethods,edit_2fa,account.updatePasswordSettings 7 | bots.BotMethods,inline_query,messages.getInlineBotResults 8 | chats.ChatMethods,action,messages.setTyping 9 | chats.ChatMethods,edit_admin,channels.editAdmin messages.editChatAdmin 10 | chats.ChatMethods,edit_permissions,channels.editBanned messages.editChatDefaultBannedRights 11 | chats.ChatMethods,iter_participants,channels.getParticipants 12 | chats.ChatMethods,iter_admin_log,channels.getAdminLog 13 | dialogs.DialogMethods,iter_dialogs,messages.getDialogs 14 | dialogs.DialogMethods,iter_drafts,messages.getAllDrafts 15 | dialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders 16 | downloads.DownloadMethods,download_media,upload.getFile 17 | messages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages 18 | messages.MessageMethods,send_message,messages.sendMessage 19 | messages.MessageMethods,forward_messages,messages.forwardMessages 20 | messages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage 21 | messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages 22 | messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory 23 | updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference 24 | uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia 25 | uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart 26 | users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername 27 | -------------------------------------------------------------------------------- /telethon/_updates/entitycache.py: -------------------------------------------------------------------------------- 1 | from .session import EntityType, Entity 2 | 3 | 4 | _sentinel = object() 5 | 6 | 7 | class EntityCache: 8 | def __init__( 9 | self, 10 | hash_map: dict = _sentinel, 11 | self_id: int = None, 12 | self_bot: bool = None 13 | ): 14 | self.hash_map = {} if hash_map is _sentinel else hash_map 15 | self.self_id = self_id 16 | self.self_bot = self_bot 17 | 18 | def set_self_user(self, id, bot, hash): 19 | self.self_id = id 20 | self.self_bot = bot 21 | if hash: 22 | self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER) 23 | 24 | def get(self, id): 25 | try: 26 | hash, ty = self.hash_map[id] 27 | return Entity(ty, id, hash) 28 | except KeyError: 29 | return None 30 | 31 | def extend(self, users, chats): 32 | # See https://core.telegram.org/api/min for "issues" with "min constructors". 33 | self.hash_map.update( 34 | (u.id, ( 35 | u.access_hash, 36 | EntityType.BOT if u.bot else EntityType.USER, 37 | )) 38 | for u in users 39 | if getattr(u, 'access_hash', None) and not u.min 40 | ) 41 | self.hash_map.update( 42 | (c.id, ( 43 | c.access_hash, 44 | EntityType.MEGAGROUP if c.megagroup else ( 45 | EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL 46 | ), 47 | )) 48 | for c in chats 49 | if getattr(c, 'access_hash', None) and not getattr(c, 'min', None) 50 | ) 51 | 52 | def put(self, entity): 53 | self.hash_map[entity.id] = (entity.hash, entity.ty) 54 | 55 | def retain(self, filter): 56 | self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)} 57 | 58 | def __len__(self): 59 | return len(self.hash_map) 60 | -------------------------------------------------------------------------------- /telethon/hints.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import typing 3 | 4 | from . import helpers 5 | from .tl import types, custom 6 | 7 | Phone = str 8 | Username = str 9 | PeerID = int 10 | Entity = typing.Union[types.User, types.Chat, types.Channel] 11 | FullEntity = typing.Union[types.UserFull, types.messages.ChatFull, types.ChatFull, types.ChannelFull] 12 | 13 | EntityLike = typing.Union[ 14 | Phone, 15 | Username, 16 | PeerID, 17 | types.TypePeer, 18 | types.TypeInputPeer, 19 | Entity, 20 | FullEntity 21 | ] 22 | EntitiesLike = typing.Union[EntityLike, typing.Sequence[EntityLike]] 23 | 24 | ButtonLike = typing.Union[types.TypeKeyboardButton, custom.Button] 25 | MarkupLike = typing.Union[ 26 | types.TypeReplyMarkup, 27 | ButtonLike, 28 | typing.Sequence[ButtonLike], 29 | typing.Sequence[typing.Sequence[ButtonLike]] 30 | ] 31 | 32 | TotalList = helpers.TotalList 33 | 34 | DateLike = typing.Optional[typing.Union[float, datetime.datetime, datetime.date, datetime.timedelta]] 35 | 36 | LocalPath = str 37 | ExternalUrl = str 38 | BotFileID = str 39 | FileLike = typing.Union[ 40 | LocalPath, 41 | ExternalUrl, 42 | BotFileID, 43 | bytes, 44 | typing.BinaryIO, 45 | types.TypeMessageMedia, 46 | types.TypeInputFile, 47 | types.TypeInputFileLocation, 48 | types.TypeInputMedia, 49 | types.TypePhoto, 50 | types.TypeInputPhoto, 51 | types.TypeDocument, 52 | types.TypeInputDocument 53 | ] 54 | 55 | # Can't use `typing.Type` in Python 3.5.2 56 | # See https://github.com/python/typing/issues/266 57 | try: 58 | OutFileLike = typing.Union[ 59 | str, 60 | typing.Type[bytes], 61 | typing.BinaryIO 62 | ] 63 | except TypeError: 64 | OutFileLike = typing.Union[ 65 | str, 66 | typing.BinaryIO 67 | ] 68 | 69 | MessageLike = typing.Union[str, types.Message] 70 | MessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage] 71 | 72 | ProgressCallback = typing.Callable[[int, int], None] 73 | -------------------------------------------------------------------------------- /tests/telethon/test_utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import pathlib 3 | 4 | import pytest 5 | 6 | from telethon import utils 7 | from telethon.tl.types import ( 8 | MessageMediaGame, Game, PhotoEmpty 9 | ) 10 | 11 | 12 | def test_game_input_media_memory_error(): 13 | large_long = 2**62 14 | media = MessageMediaGame(Game( 15 | id=large_long, # <- key to trigger `MemoryError` 16 | access_hash=large_long, 17 | short_name='short_name', 18 | title='title', 19 | description='description', 20 | photo=PhotoEmpty(large_long), 21 | )) 22 | input_media = utils.get_input_media(media) 23 | bytes(input_media) # <- shouldn't raise `MemoryError` 24 | 25 | 26 | def test_private_get_extension(): 27 | # Positive cases 28 | png_header = bytes.fromhex('89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52') 29 | png_buffer = io.BytesIO(png_header) 30 | 31 | class CustomFd: 32 | def __init__(self, name): 33 | self.name = name 34 | 35 | assert utils._get_extension('foo.bar.baz') == '.baz' 36 | assert utils._get_extension(pathlib.Path('foo.bar.baz')) == '.baz' 37 | assert utils._get_extension(CustomFd('foo.bar.baz')) == '.baz' 38 | 39 | # Negative cases 40 | null_header = bytes.fromhex('00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00') 41 | null_buffer = io.BytesIO(null_header) 42 | 43 | empty_header = bytes() 44 | empty_buffer = io.BytesIO(empty_header) 45 | 46 | assert utils._get_extension('foo') == '' 47 | assert utils._get_extension(pathlib.Path('foo')) == '' 48 | assert utils._get_extension(null_header) == '' 49 | assert utils._get_extension(null_buffer) == '' 50 | assert utils._get_extension(null_buffer) == '' # make sure it did seek back 51 | assert utils._get_extension(empty_header) == '' 52 | assert utils._get_extension(empty_buffer) == '' 53 | assert utils._get_extension(empty_buffer) == '' # make sure it did seek back 54 | assert utils._get_extension(CustomFd('foo')) == '' 55 | -------------------------------------------------------------------------------- /telethon_examples/assistant.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is only the "core" of the bot. It is responsible for loading the 3 | plugins module and initializing it. You may obtain the plugins by running: 4 | 5 | git clone https://github.com/Lonami/TelethonianBotExt plugins 6 | 7 | In the same folder where this file lives. As a result, the directory should 8 | look like the following: 9 | 10 | assistant.py 11 | plugins/ 12 | ... 13 | """ 14 | import asyncio 15 | import os 16 | import sys 17 | import time 18 | 19 | from telethon import TelegramClient 20 | 21 | try: 22 | # Standalone script assistant.py with folder plugins/ 23 | import plugins 24 | except ImportError: 25 | try: 26 | # Running as a module with `python -m assistant` and structure: 27 | # 28 | # assistant/ 29 | # __main__.py (this file) 30 | # plugins/ (cloned) 31 | from . import plugins 32 | except ImportError: 33 | print('could not load the plugins module, does the directory exist ' 34 | 'in the correct location?', file=sys.stderr) 35 | 36 | exit(1) 37 | 38 | 39 | def get_env(name, message, cast=str): 40 | if name in os.environ: 41 | return os.environ[name] 42 | while True: 43 | value = input(message) 44 | try: 45 | return cast(value) 46 | except ValueError as e: 47 | print(e, file=sys.stderr) 48 | time.sleep(1) 49 | 50 | 51 | API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int) 52 | API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ') 53 | TOKEN = get_env('TG_TOKEN', 'Enter the bot token: ') 54 | NAME = TOKEN.split(':')[0] 55 | 56 | 57 | async def main(): 58 | bot = TelegramClient(NAME, API_ID, API_HASH) 59 | 60 | await bot.start(bot_token=TOKEN) 61 | 62 | try: 63 | await plugins.init(bot) 64 | await bot.run_until_disconnected() 65 | finally: 66 | await bot.disconnect() 67 | 68 | 69 | if __name__ == '__main__': 70 | asyncio.run(main()) 71 | -------------------------------------------------------------------------------- /telethon/events/messageedited.py: -------------------------------------------------------------------------------- 1 | from .common import name_inner_event 2 | from .newmessage import NewMessage 3 | from ..tl import types 4 | 5 | 6 | @name_inner_event 7 | class MessageEdited(NewMessage): 8 | """ 9 | Occurs whenever a message is edited. Just like `NewMessage 10 | `, you should treat 11 | this event as a `Message `. 12 | 13 | .. warning:: 14 | 15 | On channels, `Message.out ` 16 | will be `True` if you sent the message originally, **not if 17 | you edited it**! This can be dangerous if you run outgoing 18 | commands on edits. 19 | 20 | Some examples follow: 21 | 22 | * You send a message "A", ``out is True``. 23 | * You edit "A" to "B", ``out is True``. 24 | * Someone else edits "B" to "C", ``out is True`` (**be careful!**). 25 | * Someone sends "X", ``out is False``. 26 | * Someone edits "X" to "Y", ``out is False``. 27 | * You edit "Y" to "Z", ``out is False``. 28 | 29 | Since there are useful cases where you need the right ``out`` 30 | value, the library cannot do anything automatically to help you. 31 | Instead, consider using ``from_users='me'`` (it won't work in 32 | broadcast channels at all since the sender is the channel and 33 | not you). 34 | 35 | Example 36 | .. code-block:: python 37 | 38 | from telethon import events 39 | 40 | @client.on(events.MessageEdited) 41 | async def handler(event): 42 | # Log the date of new edits 43 | print('Message', event.id, 'changed at', event.date) 44 | """ 45 | @classmethod 46 | def build(cls, update, others=None, self_id=None): 47 | if isinstance(update, (types.UpdateEditMessage, 48 | types.UpdateEditChannelMessage)): 49 | return cls.Event(update.message) 50 | 51 | class Event(NewMessage.Event): 52 | pass # Required if we want a different name for it 53 | -------------------------------------------------------------------------------- /telethon/crypto/authkey.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds the AuthKey class. 3 | """ 4 | import struct 5 | from hashlib import sha1 6 | 7 | from ..extensions import BinaryReader 8 | 9 | 10 | class AuthKey: 11 | """ 12 | Represents an authorization key, used to encrypt and decrypt 13 | messages sent to Telegram's data centers. 14 | """ 15 | def __init__(self, data): 16 | """ 17 | Initializes a new authorization key. 18 | 19 | :param data: the data in bytes that represent this auth key. 20 | """ 21 | self.key = data 22 | 23 | @property 24 | def key(self): 25 | return self._key 26 | 27 | @key.setter 28 | def key(self, value): 29 | if not value: 30 | self._key = self.aux_hash = self.key_id = None 31 | return 32 | 33 | if isinstance(value, type(self)): 34 | self._key, self.aux_hash, self.key_id = \ 35 | value._key, value.aux_hash, value.key_id 36 | return 37 | 38 | self._key = value 39 | with BinaryReader(sha1(self._key).digest()) as reader: 40 | self.aux_hash = reader.read_long(signed=False) 41 | reader.read(4) 42 | self.key_id = reader.read_long(signed=False) 43 | 44 | # TODO This doesn't really fit here, it's only used in authentication 45 | def calc_new_nonce_hash(self, new_nonce, number): 46 | """ 47 | Calculates the new nonce hash based on the current attributes. 48 | 49 | :param new_nonce: the new nonce to be hashed. 50 | :param number: number to prepend before the hash. 51 | :return: the hash for the given new nonce. 52 | """ 53 | new_nonce = new_nonce.to_bytes(32, 'little', signed=True) 54 | data = new_nonce + struct.pack(' str:``. 27 | * `decode` definition must be ``def decode(value: str) -> bytes:``. 28 | """ 29 | def __init__(self, string: str = None): 30 | super().__init__() 31 | if string: 32 | if string[0] != CURRENT_VERSION: 33 | raise ValueError('Not a valid string') 34 | 35 | string = string[1:] 36 | ip_len = 4 if len(string) == 352 else 16 37 | self._dc_id, ip, self._port, key = struct.unpack( 38 | _STRUCT_PREFORMAT.format(ip_len), StringSession.decode(string)) 39 | 40 | self._server_address = ipaddress.ip_address(ip).compressed 41 | if any(key): 42 | self._auth_key = AuthKey(key) 43 | 44 | @staticmethod 45 | def encode(x: bytes) -> str: 46 | return base64.urlsafe_b64encode(x).decode('ascii') 47 | 48 | @staticmethod 49 | def decode(x: str) -> bytes: 50 | return base64.urlsafe_b64decode(x) 51 | 52 | def save(self: Session): 53 | if not self.auth_key: 54 | return '' 55 | 56 | ip = ipaddress.ip_address(self.server_address).packed 57 | return CURRENT_VERSION + StringSession.encode(struct.pack( 58 | _STRUCT_PREFORMAT.format(len(ip)), 59 | self.dc_id, 60 | ip, 61 | self.port, 62 | self.auth_key.key 63 | )) 64 | -------------------------------------------------------------------------------- /telethon/network/mtprotoplainsender.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the class used to communicate with Telegram's servers 3 | in plain text, when no authorization key has been created yet. 4 | """ 5 | import struct 6 | 7 | from .mtprotostate import MTProtoState 8 | from ..errors import InvalidBufferError 9 | from ..extensions import BinaryReader 10 | 11 | 12 | class MTProtoPlainSender: 13 | """ 14 | MTProto Mobile Protocol plain sender 15 | (https://core.telegram.org/mtproto/description#unencrypted-messages) 16 | """ 17 | def __init__(self, connection, *, loggers): 18 | """ 19 | Initializes the MTProto plain sender. 20 | 21 | :param connection: the Connection to be used. 22 | """ 23 | self._state = MTProtoState(auth_key=None, loggers=loggers) 24 | self._connection = connection 25 | 26 | async def send(self, request): 27 | """ 28 | Sends and receives the result for the given request. 29 | """ 30 | body = bytes(request) 31 | msg_id = self._state._get_new_msg_id() 32 | await self._connection.send( 33 | struct.pack(' 0, 'Bad length' 53 | # We could read length bytes and use those in a new reader to read 54 | # the next TLObject without including the padding, but since the 55 | # reader isn't used for anything else after this, it's unnecessary. 56 | return reader.tgread_object() 57 | -------------------------------------------------------------------------------- /telethon/network/connection/tcpfull.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from zlib import crc32 3 | 4 | from .connection import Connection, PacketCodec 5 | from ...errors import InvalidChecksumError, InvalidBufferError 6 | 7 | 8 | class FullPacketCodec(PacketCodec): 9 | tag = None 10 | 11 | def __init__(self, connection): 12 | super().__init__(connection) 13 | self._send_counter = 0 # Important or Telegram won't reply 14 | 15 | def encode_packet(self, data): 16 | # https://core.telegram.org/mtproto#tcp-transport 17 | # total length, sequence number, packet and checksum (CRC32) 18 | length = len(data) + 12 19 | data = struct.pack('` and `SenderGetter 14 | ` which means you 15 | have access to all their sender and chat properties and methods. 16 | 17 | Attributes: 18 | 19 | original_fwd (:tl:`MessageFwdHeader`): 20 | The original :tl:`MessageFwdHeader` instance. 21 | 22 | Any other attribute: 23 | Attributes not described here are the same as those available 24 | in the original :tl:`MessageFwdHeader`. 25 | """ 26 | def __init__(self, client, original, entities): 27 | # Copy all the fields, not reference! It would cause memory cycles: 28 | # self.original_fwd.original_fwd.original_fwd.original_fwd 29 | # ...would be valid if we referenced. 30 | self.__dict__.update(original.__dict__) 31 | self.original_fwd = original 32 | 33 | sender_id = sender = input_sender = peer = chat = input_chat = None 34 | if original.from_id: 35 | ty = helpers._entity_type(original.from_id) 36 | if ty == helpers._EntityType.USER: 37 | sender_id = utils.get_peer_id(original.from_id) 38 | sender, input_sender = utils._get_entity_pair( 39 | sender_id, entities, client._mb_entity_cache) 40 | 41 | elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL): 42 | peer = original.from_id 43 | chat, input_chat = utils._get_entity_pair( 44 | utils.get_peer_id(peer), entities, client._mb_entity_cache) 45 | 46 | # This call resets the client 47 | ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat) 48 | SenderGetter.__init__(self, sender_id, sender=sender, input_sender=input_sender) 49 | self._client = client 50 | 51 | # TODO We could reload the message 52 | -------------------------------------------------------------------------------- /telethon/events/messagedeleted.py: -------------------------------------------------------------------------------- 1 | from .common import EventBuilder, EventCommon, name_inner_event 2 | from ..tl import types 3 | 4 | 5 | @name_inner_event 6 | class MessageDeleted(EventBuilder): 7 | """ 8 | Occurs whenever a message is deleted. Note that this event isn't 100% 9 | reliable, since Telegram doesn't always notify the clients that a message 10 | was deleted. 11 | 12 | .. important:: 13 | 14 | Telegram **does not** send information about *where* a message 15 | was deleted if it occurs in private conversations with other users 16 | or in small group chats, because message IDs are *unique* and you 17 | can identify the chat with the message ID alone if you saved it 18 | previously. 19 | 20 | Telethon **does not** save information of where messages occur, 21 | so it cannot know in which chat a message was deleted (this will 22 | only work in channels, where the channel ID *is* present). 23 | 24 | This means that the ``chats=`` parameter will not work reliably, 25 | unless you intend on working with channels and super-groups only. 26 | 27 | Example 28 | .. code-block:: python 29 | 30 | from telethon import events 31 | 32 | @client.on(events.MessageDeleted) 33 | async def handler(event): 34 | # Log all deleted message IDs 35 | for msg_id in event.deleted_ids: 36 | print('Message', msg_id, 'was deleted in', event.chat_id) 37 | """ 38 | @classmethod 39 | def build(cls, update, others=None, self_id=None): 40 | if isinstance(update, types.UpdateDeleteMessages): 41 | return cls.Event( 42 | deleted_ids=update.messages, 43 | peer=None 44 | ) 45 | elif isinstance(update, types.UpdateDeleteChannelMessages): 46 | return cls.Event( 47 | deleted_ids=update.messages, 48 | peer=types.PeerChannel(update.channel_id) 49 | ) 50 | 51 | class Event(EventCommon): 52 | def __init__(self, deleted_ids, peer): 53 | super().__init__( 54 | chat_peer=peer, msg_id=(deleted_ids or [0])[0] 55 | ) 56 | self.deleted_id = None if not deleted_ids else deleted_ids[0] 57 | self.deleted_ids = deleted_ids 58 | -------------------------------------------------------------------------------- /readthedocs/custom_roles.py: -------------------------------------------------------------------------------- 1 | from docutils import nodes, utils 2 | from docutils.parsers.rst.roles import set_classes 3 | 4 | 5 | def make_link_node(rawtext, app, name, options): 6 | """ 7 | Create a link to the TL reference. 8 | 9 | :param rawtext: Text being replaced with link node. 10 | :param app: Sphinx application context 11 | :param name: Name of the object to link to 12 | :param options: Options dictionary passed to role func. 13 | """ 14 | try: 15 | base = app.config.tl_ref_url 16 | if not base: 17 | raise AttributeError 18 | except AttributeError as e: 19 | raise ValueError('tl_ref_url config value is not set') from e 20 | 21 | if base[-1] != '/': 22 | base += '/' 23 | 24 | set_classes(options) 25 | node = nodes.reference(rawtext, utils.unescape(name), 26 | refuri='{}?q={}'.format(base, name), 27 | **options) 28 | return node 29 | 30 | 31 | # noinspection PyUnusedLocal 32 | def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None): 33 | """ 34 | Link to the TL reference. 35 | 36 | Returns 2 part tuple containing list of nodes to insert into the 37 | document and a list of system messages. Both are allowed to be empty. 38 | 39 | :param name: The role name used in the document. 40 | :param rawtext: The entire markup snippet, with role. 41 | :param text: The text marked with the role. 42 | :param lineno: The line number where rawtext appears in the input. 43 | :param inliner: The inliner instance that called us. 44 | :param options: Directive options for customization. 45 | :param content: The directive content for customization. 46 | """ 47 | if options is None: 48 | options = {} 49 | 50 | # TODO Report error on type not found? 51 | # Usage: 52 | # msg = inliner.reporter.error(..., line=lineno) 53 | # return [inliner.problematic(rawtext, rawtext, msg)], [msg] 54 | app = inliner.document.settings.env.app 55 | node = make_link_node(rawtext, app, text, options) 56 | return [node], [] 57 | 58 | 59 | def setup(app): 60 | """ 61 | Install the plugin. 62 | 63 | :param app: Sphinx application context. 64 | """ 65 | app.add_role('tl', tl_role) 66 | app.add_config_value('tl_ref_url', None, 'env') 67 | return 68 | -------------------------------------------------------------------------------- /telethon_generator/parsers/methods.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import enum 3 | import warnings 4 | 5 | 6 | class Usability(enum.Enum): 7 | UNKNOWN = 0 8 | USER = 1 9 | BOT = 2 10 | BOTH = 4 11 | 12 | @property 13 | def key(self): 14 | return { 15 | Usability.UNKNOWN: 'unknown', 16 | Usability.USER: 'user', 17 | Usability.BOT: 'bot', 18 | Usability.BOTH: 'both', 19 | }[self] 20 | 21 | 22 | class MethodInfo: 23 | def __init__(self, name, usability, errors, friendly): 24 | self.name = name 25 | self.errors = errors 26 | self.friendly = friendly 27 | try: 28 | self.usability = { 29 | 'unknown': Usability.UNKNOWN, 30 | 'user': Usability.USER, 31 | 'bot': Usability.BOT, 32 | 'both': Usability.BOTH, 33 | }[usability.lower()] 34 | except KeyError: 35 | raise ValueError('Usability must be either user, bot, both or ' 36 | 'unknown, not {}'.format(usability)) from None 37 | 38 | 39 | def parse_methods(csv_file, friendly_csv_file, errors_dict): 40 | """ 41 | Parses the input CSV file with columns (method, usability, errors) 42 | and yields `MethodInfo` instances as a result. 43 | """ 44 | raw_to_friendly = {} 45 | with friendly_csv_file.open(newline='') as f: 46 | f = csv.reader(f) 47 | next(f, None) # header 48 | for ns, friendly, raw_list in f: 49 | for raw in raw_list.split(): 50 | raw_to_friendly[raw] = (ns, friendly) 51 | 52 | with csv_file.open(newline='') as f: 53 | f = csv.reader(f) 54 | next(f, None) # header 55 | for line, (method, usability, errors) in enumerate(f, start=2): 56 | try: 57 | errors = [errors_dict[x] for x in errors.split()] 58 | except KeyError: 59 | raise ValueError('Method {} references unknown errors {}' 60 | .format(method, errors)) from None 61 | 62 | friendly = raw_to_friendly.pop(method, None) 63 | yield MethodInfo(method, usability, errors, friendly) 64 | 65 | if raw_to_friendly: 66 | warnings.warn('note: unknown raw methods in friendly mapping: {}' 67 | .format(', '.join(raw_to_friendly))) 68 | -------------------------------------------------------------------------------- /readthedocs/developing/project-structure.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Project Structure 3 | ================= 4 | 5 | 6 | Main interface 7 | ============== 8 | 9 | The library itself is under the ``telethon/`` directory. The 10 | ``__init__.py`` file there exposes the main ``TelegramClient``, a class 11 | that servers as a nice interface with the most commonly used methods on 12 | Telegram such as sending messages, retrieving the message history, 13 | handling updates, etc. 14 | 15 | The ``TelegramClient`` inherits from several mixing ``Method`` classes, 16 | since there are so many methods that having them in a single file would 17 | make maintenance painful (it was three thousand lines before this separation 18 | happened!). It's a "god object", but there is only a way to interact with 19 | Telegram really. 20 | 21 | The ``TelegramBaseClient`` is an ABC which will support all of these mixins 22 | so they can work together nicely. It doesn't even know how to invoke things 23 | because they need to be resolved with user information first (to work with 24 | input entities comfortably). 25 | 26 | The client makes use of the ``network/mtprotosender.py``. The 27 | ``MTProtoSender`` is responsible for connecting, reconnecting, 28 | packing, unpacking, sending and receiving items from the network. 29 | Basically, the low-level communication with Telegram, and handling 30 | MTProto-related functions and types such as ``BadSalt``. 31 | 32 | The sender makes use of a ``Connection`` class which knows the format in 33 | which outgoing messages should be sent (how to encode their length and 34 | their body, if they're further encrypted). 35 | 36 | Auto-generated code 37 | =================== 38 | 39 | The files under ``telethon_generator/`` are used to generate the code 40 | that gets placed under ``telethon/tl/``. The parsers take in files in 41 | a specific format (such as ``.tl`` for objects and ``.json`` for errors) 42 | and spit out the generated classes which represent, as Python classes, 43 | the request and types defined in the ``.tl`` file. It also constructs 44 | an index so that they can be imported easily. 45 | 46 | Custom documentation can also be generated to easily navigate through 47 | the vast amount of items offered by the API. 48 | 49 | If you clone the repository, you will have to run ``python setup.py gen`` 50 | in order to generate the code. Installing the library runs the generator 51 | too, but the mentioned command will just generate code. 52 | -------------------------------------------------------------------------------- /telethon_generator/sourcebuilder.py: -------------------------------------------------------------------------------- 1 | class SourceBuilder: 2 | """This class should be used to build .py source files""" 3 | 4 | def __init__(self, out_stream, indent_size=4): 5 | self.current_indent = 0 6 | self.on_new_line = False 7 | self.indent_size = indent_size 8 | self.out_stream = out_stream 9 | 10 | # Was a new line added automatically before? If so, avoid it 11 | self.auto_added_line = False 12 | 13 | def indent(self): 14 | """Indents the current source code line 15 | by the current indentation level 16 | """ 17 | self.write(' ' * (self.current_indent * self.indent_size)) 18 | 19 | def write(self, string, *args, **kwargs): 20 | """Writes a string into the source code, 21 | applying indentation if required 22 | """ 23 | if self.on_new_line: 24 | self.on_new_line = False # We're not on a new line anymore 25 | # If the string was not empty, indent; Else probably a new line 26 | if string.strip(): 27 | self.indent() 28 | 29 | if args or kwargs: 30 | self.out_stream.write(string.format(*args, **kwargs)) 31 | else: 32 | self.out_stream.write(string) 33 | 34 | def writeln(self, string='', *args, **kwargs): 35 | """Writes a string into the source code _and_ appends a new line, 36 | applying indentation if required 37 | """ 38 | self.write(string + '\n', *args, **kwargs) 39 | self.on_new_line = True 40 | 41 | # If we're writing a block, increment indent for the next time 42 | if string and string[-1] == ':': 43 | self.current_indent += 1 44 | 45 | # Clear state after the user adds a new line 46 | self.auto_added_line = False 47 | 48 | def end_block(self): 49 | """Ends an indentation block, leaving an empty line afterwards""" 50 | self.current_indent -= 1 51 | 52 | # If we did not add a new line automatically yet, now it's the time! 53 | if not self.auto_added_line: 54 | self.writeln() 55 | self.auto_added_line = True 56 | 57 | def __str__(self): 58 | self.out_stream.seek(0) 59 | return self.out_stream.read() 60 | 61 | def __enter__(self): 62 | return self 63 | 64 | def __exit__(self, exc_type, exc_val, exc_tb): 65 | self.out_stream.close() 66 | -------------------------------------------------------------------------------- /telethon_generator/syncerrors.py: -------------------------------------------------------------------------------- 1 | # Should be fed with the JSON obtained from https://core.telegram.org/api/errors#error-database 2 | import re 3 | import csv 4 | import sys 5 | import json 6 | from pathlib import Path 7 | 8 | sys.path.insert(0, '..') 9 | 10 | from telethon_generator.parsers.errors import parse_errors, Error 11 | from telethon_generator.parsers.methods import parse_methods, MethodInfo 12 | 13 | ERRORS = Path('data/errors.csv') 14 | METHODS = Path('data/methods.csv') 15 | FRIENDLY = Path('data/friendly.csv') 16 | 17 | 18 | def main(): 19 | new_errors = [] 20 | new_methods = [] 21 | 22 | self_errors = {e.str_code: e for e in parse_errors(ERRORS)} 23 | self_methods = {m.name: m for m in parse_methods(METHODS, FRIENDLY, self_errors)} 24 | 25 | tg_data = json.load(sys.stdin) 26 | 27 | def get_desc(code): 28 | return re.sub(r'\s*&\w+;\s*', '', (tg_data['descriptions'].get(code) or '').rstrip('.')) 29 | 30 | for int_code, errors in tg_data['errors'].items(): 31 | int_code = int(int_code) # json does not support non-string keys 32 | for code, methods in errors.items(): 33 | if not re.match(r'\w+', code): 34 | continue # skip, full code is unknown (contains asterisk or is multiple words) 35 | str_code = code.replace('%d', 'X') 36 | if error := self_errors.get(str_code): 37 | error.int_codes.append(int_code) # de-duplicated once later 38 | if not error.description: # prefer our descriptions 39 | if not error.has_captures: # need descriptions with specific text if error has captures 40 | error.description = get_desc(code) 41 | else: 42 | self_errors[str_code] = Error([int_code], str_code, get_desc(code)) 43 | 44 | new_errors.extend((e.str_code, ' '.join(map(str, sorted(set(e.int_codes)))), e.description) for e in self_errors.values()) 45 | new_methods.extend((m.name, m.usability.key, ' '.join(sorted(e.str_code for e in m.errors))) for m in self_methods.values()) 46 | 47 | csv.register_dialect('plain', lineterminator='\n') 48 | with ERRORS.open('w', encoding='utf-8', newline='') as fd: 49 | csv.writer(fd, 'plain').writerows((('name', 'codes', 'description'), *sorted(new_errors))) 50 | with METHODS.open('w', encoding='utf-8', newline='') as fd: 51 | csv.writer(fd, 'plain').writerows((('method', 'usability', 'errors'), *sorted(new_methods))) 52 | 53 | 54 | if __name__ == '__main__': 55 | main() 56 | -------------------------------------------------------------------------------- /telethon_generator/generators/errors.py: -------------------------------------------------------------------------------- 1 | def generate_errors(errors, f): 2 | # Exact/regex match to create {CODE: ErrorClassName} 3 | exact_match = [] 4 | regex_match = [] 5 | 6 | # Find out what subclasses to import and which to create 7 | import_base, create_base = set(), {} 8 | for error in errors: 9 | if error.subclass_exists: 10 | import_base.add(error.subclass) 11 | else: 12 | create_base[error.subclass] = error.int_code 13 | 14 | if error.has_captures: 15 | regex_match.append(error) 16 | else: 17 | exact_match.append(error) 18 | 19 | # Imports and new subclass creation 20 | f.write('from .rpcbaseerrors import RPCError, {}\n' 21 | .format(", ".join(sorted(import_base)))) 22 | 23 | for cls, int_code in sorted(create_base.items(), key=lambda t: t[1]): 24 | f.write('\n\nclass {}(RPCError):\n code = {}\n' 25 | .format(cls, int_code)) 26 | 27 | # Error classes generation 28 | for error in errors: 29 | f.write('\n\nclass {}({}):\n '.format(error.name, error.subclass)) 30 | 31 | if error.has_captures: 32 | f.write('def __init__(self, request, capture=0):\n ' 33 | ' self.request = request\n ') 34 | f.write(' self.{} = int(capture)\n ' 35 | .format(error.capture_name)) 36 | else: 37 | f.write('def __init__(self, request):\n ' 38 | ' self.request = request\n ') 39 | 40 | f.write('super(Exception, self).__init__(' 41 | '{}'.format(repr(error.description))) 42 | 43 | if error.has_captures: 44 | f.write('.format({0}=self.{0})'.format(error.capture_name)) 45 | 46 | f.write(' + self._fmt_request(self.request))\n\n') 47 | f.write(' def __reduce__(self):\n ') 48 | if error.has_captures: 49 | f.write('return type(self), (self.request, self.{})\n'.format(error.capture_name)) 50 | else: 51 | f.write('return type(self), (self.request,)\n') 52 | 53 | # Create the actual {CODE: ErrorClassName} dict once classes are defined 54 | f.write('\n\nrpc_errors_dict = {\n') 55 | for error in exact_match: 56 | f.write(' {}: {},\n'.format(repr(error.pattern), error.name)) 57 | f.write('}\n\nrpc_errors_re = (\n') 58 | for error in regex_match: 59 | f.write(' ({}, {}),\n'.format(repr(error.pattern), error.name)) 60 | f.write(')\n') 61 | -------------------------------------------------------------------------------- /telethon/client/bots.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from .. import hints 4 | from ..tl import types, functions, custom 5 | 6 | if typing.TYPE_CHECKING: 7 | from .telegramclient import TelegramClient 8 | 9 | 10 | class BotMethods: 11 | async def inline_query( 12 | self: 'TelegramClient', 13 | bot: 'hints.EntityLike', 14 | query: str, 15 | *, 16 | entity: 'hints.EntityLike' = None, 17 | offset: str = None, 18 | geo_point: 'types.GeoPoint' = None) -> custom.InlineResults: 19 | """ 20 | Makes an inline query to the specified bot (``@vote New Poll``). 21 | 22 | Arguments 23 | bot (`entity`): 24 | The bot entity to which the inline query should be made. 25 | 26 | query (`str`): 27 | The query that should be made to the bot. 28 | 29 | entity (`entity`, optional): 30 | The entity where the inline query is being made from. Certain 31 | bots use this to display different results depending on where 32 | it's used, such as private chats, groups or channels. 33 | 34 | If specified, it will also be the default entity where the 35 | message will be sent after clicked. Otherwise, the "empty 36 | peer" will be used, which some bots may not handle correctly. 37 | 38 | offset (`str`, optional): 39 | The string offset to use for the bot. 40 | 41 | geo_point (:tl:`GeoPoint`, optional) 42 | The geo point location information to send to the bot 43 | for localised results. Available under some bots. 44 | 45 | Returns 46 | A list of `custom.InlineResult 47 | `. 48 | 49 | Example 50 | .. code-block:: python 51 | 52 | # Make an inline query to @like 53 | results = await client.inline_query('like', 'Do you like Telethon?') 54 | 55 | # Send the first result to some chat 56 | message = await results[0].click('TelethonOffTopic') 57 | """ 58 | bot = await self.get_input_entity(bot) 59 | if entity: 60 | peer = await self.get_input_entity(entity) 61 | else: 62 | peer = types.InputPeerEmpty() 63 | 64 | result = await self(functions.messages.GetInlineBotResultsRequest( 65 | bot=bot, 66 | peer=peer, 67 | query=query, 68 | offset=offset or '', 69 | geo_point=geo_point 70 | )) 71 | 72 | return custom.InlineResults(self, result, entity=peer if entity else None) 73 | -------------------------------------------------------------------------------- /readthedocs/modules/client.rst: -------------------------------------------------------------------------------- 1 | .. _telethon-client: 2 | 3 | ============== 4 | TelegramClient 5 | ============== 6 | 7 | .. currentmodule:: telethon.client 8 | 9 | The `TelegramClient ` aggregates several mixin 10 | classes to provide all the common functionality in a nice, Pythonic interface. 11 | Each mixin has its own methods, which you all can use. 12 | 13 | **In short, to create a client you must run:** 14 | 15 | .. code-block:: python 16 | 17 | from telethon import TelegramClient 18 | 19 | client = TelegramClient(name, api_id, api_hash) 20 | 21 | async def main(): 22 | # Now you can use all client methods listed below, like for example... 23 | await client.send_message('me', 'Hello to myself!') 24 | 25 | with client: 26 | client.loop.run_until_complete(main()) 27 | 28 | 29 | You **don't** need to import these `AuthMethods`, `MessageMethods`, etc. 30 | Together they are the `TelegramClient ` and 31 | you can access all of their methods. 32 | 33 | See :ref:`client-ref` for a short summary. 34 | 35 | .. automodule:: telethon.client.telegramclient 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | 40 | .. automodule:: telethon.client.telegrambaseclient 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | .. automodule:: telethon.client.account 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | 50 | .. automodule:: telethon.client.auth 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | .. automodule:: telethon.client.bots 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | .. automodule:: telethon.client.buttons 61 | :members: 62 | :undoc-members: 63 | :show-inheritance: 64 | 65 | .. automodule:: telethon.client.chats 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | .. automodule:: telethon.client.dialogs 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | .. automodule:: telethon.client.downloads 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | 80 | .. automodule:: telethon.client.messageparse 81 | :members: 82 | :undoc-members: 83 | :show-inheritance: 84 | 85 | .. automodule:: telethon.client.messages 86 | :members: 87 | :undoc-members: 88 | :show-inheritance: 89 | 90 | .. automodule:: telethon.client.updates 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | .. automodule:: telethon.client.uploads 96 | :members: 97 | :undoc-members: 98 | :show-inheritance: 99 | 100 | .. automodule:: telethon.client.users 101 | :members: 102 | :undoc-members: 103 | :show-inheritance: 104 | -------------------------------------------------------------------------------- /telethon/sync.py: -------------------------------------------------------------------------------- 1 | """ 2 | This magical module will rewrite all public methods in the public interface 3 | of the library so they can run the loop on their own if it's not already 4 | running. This rewrite may not be desirable if the end user always uses the 5 | methods they way they should be ran, but it's incredibly useful for quick 6 | scripts and the runtime overhead is relatively low. 7 | 8 | Some really common methods which are hardly used offer this ability by 9 | default, such as ``.start()`` and ``.run_until_disconnected()`` (since 10 | you may want to start, and then run until disconnected while using async 11 | event handlers). 12 | """ 13 | import asyncio 14 | import functools 15 | import inspect 16 | 17 | from . import events, errors, utils, connection, helpers 18 | from .client.account import _TakeoutClient 19 | from .client.telegramclient import TelegramClient 20 | from .tl import types, functions, custom 21 | from .tl.custom import ( 22 | Draft, Dialog, MessageButton, Forward, Button, 23 | Message, InlineResult, Conversation 24 | ) 25 | from .tl.custom.chatgetter import ChatGetter 26 | from .tl.custom.sendergetter import SenderGetter 27 | 28 | 29 | def _syncify_wrap(t, method_name): 30 | method = getattr(t, method_name) 31 | 32 | @functools.wraps(method) 33 | def syncified(*args, **kwargs): 34 | coro = method(*args, **kwargs) 35 | loop = helpers.get_running_loop() 36 | if loop.is_running(): 37 | return coro 38 | else: 39 | return loop.run_until_complete(coro) 40 | 41 | # Save an accessible reference to the original method 42 | setattr(syncified, '__tl.sync', method) 43 | setattr(t, method_name, syncified) 44 | 45 | 46 | def syncify(*types): 47 | """ 48 | Converts all the methods in the given types (class definitions) 49 | into synchronous, which return either the coroutine or the result 50 | based on whether ``asyncio's`` event loop is running. 51 | """ 52 | # Our asynchronous generators all are `RequestIter`, which already 53 | # provide a synchronous iterator variant, so we don't need to worry 54 | # about asyncgenfunction's here. 55 | for t in types: 56 | for name in dir(t): 57 | if not name.startswith('_') or name == '__call__': 58 | if inspect.iscoroutinefunction(getattr(t, name)): 59 | _syncify_wrap(t, name) 60 | 61 | 62 | syncify(TelegramClient, _TakeoutClient, Draft, Dialog, MessageButton, 63 | ChatGetter, SenderGetter, Forward, Message, InlineResult, Conversation) 64 | 65 | 66 | # Private special case, since a conversation's methods return 67 | # futures (but the public function themselves are synchronous). 68 | _syncify_wrap(Conversation, '_get_result') 69 | 70 | __all__ = [ 71 | 'TelegramClient', 'Button', 72 | 'types', 'functions', 'custom', 'errors', 73 | 'events', 'utils', 'connection' 74 | ] 75 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Telethon 2 | ======== 3 | .. epigraph:: 4 | 5 | ⭐️ Thanks **everyone** who has starred the project, it means a lot! 6 | 7 | |logo| **Telethon** is an asyncio_ **Python 3** 8 | MTProto_ library to interact with Telegram_'s API 9 | as a user or through a bot account (bot API alternative). 10 | 11 | .. important:: 12 | 13 | If you have code using Telethon before its 1.0 version, you must 14 | read `Compatibility and Convenience`_ to learn how to migrate. 15 | As with any third-party library for Telegram, be careful not to 16 | break `Telegram's ToS`_ or `Telegram can ban the account`_. 17 | 18 | What is this? 19 | ------------- 20 | 21 | Telegram is a popular messaging application. This library is meant 22 | to make it easy for you to write Python programs that can interact 23 | with Telegram. Think of it as a wrapper that has already done the 24 | heavy job for you, so you can focus on developing an application. 25 | 26 | 27 | Installing 28 | ---------- 29 | 30 | .. code-block:: sh 31 | 32 | pip3 install telethon 33 | 34 | 35 | Creating a client 36 | ----------------- 37 | 38 | .. code-block:: python 39 | 40 | from telethon import TelegramClient, events, sync 41 | 42 | # These example values won't work. You must get your own api_id and 43 | # api_hash from https://my.telegram.org, under API Development. 44 | api_id = 12345 45 | api_hash = '0123456789abcdef0123456789abcdef' 46 | 47 | client = TelegramClient('session_name', api_id, api_hash) 48 | client.start() 49 | 50 | 51 | Doing stuff 52 | ----------- 53 | 54 | .. code-block:: python 55 | 56 | print(client.get_me().stringify()) 57 | 58 | client.send_message('username', 'Hello! Talking to you from Telethon') 59 | client.send_file('username', '/home/myself/Pictures/holidays.jpg') 60 | 61 | client.download_profile_photo('me') 62 | messages = client.get_messages('username') 63 | messages[0].download_media() 64 | 65 | @client.on(events.NewMessage(pattern='(?i)hi|hello')) 66 | async def handler(event): 67 | await event.respond('Hey!') 68 | 69 | 70 | Next steps 71 | ---------- 72 | 73 | Do you like how Telethon looks? Check out `Read The Docs`_ for a more 74 | in-depth explanation, with examples, troubleshooting issues, and more 75 | useful information. 76 | 77 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 78 | .. _MTProto: https://core.telegram.org/mtproto 79 | .. _Telegram: https://telegram.org 80 | .. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html 81 | .. _Telegram's ToS: https://core.telegram.org/api/terms 82 | .. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library 83 | .. _Read The Docs: https://docs.telethon.dev 84 | 85 | .. |logo| image:: logo.svg 86 | :width: 24pt 87 | :height: 24pt 88 | -------------------------------------------------------------------------------- /tests/telethon/test_helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | tests for telethon.helpers 3 | """ 4 | 5 | from base64 import b64decode 6 | 7 | import pytest 8 | 9 | from telethon import helpers 10 | from telethon.utils import get_inner_text 11 | from telethon.tl.types import MessageEntityUnknown as Meu 12 | 13 | 14 | def test_strip_text(): 15 | text = ' text ' 16 | text_stripped = 'text' 17 | entities_before_and_after = ( 18 | ([], []), 19 | ([Meu(i, 0) for i in range(10)], []), # del '' 20 | ([Meu(0, 0), Meu(0, 1), Meu(5, 1)], []), # del '', ' ', ' ' 21 | ([Meu(0, 3)], [Meu(0, 2)]), # ' te' -> 'te' 22 | ([Meu(3, 1)], [Meu(2, 1)]), # 'x' 23 | ([Meu(3, 2)], [Meu(2, 2)]), # 'xt' 24 | ([Meu(3, 3)], [Meu(2, 2)]), # 'xt ' -> 'xt' 25 | ([Meu(0, 6)], [Meu(0, 4)]), # ' text ' -> 'text' 26 | ) 27 | for entities_before, entities_expected in entities_before_and_after: 28 | entities_for_test = [Meu(meu.offset, meu.length) for meu in entities_before] # deep copy 29 | text_after = helpers.strip_text(text, entities_for_test) 30 | assert text_after == text_stripped 31 | assert sorted((e.offset, e.length) for e in entities_for_test) \ 32 | == sorted((e.offset, e.length) for e in entities_expected) 33 | inner_text_before = get_inner_text(text, entities_before) 34 | inner_text_before_stripped = [t.strip() for t in inner_text_before] 35 | inner_text_after = get_inner_text(text_after, entities_for_test) 36 | for t in inner_text_after: 37 | assert t in inner_text_before_stripped 38 | 39 | 40 | class TestSyncifyAsyncContext: 41 | class NoopContextManager: 42 | def __init__(self, loop): 43 | self.count = 0 44 | self.loop = loop 45 | 46 | async def __aenter__(self): 47 | self.count += 1 48 | return self 49 | 50 | async def __aexit__(self, exc_type, *args): 51 | assert exc_type is None 52 | self.count -= 1 53 | 54 | __enter__ = helpers._sync_enter 55 | __exit__ = helpers._sync_exit 56 | 57 | def test_sync_acontext(self, event_loop): 58 | contm = self.NoopContextManager(event_loop) 59 | assert contm.count == 0 60 | 61 | with contm: 62 | assert contm.count == 1 63 | 64 | assert contm.count == 0 65 | 66 | @pytest.mark.asyncio 67 | async def test_async_acontext(self, event_loop): 68 | contm = self.NoopContextManager(event_loop) 69 | assert contm.count == 0 70 | 71 | async with contm: 72 | assert contm.count == 1 73 | 74 | assert contm.count == 0 75 | 76 | 77 | def test_generate_key_data_from_nonce(): 78 | gkdfn = helpers.generate_key_data_from_nonce 79 | 80 | key_expect = b64decode(b'NFwRFB8Knw/kAmvPWjtrQauWysHClVfQh0UOAaABqZA=') 81 | nonce_expect = b64decode(b'1AgjhU9eDvJRjFik73bjR2zZEATzL/jLu9yodYfWEgA=') 82 | assert gkdfn(123456789, 1234567) == (key_expect, nonce_expect) 83 | -------------------------------------------------------------------------------- /tests/telethon/extensions/test_markdown.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `telethon.extensions.markdown`. 3 | """ 4 | from telethon.extensions import markdown 5 | from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl 6 | 7 | 8 | def test_entity_edges(): 9 | """ 10 | Test that entities at the edges (start and end) don't crash. 11 | """ 12 | text = 'Hello, world' 13 | entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] 14 | result = markdown.unparse(text, entities) 15 | assert result == '**Hello**, **world**' 16 | 17 | 18 | def test_malformed_entities(): 19 | """ 20 | Test that malformed entity offsets from bad clients 21 | don't crash and produce the expected results. 22 | """ 23 | text = '🏆Telegram Official Android Challenge is over🏆.' 24 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] 25 | result = markdown.unparse(text, entities) 26 | assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆." 27 | 28 | 29 | def test_trailing_malformed_entities(): 30 | """ 31 | Similar to `test_malformed_entities`, but for the edge 32 | case where the malformed entity offset is right at the end 33 | (note the lack of a trailing dot in the text string). 34 | """ 35 | text = '🏆Telegram Official Android Challenge is over🏆' 36 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] 37 | result = markdown.unparse(text, entities) 38 | assert result == "🏆[Telegram Official Android Challenge is over](https://example.com)🏆" 39 | 40 | 41 | def test_entities_together(): 42 | """ 43 | Test that an entity followed immediately by a different one behaves well. 44 | """ 45 | original = '**⚙️**__Settings__' 46 | stripped = '⚙️Settings' 47 | 48 | text, entities = markdown.parse(original) 49 | assert text == stripped 50 | assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] 51 | 52 | text = markdown.unparse(text, entities) 53 | assert text == original 54 | 55 | 56 | def test_nested_entities(): 57 | """ 58 | Test that an entity nested inside another one behaves well. 59 | """ 60 | original = '**[Example](https://example.com)**' 61 | stripped = 'Example' 62 | 63 | text, entities = markdown.parse(original) 64 | assert text == stripped 65 | assert entities == [MessageEntityBold(0, 7), MessageEntityTextUrl(0, 7, url='https://example.com')] 66 | 67 | text = markdown.unparse(text, entities) 68 | assert text == original 69 | 70 | 71 | def test_offset_at_emoji(): 72 | """ 73 | Tests that an entity starting at a emoji preserves the emoji. 74 | """ 75 | text = 'Hi\n👉 See example' 76 | entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] 77 | parsed = '**Hi**\n__👉__ See **example**' 78 | 79 | assert markdown.parse(parsed) == (text, entities) 80 | assert markdown.unparse(text, entities) == parsed 81 | -------------------------------------------------------------------------------- /telethon/tl/custom/inlineresults.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .inlineresult import InlineResult 4 | 5 | 6 | class InlineResults(list): 7 | """ 8 | Custom class that encapsulates :tl:`BotResults` providing 9 | an abstraction to easily access some commonly needed features 10 | (such as clicking one of the results to select it) 11 | 12 | Note that this is a list of `InlineResult 13 | ` 14 | so you can iterate over it or use indices to 15 | access its elements. In addition, it has some 16 | attributes. 17 | 18 | Attributes: 19 | result (:tl:`BotResults`): 20 | The original :tl:`BotResults` object. 21 | 22 | query_id (`int`): 23 | The random ID that identifies this query. 24 | 25 | cache_time (`int`): 26 | For how long the results should be considered 27 | valid. You can call `results_valid` at any 28 | moment to determine if the results are still 29 | valid or not. 30 | 31 | users (:tl:`User`): 32 | The users present in this inline query. 33 | 34 | gallery (`bool`): 35 | Whether these results should be presented 36 | in a grid (as a gallery of images) or not. 37 | 38 | next_offset (`str`, optional): 39 | The string to be used as an offset to get 40 | the next chunk of results, if any. 41 | 42 | switch_pm (:tl:`InlineBotSwitchPM`, optional): 43 | If presents, the results should show a button to 44 | switch to a private conversation with the bot using 45 | the text in this object. 46 | """ 47 | def __init__(self, client, original, *, entity=None): 48 | super().__init__(InlineResult(client, x, original.query_id, entity=entity) 49 | for x in original.results) 50 | 51 | self.result = original 52 | self.query_id = original.query_id 53 | self.cache_time = original.cache_time 54 | self._valid_until = time.time() + self.cache_time 55 | self.users = original.users 56 | self.gallery = bool(original.gallery) 57 | self.next_offset = original.next_offset 58 | self.switch_pm = original.switch_pm 59 | 60 | def results_valid(self): 61 | """ 62 | Returns `True` if the cache time has not expired 63 | yet and the results can still be considered valid. 64 | """ 65 | return time.time() < self._valid_until 66 | 67 | def _to_str(self, item_function): 68 | return ('[{}, query_id={}, cache_time={}, users={}, gallery={}, ' 69 | 'next_offset={}, switch_pm={}]'.format( 70 | ', '.join(item_function(x) for x in self), 71 | self.query_id, 72 | self.cache_time, 73 | self.users, 74 | self.gallery, 75 | self.next_offset, 76 | self.switch_pm 77 | )) 78 | 79 | def __str__(self): 80 | return self._to_str(str) 81 | 82 | def __repr__(self): 83 | return self._to_str(repr) 84 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report about a bug inside the library. 3 | body: 4 | 5 | - type: textarea 6 | id: reproducing-example 7 | attributes: 8 | label: Code that causes the issue 9 | description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies. 10 | placeholder: | 11 | ```python 12 | from telethon.sync import TelegramClient 13 | ... 14 | 15 | ``` 16 | validations: 17 | required: true 18 | 19 | - type: textarea 20 | id: expected-behavior 21 | attributes: 22 | label: Expected behavior 23 | description: Explain what you should expect to happen. Include reproduction steps. 24 | placeholder: | 25 | "I was doing... I was expecting the following to happen..." 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: actual-behavior 31 | attributes: 32 | label: Actual behavior 33 | description: Explain what actually happens. 34 | placeholder: | 35 | "This happened instead..." 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: traceback 41 | attributes: 42 | label: Traceback 43 | description: | 44 | The traceback, if the problem is a crash. 45 | placeholder: | 46 | ``` 47 | Traceback (most recent call last): 48 | File "code.py", line 1, in 49 | 50 | ``` 51 | 52 | - type: input 53 | id: telethon-version 54 | attributes: 55 | label: Telethon version 56 | description: The output of `python -c "import telethon; print(telethon.__version__)"`. 57 | placeholder: "1.x" 58 | validations: 59 | required: true 60 | 61 | - type: input 62 | id: python-version 63 | attributes: 64 | label: Python version 65 | description: The output of `python --version`. 66 | placeholder: "3.x" 67 | validations: 68 | required: true 69 | 70 | - type: input 71 | id: os 72 | attributes: 73 | label: Operating system (including distribution name and version) 74 | placeholder: Windows 11, macOS 13.4, Ubuntu 23.04... 75 | validations: 76 | required: true 77 | 78 | - type: textarea 79 | id: other-details 80 | attributes: 81 | label: Other details 82 | placeholder: | 83 | Additional details and attachments. Is it a server? Network condition? 84 | 85 | - type: checkboxes 86 | id: checklist 87 | attributes: 88 | label: Checklist 89 | description: Read this carefully, we will close and ignore your issue if you skimmed through this. 90 | options: 91 | - label: The error is in the library's code, and not in my own. 92 | required: true 93 | - label: I have searched for this issue before posting it and there isn't an open duplicate. 94 | required: true 95 | - label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version. 96 | required: true 97 | -------------------------------------------------------------------------------- /readthedocs/concepts/strings.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | String-based Debugging 3 | ====================== 4 | 5 | Debugging is *really* important. Telegram's API is really big and there 6 | are a lot of things that you should know. Such as, what attributes or fields 7 | does a result have? Well, the easiest thing to do is printing it: 8 | 9 | .. code-block:: python 10 | 11 | entity = await client.get_entity('username') 12 | print(entity) 13 | 14 | That will show a huge **string** similar to the following: 15 | 16 | .. code-block:: python 17 | 18 | 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) 19 | 20 | That's a lot of text. But as you can see, all the properties are there. 21 | So if you want the title you **don't use regex** or anything like 22 | splitting ``str(entity)`` to get what you want. You just access the 23 | attribute you need: 24 | 25 | .. code-block:: python 26 | 27 | title = entity.title 28 | 29 | Can we get better than the shown string, though? Yes! 30 | 31 | .. code-block:: python 32 | 33 | print(entity.stringify()) 34 | 35 | Will show a much better representation: 36 | 37 | .. code-block:: python 38 | 39 | Channel( 40 | id=1066197625, 41 | title='Telegram Usernames', 42 | photo=ChatPhotoEmpty( 43 | ), 44 | date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), 45 | version=0, 46 | creator=False, 47 | left=True, 48 | broadcast=True, 49 | verified=True, 50 | megagroup=False, 51 | restricted=False, 52 | signatures=False, 53 | min=False, 54 | scam=False, 55 | has_link=False, 56 | has_geo=False, 57 | slowmode_enabled=False, 58 | access_hash=-6309373984955162244, 59 | username='username', 60 | restriction_reason=[ 61 | ], 62 | admin_rights=None, 63 | banned_rights=None, 64 | default_banned_rights=None, 65 | participants_count=None 66 | ) 67 | 68 | 69 | Now it's easy to see how we could get, for example, 70 | the ``year`` value. It's inside ``date``: 71 | 72 | .. code-block:: python 73 | 74 | channel_year = entity.date.year 75 | 76 | You don't need to print everything to see what all the possible values 77 | can be. You can just search in http://tl.telethon.dev/. 78 | 79 | Remember that you can use Python's `isinstance 80 | `_ 81 | to check the type of something. For example: 82 | 83 | .. code-block:: python 84 | 85 | from telethon import types 86 | 87 | if isinstance(entity.photo, types.ChatPhotoEmpty): 88 | print('Channel has no photo') 89 | -------------------------------------------------------------------------------- /tests/telethon/extensions/test_html.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for `telethon.extensions.html`. 3 | """ 4 | from telethon.extensions import html 5 | from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl 6 | 7 | 8 | def test_entity_edges(): 9 | """ 10 | Test that entities at the edges (start and end) don't crash. 11 | """ 12 | text = 'Hello, world' 13 | entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)] 14 | result = html.unparse(text, entities) 15 | assert result == 'Hello, world' 16 | 17 | 18 | def test_malformed_entities(): 19 | """ 20 | Test that malformed entity offsets from bad clients 21 | don't crash and produce the expected results. 22 | """ 23 | text = '🏆Telegram Official Android Challenge is over🏆.' 24 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] 25 | result = html.unparse(text, entities) 26 | assert result == '🏆Telegram Official Android Challenge is over🏆.' 27 | 28 | 29 | def test_trailing_malformed_entities(): 30 | """ 31 | Similar to `test_malformed_entities`, but for the edge 32 | case where the malformed entity offset is right at the end 33 | (note the lack of a trailing dot in the text string). 34 | """ 35 | text = '🏆Telegram Official Android Challenge is over🏆' 36 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')] 37 | result = html.unparse(text, entities) 38 | assert result == '🏆Telegram Official Android Challenge is over🏆' 39 | 40 | 41 | def test_entities_together(): 42 | """ 43 | Test that an entity followed immediately by a different one behaves well. 44 | """ 45 | original = '⚙️Settings' 46 | stripped = '⚙️Settings' 47 | 48 | text, entities = html.parse(original) 49 | assert text == stripped 50 | assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)] 51 | 52 | text = html.unparse(text, entities) 53 | assert text == original 54 | 55 | 56 | def test_nested_entities(): 57 | """ 58 | Test that an entity nested inside another one behaves well. 59 | """ 60 | original = 'Example' 61 | original_entities = [MessageEntityTextUrl(0, 7, url='https://example.com'), MessageEntityBold(0, 7)] 62 | stripped = 'Example' 63 | 64 | text, entities = html.parse(original) 65 | assert text == stripped 66 | assert entities == original_entities 67 | 68 | text = html.unparse(text, entities) 69 | assert text == original 70 | 71 | 72 | def test_offset_at_emoji(): 73 | """ 74 | Tests that an entity starting at a emoji preserves the emoji. 75 | """ 76 | text = 'Hi\n👉 See example' 77 | entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] 78 | parsed = 'Hi\n👉 See example' 79 | 80 | assert html.parse(parsed) == (text, entities) 81 | assert html.unparse(text, entities) == parsed 82 | -------------------------------------------------------------------------------- /telethon_generator/parsers/errors.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import re 3 | 4 | from ..utils import snake_to_camel_case 5 | 6 | # Core base classes depending on the integer error code 7 | KNOWN_BASE_CLASSES = { 8 | 303: 'InvalidDCError', 9 | 400: 'BadRequestError', 10 | 401: 'UnauthorizedError', 11 | 403: 'ForbiddenError', 12 | 404: 'NotFoundError', 13 | 406: 'AuthKeyError', 14 | 420: 'FloodError', 15 | 500: 'ServerError', 16 | 503: 'TimedOutError' 17 | } 18 | 19 | 20 | def _get_class_name(error_code): 21 | """ 22 | Gets the corresponding class name for the given error code, 23 | this either being an integer (thus base error name) or str. 24 | """ 25 | if isinstance(error_code, int): 26 | return KNOWN_BASE_CLASSES.get( 27 | abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg') 28 | ) 29 | 30 | if error_code.startswith('2'): 31 | error_code = re.sub(r'2', 'TWO_', error_code, count=1) 32 | 33 | if re.match(r'\d+', error_code): 34 | raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code)) 35 | 36 | return snake_to_camel_case( 37 | error_code.replace('FIRSTNAME', 'FIRST_NAME')\ 38 | .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error') 39 | 40 | 41 | class Error: 42 | def __init__(self, codes, name, description): 43 | # TODO Some errors have the same name but different integer codes 44 | # Should these be split into different files or doesn't really matter? 45 | # Telegram isn't exactly consistent with returned errors anyway. 46 | self.int_code = codes[0] 47 | self.int_codes = codes 48 | self.str_code = name 49 | self.subclass = _get_class_name(codes[0]) 50 | self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES 51 | self.description = description 52 | 53 | self.has_captures = '_X' in name 54 | if self.has_captures: 55 | self.name = _get_class_name(name.replace('_X', '_')) 56 | self.pattern = name.replace('_X', r'_(\d+)') 57 | self.capture_name = re.search(r'{(\w+)}', description).group(1) 58 | else: 59 | self.name = _get_class_name(name) 60 | self.pattern = name 61 | self.capture_name = None 62 | 63 | 64 | def parse_errors(csv_file): 65 | """ 66 | Parses the input CSV file with columns (name, error codes, description) 67 | and yields `Error` instances as a result. 68 | """ 69 | with csv_file.open(newline='') as f: 70 | f = csv.reader(f) 71 | next(f, None) # header 72 | for line, tup in enumerate(f, start=2): 73 | try: 74 | name, codes, description = tup 75 | except ValueError: 76 | raise ValueError('Columns count mismatch, unquoted comma in ' 77 | 'desc? (line {})'.format(line)) from None 78 | 79 | try: 80 | codes = [int(x) for x in codes.split()] or [400] 81 | except ValueError: 82 | raise ValueError('Not all codes are integers ' 83 | '(line {})'.format(line)) from None 84 | 85 | yield Error([int(x) for x in codes], name, description) 86 | -------------------------------------------------------------------------------- /tests/telethon/client/test_messages.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from unittest import mock 3 | from unittest.mock import MagicMock 4 | 5 | import pytest 6 | 7 | from telethon import TelegramClient 8 | from telethon.client import MessageMethods 9 | from telethon.tl.types import PeerChat, MessageMediaDocument, Message, MessageEntityBold 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_send_message_with_file_forwards_args(): 14 | arguments = {} 15 | sentinel = object() 16 | 17 | for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters): 18 | if name in {'self', 'entity', 'file'}: 19 | continue # positional 20 | 21 | if name in {'message'}: 22 | continue # renamed 23 | 24 | if name in {'link_preview'}: 25 | continue # make no sense in send_file 26 | 27 | arguments[name] = value 28 | 29 | class MockedClient(TelegramClient): 30 | # noinspection PyMissingConstructor 31 | def __init__(self): 32 | pass 33 | 34 | async def send_file(self, entity, file, **kwargs): 35 | assert entity == 'a' 36 | assert file == 'b' 37 | for k, v in arguments.items(): 38 | assert k in kwargs 39 | assert kwargs[k] == v 40 | 41 | return sentinel 42 | 43 | client = MockedClient() 44 | assert (await client.send_message('a', file='b', **arguments)) == sentinel 45 | 46 | 47 | class TestMessageMethods: 48 | @pytest.mark.asyncio 49 | @pytest.mark.parametrize( 50 | 'formatting_entities', 51 | ([MessageEntityBold(offset=0, length=0)], None) 52 | ) 53 | async def test_send_msg_and_file(self, formatting_entities): 54 | async def async_func(result): # AsyncMock was added only in 3.8 55 | return result 56 | msg_methods = MessageMethods() 57 | expected_result = Message( 58 | id=0, peer_id=PeerChat(chat_id=0), message='', date=None, 59 | ) 60 | entity = 'test_entity' 61 | message = Message( 62 | id=1, peer_id=PeerChat(chat_id=0), message='expected_caption', date=None, 63 | entities=[MessageEntityBold(offset=9, length=9)], 64 | ) 65 | media_file = MessageMediaDocument() 66 | 67 | with mock.patch.object( 68 | target=MessageMethods, attribute='send_file', 69 | new=MagicMock(return_value=async_func(expected_result)), create=True, 70 | ) as mock_obj: 71 | result = await msg_methods.send_message( 72 | entity=entity, message=message, file=media_file, 73 | formatting_entities=formatting_entities, 74 | ) 75 | mock_obj.assert_called_once_with( 76 | entity, media_file, caption=message.message, 77 | formatting_entities=formatting_entities or message.entities, 78 | reply_to=None, silent=None, attributes=None, parse_mode=(), 79 | force_document=False, thumb=None, buttons=None, 80 | clear_draft=False, schedule=None, supports_streaming=False, 81 | comment_to=None, background=None, nosound_video=None, 82 | send_as=None, message_effect_id=None, 83 | ) 84 | assert result == expected_result 85 | -------------------------------------------------------------------------------- /readthedocs/modules/custom.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Custom package 3 | ============== 4 | 5 | The `telethon.tl.custom` package contains custom classes that the library 6 | uses in order to make working with Telegram easier. Only those that you 7 | are supposed to use will be documented here. You can use undocumented ones 8 | at your own risk. 9 | 10 | More often than not, you don't need to import these (unless you want 11 | type hinting), nor do you need to manually create instances of these 12 | classes. They are returned by client methods. 13 | 14 | .. contents:: 15 | 16 | .. automodule:: telethon.tl.custom 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | 21 | 22 | AdminLogEvent 23 | ============= 24 | 25 | .. automodule:: telethon.tl.custom.adminlogevent 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | 31 | Button 32 | ====== 33 | 34 | .. automodule:: telethon.tl.custom.button 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | ChatGetter 41 | ========== 42 | 43 | .. automodule:: telethon.tl.custom.chatgetter 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | 49 | Conversation 50 | ============ 51 | 52 | .. automodule:: telethon.tl.custom.conversation 53 | :members: 54 | :undoc-members: 55 | :show-inheritance: 56 | 57 | 58 | Dialog 59 | ====== 60 | 61 | .. automodule:: telethon.tl.custom.dialog 62 | :members: 63 | :undoc-members: 64 | :show-inheritance: 65 | 66 | 67 | Draft 68 | ===== 69 | 70 | .. automodule:: telethon.tl.custom.draft 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | 76 | File 77 | ==== 78 | 79 | .. automodule:: telethon.tl.custom.file 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | 84 | 85 | Forward 86 | ======= 87 | 88 | .. automodule:: telethon.tl.custom.forward 89 | :members: 90 | :undoc-members: 91 | :show-inheritance: 92 | 93 | 94 | InlineBuilder 95 | ============= 96 | 97 | .. automodule:: telethon.tl.custom.inlinebuilder 98 | :members: 99 | :undoc-members: 100 | :show-inheritance: 101 | 102 | 103 | InlineResult 104 | ============ 105 | 106 | .. automodule:: telethon.tl.custom.inlineresult 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | 112 | InlineResults 113 | ============= 114 | 115 | .. automodule:: telethon.tl.custom.inlineresults 116 | :members: 117 | :undoc-members: 118 | :show-inheritance: 119 | 120 | 121 | Message 122 | ======= 123 | 124 | .. automodule:: telethon.tl.custom.message 125 | :members: 126 | :undoc-members: 127 | :show-inheritance: 128 | 129 | 130 | MessageButton 131 | ============= 132 | 133 | .. automodule:: telethon.tl.custom.messagebutton 134 | :members: 135 | :undoc-members: 136 | :show-inheritance: 137 | 138 | 139 | ParticipantPermissions 140 | ====================== 141 | 142 | .. automodule:: telethon.tl.custom.participantpermissions 143 | :members: 144 | :undoc-members: 145 | :show-inheritance: 146 | 147 | 148 | QRLogin 149 | ======= 150 | 151 | .. automodule:: telethon.tl.custom.qrlogin 152 | :members: 153 | :undoc-members: 154 | :show-inheritance: 155 | 156 | 157 | SenderGetter 158 | ============ 159 | 160 | .. automodule:: telethon.tl.custom.sendergetter 161 | :members: 162 | :undoc-members: 163 | :show-inheritance: 164 | -------------------------------------------------------------------------------- /readthedocs/basic/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | Telethon is a Python library, which means you need to download and install 8 | Python from https://www.python.org/downloads/ if you haven't already. Once 9 | you have Python installed, `upgrade pip`__ and run: 10 | 11 | .. code-block:: sh 12 | 13 | python3 -m pip install --upgrade pip 14 | python3 -m pip install --upgrade telethon 15 | 16 | …to install or upgrade the library to the latest version. 17 | 18 | .. __: https://pythonspeed.com/articles/upgrade-pip/ 19 | 20 | Installing Development Versions 21 | =============================== 22 | 23 | If you want the *latest* unreleased changes, 24 | you can run the following command instead: 25 | 26 | .. code-block:: sh 27 | 28 | python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip 29 | 30 | .. note:: 31 | 32 | The development version may have bugs and is not recommended for production 33 | use. However, when you are `reporting a library bug`__, you should try if the 34 | bug still occurs in this version. 35 | 36 | .. __: https://github.com/LonamiWebs/Telethon/issues/ 37 | 38 | 39 | Verification 40 | ============ 41 | 42 | To verify that the library is installed correctly, run the following command: 43 | 44 | .. code-block:: sh 45 | 46 | python3 -c "import telethon; print(telethon.__version__)" 47 | 48 | The version number of the library should show in the output. 49 | 50 | 51 | Optional Dependencies 52 | ===================== 53 | 54 | If cryptg_ is installed, **the library will work a lot faster**, since 55 | encryption and decryption will be made in C instead of Python. If your 56 | code deals with a lot of updates or you are downloading/uploading a lot 57 | of files, you will notice a considerable speed-up (from a hundred kilobytes 58 | per second to several megabytes per second, if your connection allows it). 59 | If it's not installed, pyaes_ will be used (which is pure Python, so it's 60 | much slower). 61 | 62 | If pillow_ is installed, large images will be automatically resized when 63 | sending photos to prevent Telegram from failing with "invalid image". 64 | Official clients also do this. 65 | 66 | If aiohttp_ is installed, the library will be able to download 67 | :tl:`WebDocument` media files (otherwise you will get an error). 68 | 69 | If hachoir_ is installed, it will be used to extract metadata from files 70 | when sending documents. Telegram uses this information to show the song's 71 | performer, artist, title, duration, and for videos too (including size). 72 | Otherwise, they will default to empty values, and you can set the attributes 73 | manually. 74 | 75 | .. note:: 76 | 77 | Some of the modules may require additional dependencies before being 78 | installed through ``pip``. If you have an ``apt``-based system, consider 79 | installing the most commonly missing dependencies (with the right ``pip``): 80 | 81 | .. code-block:: sh 82 | 83 | apt update 84 | apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev 85 | pip install -U --user setuptools 86 | pip install -U --user telethon cryptg pillow 87 | 88 | Thanks to `@bb010g`_ for writing down this nice list. 89 | 90 | 91 | .. _cryptg: https://github.com/cher-nov/cryptg 92 | .. _pyaes: https://github.com/ricmoo/pyaes 93 | .. _pillow: https://python-pillow.org 94 | .. _aiohttp: https://docs.aiohttp.org 95 | .. _hachoir: https://hachoir.readthedocs.io 96 | .. _@bb010g: https://static.bb010g.com 97 | -------------------------------------------------------------------------------- /readthedocs/index.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | Telethon's Documentation 3 | ======================== 4 | 5 | .. code-block:: python 6 | 7 | from telethon.sync import TelegramClient, events 8 | 9 | with TelegramClient('name', api_id, api_hash) as client: 10 | client.send_message('me', 'Hello, myself!') 11 | print(client.download_profile_photo('me')) 12 | 13 | @client.on(events.NewMessage(pattern='(?i).*Hello')) 14 | async def handler(event): 15 | await event.reply('Hey!') 16 | 17 | client.run_until_disconnected() 18 | 19 | 20 | * Are you new here? Jump straight into :ref:`installation`! 21 | * Looking for the method reference? See :ref:`client-ref`. 22 | * Did you upgrade the library? Please read :ref:`changelog`. 23 | * Used Telethon before v1.0? See :ref:`compatibility-and-convenience`. 24 | * Coming from Bot API or want to create new bots? See :ref:`botapi`. 25 | * Need the full API reference? https://tl.telethon.dev/. 26 | 27 | 28 | What is this? 29 | ------------- 30 | 31 | Telegram is a popular messaging application. This library is meant 32 | to make it easy for you to write Python programs that can interact 33 | with Telegram. Think of it as a wrapper that has already done the 34 | heavy job for you, so you can focus on developing an application. 35 | 36 | 37 | How should I use the documentation? 38 | ----------------------------------- 39 | 40 | If you are getting started with the library, you should follow the 41 | documentation in order by pressing the "Next" button at the bottom-right 42 | of every page. 43 | 44 | You can also use the menu on the left to quickly skip over sections. 45 | 46 | .. toctree:: 47 | :hidden: 48 | :caption: First Steps 49 | 50 | basic/installation 51 | basic/signing-in 52 | basic/quick-start 53 | basic/updates 54 | basic/next-steps 55 | 56 | .. toctree:: 57 | :hidden: 58 | :caption: Quick References 59 | 60 | quick-references/faq 61 | quick-references/client-reference 62 | quick-references/events-reference 63 | quick-references/objects-reference 64 | 65 | .. toctree:: 66 | :hidden: 67 | :caption: Concepts 68 | 69 | concepts/strings 70 | concepts/entities 71 | concepts/chats-vs-channels 72 | concepts/updates 73 | concepts/sessions 74 | concepts/full-api 75 | concepts/errors 76 | concepts/botapi-vs-mtproto 77 | concepts/asyncio 78 | 79 | .. toctree:: 80 | :hidden: 81 | :caption: Full API Examples 82 | 83 | examples/word-of-warning 84 | examples/chats-and-channels 85 | examples/users 86 | examples/working-with-messages 87 | 88 | .. toctree:: 89 | :hidden: 90 | :caption: Developing 91 | 92 | developing/philosophy.rst 93 | developing/test-servers.rst 94 | developing/project-structure.rst 95 | developing/coding-style.rst 96 | developing/testing.rst 97 | developing/understanding-the-type-language.rst 98 | developing/tips-for-porting-the-project.rst 99 | developing/telegram-api-in-other-languages.rst 100 | 101 | .. toctree:: 102 | :hidden: 103 | :caption: Miscellaneous 104 | 105 | misc/changelog 106 | misc/compatibility-and-convenience 107 | 108 | .. toctree:: 109 | :hidden: 110 | :caption: Telethon Modules 111 | 112 | modules/client 113 | modules/events 114 | modules/custom 115 | modules/utils 116 | modules/errors 117 | modules/sessions 118 | modules/network 119 | modules/helpers 120 | -------------------------------------------------------------------------------- /telethon_examples/replier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | A example script to automatically send messages based on certain triggers. 4 | 5 | This script assumes that you have certain files on the working directory, 6 | such as "xfiles.m4a" or "anytime.png" for some of the automated replies. 7 | """ 8 | import os 9 | import sys 10 | import time 11 | from collections import defaultdict 12 | 13 | from telethon import TelegramClient, events 14 | 15 | import logging 16 | logging.basicConfig(level=logging.WARNING) 17 | 18 | # "When did we last react?" dictionary, 0.0 by default 19 | recent_reacts = defaultdict(float) 20 | 21 | 22 | def get_env(name, message, cast=str): 23 | if name in os.environ: 24 | return os.environ[name] 25 | while True: 26 | value = input(message) 27 | try: 28 | return cast(value) 29 | except ValueError as e: 30 | print(e, file=sys.stderr) 31 | time.sleep(1) 32 | 33 | 34 | def can_react(chat_id): 35 | # Get the time when we last sent a reaction (or 0) 36 | last = recent_reacts[chat_id] 37 | 38 | # Get the current time 39 | now = time.time() 40 | 41 | # If 10 minutes as seconds have passed, we can react 42 | if now - last < 10 * 60: 43 | # Make sure we updated the last reaction time 44 | recent_reacts[chat_id] = now 45 | return True 46 | else: 47 | return False 48 | 49 | 50 | # Register `events.NewMessage` before defining the client. 51 | # Once you have a client, `add_event_handler` will use this event. 52 | @events.register(events.NewMessage) 53 | async def handler(event): 54 | # There are better ways to do this, but this is simple. 55 | # If the message is not outgoing (i.e. someone else sent it) 56 | if not event.out: 57 | if 'emacs' in event.raw_text: 58 | if can_react(event.chat_id): 59 | await event.reply('> emacs\nneeds more vim') 60 | 61 | elif 'vim' in event.raw_text: 62 | if can_react(event.chat_id): 63 | await event.reply('> vim\nneeds more emacs') 64 | 65 | elif 'chrome' in event.raw_text: 66 | if can_react(event.chat_id): 67 | await event.reply('> chrome\nneeds more firefox') 68 | 69 | # Reply always responds as a reply. We can respond without replying too 70 | if 'shrug' in event.raw_text: 71 | if can_react(event.chat_id): 72 | await event.respond(r'¯\_(ツ)_/¯') 73 | 74 | # We can also use client methods from here 75 | client = event.client 76 | 77 | # If we sent the message, we are replying to someone, 78 | # and we said "save pic" in the message 79 | if event.out and event.is_reply and 'save pic' in event.raw_text: 80 | reply_msg = await event.get_reply_message() 81 | replied_to_user = await reply_msg.get_input_sender() 82 | 83 | message = await event.reply('Downloading your profile photo...') 84 | file = await client.download_profile_photo(replied_to_user) 85 | await message.edit('I saved your photo in {}'.format(file)) 86 | 87 | 88 | client = TelegramClient( 89 | os.environ.get('TG_SESSION', 'replier'), 90 | get_env('TG_API_ID', 'Enter your API ID: ', int), 91 | get_env('TG_API_HASH', 'Enter your API hash: '), 92 | proxy=None 93 | ) 94 | 95 | with client: 96 | # This remembers the events.NewMessage we registered before 97 | client.add_event_handler(handler) 98 | 99 | print('(Press Ctrl+C to stop this)') 100 | client.run_until_disconnected() 101 | -------------------------------------------------------------------------------- /telethon/crypto/aes.py: -------------------------------------------------------------------------------- 1 | """ 2 | AES IGE implementation in Python. 3 | 4 | If available, cryptg will be used instead, otherwise 5 | if available, libssl will be used instead, otherwise 6 | the Python implementation will be used. 7 | """ 8 | import os 9 | import pyaes 10 | import logging 11 | from . import libssl 12 | 13 | 14 | __log__ = logging.getLogger(__name__) 15 | 16 | 17 | try: 18 | import cryptg 19 | __log__.info('cryptg detected, it will be used for encryption') 20 | except ImportError: 21 | cryptg = None 22 | if libssl.encrypt_ige and libssl.decrypt_ige: 23 | __log__.info('libssl detected, it will be used for encryption') 24 | else: 25 | __log__.info('cryptg module not installed and libssl not found, ' 26 | 'falling back to (slower) Python encryption') 27 | 28 | 29 | class AES: 30 | """ 31 | Class that servers as an interface to encrypt and decrypt 32 | text through the AES IGE mode. 33 | """ 34 | @staticmethod 35 | def decrypt_ige(cipher_text, key, iv): 36 | """ 37 | Decrypts the given text in 16-bytes blocks by using the 38 | given key and 32-bytes initialization vector. 39 | """ 40 | if cryptg: 41 | return cryptg.decrypt_ige(cipher_text, key, iv) 42 | if libssl.decrypt_ige: 43 | return libssl.decrypt_ige(cipher_text, key, iv) 44 | 45 | iv1 = iv[:len(iv) // 2] 46 | iv2 = iv[len(iv) // 2:] 47 | 48 | aes = pyaes.AES(key) 49 | 50 | plain_text = [] 51 | blocks_count = len(cipher_text) // 16 52 | 53 | cipher_text_block = [0] * 16 54 | for block_index in range(blocks_count): 55 | for i in range(16): 56 | cipher_text_block[i] = \ 57 | cipher_text[block_index * 16 + i] ^ iv2[i] 58 | 59 | plain_text_block = aes.decrypt(cipher_text_block) 60 | 61 | for i in range(16): 62 | plain_text_block[i] ^= iv1[i] 63 | 64 | iv1 = cipher_text[block_index * 16:block_index * 16 + 16] 65 | iv2 = plain_text_block 66 | 67 | plain_text.extend(plain_text_block) 68 | 69 | return bytes(plain_text) 70 | 71 | @staticmethod 72 | def encrypt_ige(plain_text, key, iv): 73 | """ 74 | Encrypts the given text in 16-bytes blocks by using the 75 | given key and 32-bytes initialization vector. 76 | """ 77 | padding = len(plain_text) % 16 78 | if padding: 79 | plain_text += os.urandom(16 - padding) 80 | 81 | if cryptg: 82 | return cryptg.encrypt_ige(plain_text, key, iv) 83 | if libssl.encrypt_ige: 84 | return libssl.encrypt_ige(plain_text, key, iv) 85 | 86 | iv1 = iv[:len(iv) // 2] 87 | iv2 = iv[len(iv) // 2:] 88 | 89 | aes = pyaes.AES(key) 90 | 91 | cipher_text = [] 92 | blocks_count = len(plain_text) // 16 93 | 94 | for block_index in range(blocks_count): 95 | plain_text_block = list( 96 | plain_text[block_index * 16:block_index * 16 + 16] 97 | ) 98 | for i in range(16): 99 | plain_text_block[i] ^= iv1[i] 100 | 101 | cipher_text_block = aes.encrypt(plain_text_block) 102 | 103 | for i in range(16): 104 | cipher_text_block[i] ^= iv2[i] 105 | 106 | iv1 = cipher_text_block 107 | iv2 = plain_text[block_index * 16:block_index * 16 + 16] 108 | 109 | cipher_text.extend(cipher_text_block) 110 | 111 | return bytes(cipher_text) 112 | -------------------------------------------------------------------------------- /telethon_generator/data/html/css/docs.light.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Nunito', sans-serif; 3 | color: #333; 4 | background-color:#eee; 5 | font-size: 16px; 6 | } 7 | 8 | a { 9 | color: #329add; 10 | text-decoration: none; 11 | } 12 | 13 | pre { 14 | font-family: 'Source Code Pro', monospace; 15 | padding: 8px; 16 | color: #567; 17 | background: #e0e4e8; 18 | border-radius: 0; 19 | overflow-x: auto; 20 | } 21 | 22 | a:hover { 23 | color: #64bbdd; 24 | text-decoration: underline; 25 | } 26 | 27 | table { 28 | width: 100%; 29 | max-width: 100%; 30 | } 31 | 32 | table td { 33 | border-top: 1px solid #ddd; 34 | padding: 8px; 35 | } 36 | 37 | .horizontal { 38 | margin-bottom: 16px; 39 | list-style: none; 40 | background: #e0e4e8; 41 | border-radius: 4px; 42 | padding: 8px 16px; 43 | } 44 | 45 | .horizontal li { 46 | display: inline-block; 47 | margin: 0 8px 0 0; 48 | } 49 | 50 | .horizontal img { 51 | display: inline-block; 52 | margin: 0 8px -2px 0; 53 | } 54 | 55 | h1, summary.title { 56 | font-size: 24px; 57 | } 58 | 59 | h3 { 60 | font-size: 20px; 61 | } 62 | 63 | #main_div { 64 | padding: 20px 0; 65 | max-width: 800px; 66 | margin: 0 auto; 67 | } 68 | 69 | pre::-webkit-scrollbar { 70 | visibility: visible; 71 | display: block; 72 | height: 12px; 73 | } 74 | 75 | pre::-webkit-scrollbar-track:horizontal { 76 | background: #def; 77 | border-radius: 0; 78 | height: 12px; 79 | } 80 | 81 | pre::-webkit-scrollbar-thumb:horizontal { 82 | background: #bdd; 83 | border-radius: 0; 84 | height: 12px; 85 | } 86 | 87 | :target { 88 | border: 2px solid #f8f800; 89 | background: #f8f8f8; 90 | padding: 4px; 91 | } 92 | 93 | /* 'sh' stands for Syntax Highlight */ 94 | span.sh1 { 95 | color: #f70; 96 | } 97 | 98 | span.tooltip { 99 | border-bottom: 1px dashed #444; 100 | } 101 | 102 | #searchBox { 103 | width: 100%; 104 | border: none; 105 | height: 20px; 106 | padding: 8px; 107 | font-size: 16px; 108 | border-radius: 2px; 109 | border: 2px solid #ddd; 110 | } 111 | 112 | #searchBox:placeholder-shown { 113 | font-style: italic; 114 | } 115 | 116 | button { 117 | border-radius: 2px; 118 | font-size: 16px; 119 | padding: 8px; 120 | color: #000; 121 | background-color: #f7f7f7; 122 | border: 2px solid #329add; 123 | transition-duration: 300ms; 124 | } 125 | 126 | button:hover { 127 | background-color: #329add; 128 | color: #f7f7f7; 129 | } 130 | 131 | /* https://www.w3schools.com/css/css_navbar.asp */ 132 | ul.together { 133 | list-style-type: none; 134 | margin: 0; 135 | padding: 0; 136 | overflow: hidden; 137 | } 138 | 139 | ul.together li { 140 | float: left; 141 | } 142 | 143 | ul.together li a { 144 | display: block; 145 | border-radius: 8px; 146 | background: #e0e4e8; 147 | padding: 4px 8px; 148 | margin: 8px; 149 | } 150 | 151 | /* https://stackoverflow.com/a/30810322 */ 152 | .invisible { 153 | left: 0; 154 | top: -99px; 155 | padding: 0; 156 | width: 2em; 157 | height: 2em; 158 | border: none; 159 | outline: none; 160 | position: fixed; 161 | box-shadow: none; 162 | color: transparent; 163 | background: transparent; 164 | } 165 | 166 | @media (max-width: 640px) { 167 | h1, summary.title { 168 | font-size: 18px; 169 | } 170 | h3 { 171 | font-size: 16px; 172 | } 173 | 174 | #dev_page_content_wrap { 175 | padding-top: 12px; 176 | } 177 | 178 | #dev_page_title { 179 | margin-top: 10px; 180 | margin-bottom: 20px; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /telethon/client/buttons.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from .. import utils, hints 4 | from ..tl import types, custom 5 | 6 | 7 | class ButtonMethods: 8 | @staticmethod 9 | def build_reply_markup( 10 | buttons: 'typing.Optional[hints.MarkupLike]' 11 | ) -> 'typing.Optional[types.TypeReplyMarkup]': 12 | """ 13 | Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for 14 | the given buttons. 15 | 16 | Does nothing if either no buttons are provided or the provided 17 | argument is already a reply markup. 18 | 19 | You should consider using this method if you are going to reuse 20 | the markup very often. Otherwise, it is not necessary. 21 | 22 | This method is **not** asynchronous (don't use ``await`` on it). 23 | 24 | Arguments 25 | buttons (`hints.MarkupLike`): 26 | The button, list of buttons, array of buttons or markup 27 | to convert into a markup. 28 | 29 | Example 30 | .. code-block:: python 31 | 32 | from telethon import Button 33 | 34 | markup = client.build_reply_markup(Button.inline('hi')) 35 | # later 36 | await client.send_message(chat, 'click me', buttons=markup) 37 | """ 38 | if buttons is None: 39 | return None 40 | 41 | try: 42 | if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'): 43 | return buttons 44 | except AttributeError: 45 | pass 46 | 47 | if not utils.is_list_like(buttons): 48 | buttons = [[buttons]] 49 | elif not buttons or not utils.is_list_like(buttons[0]): 50 | buttons = [buttons] 51 | 52 | is_inline = False 53 | is_normal = False 54 | resize = None 55 | single_use = None 56 | selective = None 57 | persistent = None 58 | placeholder = None 59 | 60 | rows = [] 61 | for row in buttons: 62 | current = [] 63 | for button in row: 64 | if isinstance(button, custom.Button): 65 | if button.resize is not None: 66 | resize = button.resize 67 | if button.single_use is not None: 68 | single_use = button.single_use 69 | if button.selective is not None: 70 | selective = button.selective 71 | if button.persistent is not None: 72 | persistent = button.persistent 73 | if button.placeholder is not None: 74 | placeholder = button.placeholder 75 | 76 | button = button.button 77 | elif isinstance(button, custom.MessageButton): 78 | button = button.button 79 | 80 | inline = custom.Button._is_inline(button) 81 | is_inline |= inline 82 | is_normal |= not inline 83 | 84 | if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton') 85 | current.append(button) 86 | 87 | if current: 88 | rows.append(types.KeyboardButtonRow(current)) 89 | 90 | if is_inline and is_normal: 91 | raise ValueError('You cannot mix inline with normal buttons') 92 | elif is_inline: 93 | return types.ReplyInlineMarkup(rows) 94 | return types.ReplyKeyboardMarkup( 95 | rows=rows, 96 | resize=resize, 97 | single_use=single_use, 98 | selective=selective, 99 | persistent=persistent, 100 | placeholder=placeholder 101 | ) 102 | -------------------------------------------------------------------------------- /telethon_generator/data/html/css/docs.dark.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Nunito', sans-serif; 3 | color: #bbb; 4 | background-color:#000; 5 | font-size: 16px; 6 | } 7 | 8 | a { 9 | color: #42aaed; 10 | text-decoration: none; 11 | } 12 | 13 | pre { 14 | font-family: 'Source Code Pro', monospace; 15 | padding: 8px; 16 | color: #567; 17 | background: #080a0c; 18 | border-radius: 0; 19 | overflow-x: auto; 20 | } 21 | 22 | a:hover { 23 | color: #64bbdd; 24 | text-decoration: underline; 25 | } 26 | 27 | table { 28 | width: 100%; 29 | max-width: 100%; 30 | } 31 | 32 | table td { 33 | border-top: 1px solid #111; 34 | padding: 8px; 35 | } 36 | 37 | .horizontal { 38 | margin-bottom: 16px; 39 | list-style: none; 40 | background: #080a0c; 41 | border-radius: 4px; 42 | padding: 8px 16px; 43 | } 44 | 45 | .horizontal li { 46 | display: inline-block; 47 | margin: 0 8px 0 0; 48 | } 49 | 50 | .horizontal img { 51 | display: inline-block; 52 | margin: 0 8px -2px 0; 53 | } 54 | 55 | h1, summary.title { 56 | font-size: 24px; 57 | } 58 | 59 | h3 { 60 | font-size: 20px; 61 | } 62 | 63 | #main_div { 64 | padding: 20px 0; 65 | max-width: 800px; 66 | margin: 0 auto; 67 | } 68 | 69 | pre::-webkit-scrollbar { 70 | visibility: visible; 71 | display: block; 72 | height: 12px; 73 | } 74 | 75 | pre::-webkit-scrollbar-track:horizontal { 76 | background: #222; 77 | border-radius: 0; 78 | height: 12px; 79 | } 80 | 81 | pre::-webkit-scrollbar-thumb:horizontal { 82 | background: #444; 83 | border-radius: 0; 84 | height: 12px; 85 | } 86 | 87 | :target { 88 | border: 2px solid #149; 89 | background: #246; 90 | padding: 4px; 91 | } 92 | 93 | /* 'sh' stands for Syntax Highlight */ 94 | span.sh1 { 95 | color: #f93; 96 | } 97 | 98 | span.tooltip { 99 | border-bottom: 1px dashed #ddd; 100 | } 101 | 102 | #searchBox { 103 | width: 100%; 104 | border: none; 105 | height: 20px; 106 | padding: 8px; 107 | font-size: 16px; 108 | border-radius: 2px; 109 | border: 2px solid #222; 110 | background: #000; 111 | color: #eee; 112 | } 113 | 114 | #searchBox:placeholder-shown { 115 | color: #bbb; 116 | font-style: italic; 117 | } 118 | 119 | button { 120 | border-radius: 2px; 121 | font-size: 16px; 122 | padding: 8px; 123 | color: #bbb; 124 | background-color: #111; 125 | border: 2px solid #146; 126 | transition-duration: 300ms; 127 | } 128 | 129 | button:hover { 130 | background-color: #146; 131 | color: #fff; 132 | } 133 | 134 | /* https://www.w3schools.com/css/css_navbar.asp */ 135 | ul.together { 136 | list-style-type: none; 137 | margin: 0; 138 | padding: 0; 139 | overflow: hidden; 140 | } 141 | 142 | ul.together li { 143 | float: left; 144 | } 145 | 146 | ul.together li a { 147 | display: block; 148 | border-radius: 8px; 149 | background: #111; 150 | padding: 4px 8px; 151 | margin: 8px; 152 | } 153 | 154 | /* https://stackoverflow.com/a/30810322 */ 155 | .invisible { 156 | left: 0; 157 | top: -99px; 158 | padding: 0; 159 | width: 2em; 160 | height: 2em; 161 | border: none; 162 | outline: none; 163 | position: fixed; 164 | box-shadow: none; 165 | color: transparent; 166 | background: transparent; 167 | } 168 | 169 | @media (max-width: 640px) { 170 | h1, summary.title { 171 | font-size: 18px; 172 | } 173 | h3 { 174 | font-size: 16px; 175 | } 176 | 177 | #dev_page_content_wrap { 178 | padding-top: 12px; 179 | } 180 | 181 | #dev_page_title { 182 | margin-top: 10px; 183 | margin-bottom: 20px; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /telethon/errors/rpcbaseerrors.py: -------------------------------------------------------------------------------- 1 | from ..tl import functions 2 | 3 | _NESTS_QUERY = ( 4 | functions.InvokeAfterMsgRequest, 5 | functions.InvokeAfterMsgsRequest, 6 | functions.InitConnectionRequest, 7 | functions.InvokeWithLayerRequest, 8 | functions.InvokeWithoutUpdatesRequest, 9 | functions.InvokeWithMessagesRangeRequest, 10 | functions.InvokeWithTakeoutRequest, 11 | ) 12 | 13 | class RPCError(Exception): 14 | """Base class for all Remote Procedure Call errors.""" 15 | code = None 16 | message = None 17 | 18 | def __init__(self, request, message, code=None): 19 | super().__init__('RPCError {}: {}{}'.format( 20 | code or self.code, message, self._fmt_request(request))) 21 | 22 | self.request = request 23 | self.code = code 24 | self.message = message 25 | 26 | @staticmethod 27 | def _fmt_request(request): 28 | n = 0 29 | reason = '' 30 | while isinstance(request, _NESTS_QUERY): 31 | n += 1 32 | reason += request.__class__.__name__ + '(' 33 | request = request.query 34 | reason += request.__class__.__name__ + ')' * n 35 | 36 | return ' (caused by {})'.format(reason) 37 | 38 | def __reduce__(self): 39 | return type(self), (self.request, self.message, self.code) 40 | 41 | 42 | class InvalidDCError(RPCError): 43 | """ 44 | The request must be repeated, but directed to a different data center. 45 | """ 46 | code = 303 47 | message = 'ERROR_SEE_OTHER' 48 | 49 | 50 | class BadRequestError(RPCError): 51 | """ 52 | The query contains errors. In the event that a request was created 53 | using a form and contains user generated data, the user should be 54 | notified that the data must be corrected before the query is repeated. 55 | """ 56 | code = 400 57 | message = 'BAD_REQUEST' 58 | 59 | 60 | class UnauthorizedError(RPCError): 61 | """ 62 | There was an unauthorized attempt to use functionality available only 63 | to authorized users. 64 | """ 65 | code = 401 66 | message = 'UNAUTHORIZED' 67 | 68 | 69 | class ForbiddenError(RPCError): 70 | """ 71 | Privacy violation. For example, an attempt to write a message to 72 | someone who has blacklisted the current user. 73 | """ 74 | code = 403 75 | message = 'FORBIDDEN' 76 | 77 | 78 | class NotFoundError(RPCError): 79 | """ 80 | An attempt to invoke a non-existent object, such as a method. 81 | """ 82 | code = 404 83 | message = 'NOT_FOUND' 84 | 85 | 86 | class AuthKeyError(RPCError): 87 | """ 88 | Errors related to invalid authorization key, like 89 | AUTH_KEY_DUPLICATED which can cause the connection to fail. 90 | """ 91 | code = 406 92 | message = 'AUTH_KEY' 93 | 94 | 95 | class FloodError(RPCError): 96 | """ 97 | The maximum allowed number of attempts to invoke the given method 98 | with the given input parameters has been exceeded. For example, in an 99 | attempt to request a large number of text messages (SMS) for the same 100 | phone number. 101 | """ 102 | code = 420 103 | message = 'FLOOD' 104 | 105 | 106 | class ServerError(RPCError): 107 | """ 108 | An internal server error occurred while a request was being processed 109 | for example, there was a disruption while accessing a database or file 110 | storage. 111 | """ 112 | code = 500 # Also witnessed as -500 113 | message = 'INTERNAL' 114 | 115 | 116 | class TimedOutError(RPCError): 117 | """ 118 | Clicking the inline buttons of bots that never (or take to long to) 119 | call ``answerCallbackQuery`` will result in this "special" RPCError. 120 | """ 121 | code = 503 # Only witnessed as -503 122 | message = 'Timeout' 123 | 124 | 125 | BotTimeout = TimedOutError 126 | 127 | 128 | base_errors = {x.code: x for x in ( 129 | InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError, 130 | NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError 131 | )} 132 | -------------------------------------------------------------------------------- /readthedocs/developing/testing.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Tests 3 | ===== 4 | 5 | Telethon uses `Pytest `__, for testing, `Tox 6 | `__ for environment setup, and 7 | `pytest-asyncio `__ and `pytest-cov 8 | `__ for asyncio and 9 | `coverage `__ integration. 10 | 11 | While reading the full documentation for these is probably a good idea, there 12 | is a lot to read, so a brief summary of these tools is provided below for 13 | convienience. 14 | 15 | Brief Introduction to Pytest 16 | ============================ 17 | 18 | `Pytest `__ is a tool for discovering and running python 19 | tests, as well as allowing modular reuse of test setup code using fixtures. 20 | 21 | Most Pytest tests will look something like this:: 22 | 23 | from module import my_thing, my_other_thing 24 | 25 | def test_my_thing(fixture): 26 | assert my_thing(fixture) == 42 27 | 28 | @pytest.mark.asyncio 29 | async def test_my_thing(event_loop): 30 | assert await my_other_thing(loop=event_loop) == 42 31 | 32 | Note here: 33 | 34 | 1. The test imports one specific function. The role of unit tests is to test 35 | that the implementation of some unit, like a function or class, works. 36 | It's role is not so much to test that components interact well with each 37 | other. I/O, such as connecting to remote servers, should be avoided. This 38 | helps with quickly identifying the source of an error, finding silent 39 | breakage, and makes it easier to cover all possible code paths. 40 | 41 | System or integration tests can also be useful, but are currently out of 42 | scope of Telethon's automated testing. 43 | 44 | 2. A function ``test_my_thing`` is declared. Pytest searches for files 45 | starting with ``test_``, classes starting with ``Test`` and executes any 46 | functions or methods starting with ``test_`` it finds. 47 | 48 | 3. The function is declared with a parameter ``fixture``. Fixtures are used to 49 | request things required to run the test, such as temporary directories, 50 | free TCP ports, Connections, etc. Fixtures are declared by simply adding 51 | the fixture name as parameter. A full list of available fixtures can be 52 | found with the ``pytest --fixtures`` command. 53 | 54 | 4. The test uses a simple ``assert`` to test some condition is valid. Pytest 55 | uses some magic to ensure that the errors from this are readable and easy 56 | to debug. 57 | 58 | 5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It 59 | starts a loop and executes a test function as coroutine. This should be 60 | used for testing asyncio code. It also declares the ``event_loop`` 61 | fixture, which will request an ``asyncio`` event loop. 62 | 63 | Brief Introduction to Tox 64 | ========================= 65 | 66 | `Tox `__ is a tool for automated setup 67 | of virtual environments for testing. While the tests can be run directly by 68 | just running ``pytest``, this only tests one specific python version in your 69 | existing environment, which will not catch e.g. undeclared dependencies, or 70 | version incompatabilities. 71 | 72 | Tox environments are declared in the ``tox.ini`` file. The default 73 | environments, declared at the top, can be simply run with ``tox``. The option 74 | ``tox -e py36,flake`` can be used to request specific environments to be run. 75 | 76 | Brief Introduction to Pytest-cov 77 | ================================ 78 | 79 | Coverage is a useful metric for testing. It measures the lines of code and 80 | branches that are exercised by the tests. The higher the coverage, the more 81 | likely it is that any coding errors will be caught by the tests. 82 | 83 | A brief coverage report can be generated with the ``--cov`` option to ``tox``, 84 | which will be passed on to ``pytest``. Additionally, the very useful HTML 85 | report can be generated with ``--cov --cov-report=html``, which contains a 86 | browsable copy of the source code, annotated with coverage information for each 87 | line. 88 | -------------------------------------------------------------------------------- /readthedocs/quick-references/client-reference.rst: -------------------------------------------------------------------------------- 1 | .. _client-ref: 2 | 3 | ================ 4 | Client Reference 5 | ================ 6 | 7 | This page contains a summary of all the important methods and properties that 8 | you may need when using Telethon. They are sorted by relevance and are not in 9 | alphabetical order. 10 | 11 | You should use this page to learn about which methods are available, and 12 | if you need a usage example or further description of the arguments, be 13 | sure to follow the links. 14 | 15 | .. contents:: 16 | 17 | TelegramClient 18 | ============== 19 | 20 | This is a summary of the methods and 21 | properties you will find at :ref:`telethon-client`. 22 | 23 | Auth 24 | ---- 25 | 26 | .. currentmodule:: telethon.client.auth.AuthMethods 27 | 28 | .. autosummary:: 29 | :nosignatures: 30 | 31 | start 32 | send_code_request 33 | sign_in 34 | qr_login 35 | log_out 36 | edit_2fa 37 | 38 | Base 39 | ---- 40 | 41 | .. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient 42 | 43 | .. autosummary:: 44 | :nosignatures: 45 | 46 | connect 47 | disconnect 48 | is_connected 49 | disconnected 50 | loop 51 | set_proxy 52 | 53 | Messages 54 | -------- 55 | 56 | .. py:currentmodule:: telethon.client.messages.MessageMethods 57 | 58 | .. autosummary:: 59 | :nosignatures: 60 | 61 | send_message 62 | edit_message 63 | delete_messages 64 | forward_messages 65 | iter_messages 66 | get_messages 67 | pin_message 68 | unpin_message 69 | send_read_acknowledge 70 | 71 | Uploads 72 | ------- 73 | 74 | .. py:currentmodule:: telethon.client.uploads.UploadMethods 75 | 76 | .. autosummary:: 77 | :nosignatures: 78 | 79 | send_file 80 | upload_file 81 | 82 | Downloads 83 | --------- 84 | 85 | .. currentmodule:: telethon.client.downloads.DownloadMethods 86 | 87 | .. autosummary:: 88 | :nosignatures: 89 | 90 | download_media 91 | download_profile_photo 92 | download_file 93 | iter_download 94 | 95 | Dialogs 96 | ------- 97 | 98 | .. py:currentmodule:: telethon.client.dialogs.DialogMethods 99 | 100 | .. autosummary:: 101 | :nosignatures: 102 | 103 | iter_dialogs 104 | get_dialogs 105 | edit_folder 106 | iter_drafts 107 | get_drafts 108 | delete_dialog 109 | conversation 110 | 111 | Users 112 | ----- 113 | 114 | .. py:currentmodule:: telethon.client.users.UserMethods 115 | 116 | .. autosummary:: 117 | :nosignatures: 118 | 119 | get_me 120 | is_bot 121 | is_user_authorized 122 | get_entity 123 | get_input_entity 124 | get_peer_id 125 | 126 | Chats 127 | ----- 128 | 129 | .. currentmodule:: telethon.client.chats.ChatMethods 130 | 131 | .. autosummary:: 132 | :nosignatures: 133 | 134 | iter_participants 135 | get_participants 136 | kick_participant 137 | iter_admin_log 138 | get_admin_log 139 | iter_profile_photos 140 | get_profile_photos 141 | edit_admin 142 | edit_permissions 143 | get_permissions 144 | get_stats 145 | action 146 | 147 | Parse Mode 148 | ---------- 149 | 150 | .. py:currentmodule:: telethon.client.messageparse.MessageParseMethods 151 | 152 | .. autosummary:: 153 | :nosignatures: 154 | 155 | parse_mode 156 | 157 | Updates 158 | ------- 159 | 160 | .. py:currentmodule:: telethon.client.updates.UpdateMethods 161 | 162 | .. autosummary:: 163 | :nosignatures: 164 | 165 | on 166 | run_until_disconnected 167 | add_event_handler 168 | remove_event_handler 169 | list_event_handlers 170 | catch_up 171 | set_receive_updates 172 | 173 | Bots 174 | ---- 175 | 176 | .. currentmodule:: telethon.client.bots.BotMethods 177 | 178 | .. autosummary:: 179 | :nosignatures: 180 | 181 | inline_query 182 | 183 | Buttons 184 | ------- 185 | 186 | .. currentmodule:: telethon.client.buttons.ButtonMethods 187 | 188 | .. autosummary:: 189 | :nosignatures: 190 | 191 | build_reply_markup 192 | 193 | Account 194 | ------- 195 | 196 | .. currentmodule:: telethon.client.account.AccountMethods 197 | 198 | .. autosummary:: 199 | :nosignatures: 200 | 201 | takeout 202 | end_takeout 203 | -------------------------------------------------------------------------------- /telethon/tl/custom/sendergetter.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from ... import utils 4 | 5 | 6 | class SenderGetter(abc.ABC): 7 | """ 8 | Helper base class that introduces the `sender`, `input_sender` 9 | and `sender_id` properties and `get_sender` and `get_input_sender` 10 | methods. 11 | """ 12 | def __init__(self, sender_id=None, *, sender=None, input_sender=None): 13 | self._sender_id = sender_id 14 | self._sender = sender 15 | self._input_sender = input_sender 16 | self._client = None 17 | 18 | @property 19 | def sender(self): 20 | """ 21 | Returns the :tl:`User` or :tl:`Channel` that sent this object. 22 | It may be `None` if Telegram didn't send the sender. 23 | 24 | If you only need the ID, use `sender_id` instead. 25 | 26 | If you need to call a method which needs 27 | this chat, use `input_sender` instead. 28 | 29 | If you're using `telethon.events`, use `get_sender()` instead. 30 | """ 31 | return self._sender 32 | 33 | async def get_sender(self): 34 | """ 35 | Returns `sender`, but will make an API call to find the 36 | sender unless it's already cached. 37 | 38 | If you only need the ID, use `sender_id` instead. 39 | 40 | If you need to call a method which needs 41 | this sender, use `get_input_sender()` instead. 42 | """ 43 | # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. 44 | # It's a flag that will be set if only minimal information is 45 | # available (such as display name, but username may be missing), 46 | # in which case we want to force fetch the entire thing because 47 | # the user explicitly called a method. If the user is okay with 48 | # cached information, they may use the property instead. 49 | if (self._sender is None or getattr(self._sender, 'min', None)) \ 50 | and await self.get_input_sender(): 51 | # self.get_input_sender may refresh in which case the sender may no longer be min 52 | # However it could still incur a cost so the cheap check is done twice instead. 53 | if self._sender is None or getattr(self._sender, 'min', None): 54 | try: 55 | self._sender =\ 56 | await self._client.get_entity(self._input_sender) 57 | except ValueError: 58 | await self._refetch_sender() 59 | return self._sender 60 | 61 | @property 62 | def input_sender(self): 63 | """ 64 | This :tl:`InputPeer` is the input version of the user/channel who 65 | sent the message. Similarly to `input_chat 66 | `, this doesn't 67 | have things like username or similar, but still useful in some cases. 68 | 69 | Note that this might not be available if the library can't 70 | find the input chat, or if the message a broadcast on a channel. 71 | """ 72 | if self._input_sender is None and self._sender_id and self._client: 73 | try: 74 | self._input_sender = self._client._mb_entity_cache.get( 75 | utils.resolve_id(self._sender_id)[0])._as_input_peer() 76 | except AttributeError: 77 | pass 78 | return self._input_sender 79 | 80 | async def get_input_sender(self): 81 | """ 82 | Returns `input_sender`, but will make an API call to find the 83 | input sender unless it's already cached. 84 | """ 85 | if self.input_sender is None and self._sender_id and self._client: 86 | await self._refetch_sender() 87 | return self._input_sender 88 | 89 | @property 90 | def sender_id(self): 91 | """ 92 | Returns the marked sender integer ID, if present. 93 | 94 | If there is a sender in the object, `sender_id` will *always* be set, 95 | which is why you should use it instead of `sender.id `. 96 | """ 97 | return self._sender_id 98 | 99 | async def _refetch_sender(self): 100 | """ 101 | Re-fetches sender information through other means. 102 | """ 103 | -------------------------------------------------------------------------------- /telethon/crypto/cdndecrypter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module holds the CdnDecrypter utility class. 3 | """ 4 | from hashlib import sha256 5 | 6 | from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest 7 | from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile 8 | from ..crypto import AESModeCTR 9 | from ..errors import CdnFileTamperedError 10 | 11 | 12 | class CdnDecrypter: 13 | """ 14 | Used when downloading a file results in a 'FileCdnRedirect' to 15 | both prepare the redirect, decrypt the file as it downloads, and 16 | ensure the file hasn't been tampered. https://core.telegram.org/cdn 17 | """ 18 | def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes): 19 | """ 20 | Initializes the CDN decrypter. 21 | 22 | :param cdn_client: a client connected to a CDN. 23 | :param file_token: the token of the file to be used. 24 | :param cdn_aes: the AES CTR used to decrypt the file. 25 | :param cdn_file_hashes: the hashes the decrypted file must match. 26 | """ 27 | self.client = cdn_client 28 | self.file_token = file_token 29 | self.cdn_aes = cdn_aes 30 | self.cdn_file_hashes = cdn_file_hashes 31 | 32 | @staticmethod 33 | async def prepare_decrypter(client, cdn_client, cdn_redirect): 34 | """ 35 | Prepares a new CDN decrypter. 36 | 37 | :param client: a TelegramClient connected to the main servers. 38 | :param cdn_client: a new client connected to the CDN. 39 | :param cdn_redirect: the redirect file object that caused this call. 40 | :return: (CdnDecrypter, first chunk file data) 41 | """ 42 | cdn_aes = AESModeCTR( 43 | key=cdn_redirect.encryption_key, 44 | # 12 first bytes of the IV..4 bytes of the offset (0, big endian) 45 | iv=cdn_redirect.encryption_iv[:12] + bytes(4) 46 | ) 47 | 48 | # We assume that cdn_redirect.cdn_file_hashes are ordered by offset, 49 | # and that there will be enough of these to retrieve the whole file. 50 | decrypter = CdnDecrypter( 51 | cdn_client, cdn_redirect.file_token, 52 | cdn_aes, cdn_redirect.cdn_file_hashes 53 | ) 54 | 55 | cdn_file = await cdn_client(GetCdnFileRequest( 56 | file_token=cdn_redirect.file_token, 57 | offset=cdn_redirect.cdn_file_hashes[0].offset, 58 | limit=cdn_redirect.cdn_file_hashes[0].limit 59 | )) 60 | if isinstance(cdn_file, CdnFileReuploadNeeded): 61 | # We need to use the original client here 62 | await client(ReuploadCdnFileRequest( 63 | file_token=cdn_redirect.file_token, 64 | request_token=cdn_file.request_token 65 | )) 66 | 67 | # We want to always return a valid upload.CdnFile 68 | cdn_file = decrypter.get_file() 69 | else: 70 | cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes) 71 | cdn_hash = decrypter.cdn_file_hashes.pop(0) 72 | decrypter.check(cdn_file.bytes, cdn_hash) 73 | 74 | return decrypter, cdn_file 75 | 76 | def get_file(self): 77 | """ 78 | Calls GetCdnFileRequest and decrypts its bytes. 79 | Also ensures that the file hasn't been tampered. 80 | 81 | :return: the CdnFile result. 82 | """ 83 | if self.cdn_file_hashes: 84 | cdn_hash = self.cdn_file_hashes.pop(0) 85 | cdn_file = self.client(GetCdnFileRequest( 86 | self.file_token, cdn_hash.offset, cdn_hash.limit 87 | )) 88 | cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes) 89 | self.check(cdn_file.bytes, cdn_hash) 90 | else: 91 | cdn_file = CdnFile(bytes(0)) 92 | 93 | return cdn_file 94 | 95 | @staticmethod 96 | def check(data, cdn_hash): 97 | """ 98 | Checks the integrity of the given data. 99 | Raises CdnFileTamperedError if the integrity check fails. 100 | 101 | :param data: the data to be hashed. 102 | :param cdn_hash: the expected hash. 103 | """ 104 | if sha256(data).digest() != cdn_hash.hash: 105 | raise CdnFileTamperedError() 106 | -------------------------------------------------------------------------------- /readthedocs/basic/quick-start.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Quick-Start 3 | =========== 4 | 5 | Let's see a longer example to learn some of the methods that the library 6 | has to offer. These are known as "friendly methods", and you should always 7 | use these if possible. 8 | 9 | .. code-block:: python 10 | 11 | from telethon import TelegramClient 12 | 13 | # Remember to use your own values from my.telegram.org! 14 | api_id = 12345 15 | api_hash = '0123456789abcdef0123456789abcdef' 16 | client = TelegramClient('anon', api_id, api_hash) 17 | 18 | async def main(): 19 | # Getting information about yourself 20 | me = await client.get_me() 21 | 22 | # "me" is a user object. You can pretty-print 23 | # any Telegram object with the "stringify" method: 24 | print(me.stringify()) 25 | 26 | # When you print something, you see a representation of it. 27 | # You can access all attributes of Telegram objects with 28 | # the dot operator. For example, to get the username: 29 | username = me.username 30 | print(username) 31 | print(me.phone) 32 | 33 | # You can print all the dialogs/conversations that you are part of: 34 | async for dialog in client.iter_dialogs(): 35 | print(dialog.name, 'has ID', dialog.id) 36 | 37 | # You can send messages to yourself... 38 | await client.send_message('me', 'Hello, myself!') 39 | # ...to some chat ID 40 | await client.send_message(-100123456, 'Hello, group!') 41 | # ...to your contacts 42 | await client.send_message('+34600123123', 'Hello, friend!') 43 | # ...or even to any username 44 | await client.send_message('username', 'Testing Telethon!') 45 | 46 | # You can, of course, use markdown in your messages: 47 | message = await client.send_message( 48 | 'me', 49 | 'This message has **bold**, `code`, __italics__ and ' 50 | 'a [nice website](https://example.com)!', 51 | link_preview=False 52 | ) 53 | 54 | # Sending a message returns the sent message object, which you can use 55 | print(message.raw_text) 56 | 57 | # You can reply to messages directly if you have a message object 58 | await message.reply('Cool!') 59 | 60 | # Or send files, songs, documents, albums... 61 | await client.send_file('me', '/home/me/Pictures/holidays.jpg') 62 | 63 | # You can print the message history of any chat: 64 | async for message in client.iter_messages('me'): 65 | print(message.id, message.text) 66 | 67 | # You can download media from messages, too! 68 | # The method will return the path where the file was saved. 69 | if message.photo: 70 | path = await message.download_media() 71 | print('File saved to', path) # printed after download is done 72 | 73 | with client: 74 | client.loop.run_until_complete(main()) 75 | 76 | 77 | Here, we show how to sign in, get information about yourself, send 78 | messages, files, getting chats, printing messages, and downloading 79 | files. 80 | 81 | You should make sure that you understand what the code shown here 82 | does, take note on how methods are called and used and so on before 83 | proceeding. We will see all the available methods later on. 84 | 85 | .. important:: 86 | 87 | Note that Telethon is an asynchronous library, and as such, you should 88 | get used to it and learn a bit of basic `asyncio`. This will help a lot. 89 | As a quick start, this means you generally want to write all your code 90 | inside some ``async def`` like so: 91 | 92 | .. code-block:: python 93 | 94 | client = ... 95 | 96 | async def do_something(me): 97 | ... 98 | 99 | async def main(): 100 | # Most of your code should go here. 101 | # You can of course make and use your own async def (do_something). 102 | # They only need to be async if they need to await things. 103 | me = await client.get_me() 104 | await do_something(me) 105 | 106 | with client: 107 | client.loop.run_until_complete(main()) 108 | 109 | After you understand this, you may use the ``telethon.sync`` hack if you 110 | want do so (see :ref:`compatibility-and-convenience`), but note you may 111 | run into other issues (iPython, Anaconda, etc. have some issues with it). 112 | -------------------------------------------------------------------------------- /telethon/extensions/messagepacker.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import collections 3 | import io 4 | import struct 5 | 6 | from ..tl import TLRequest 7 | from ..tl.core.messagecontainer import MessageContainer 8 | from ..tl.core.tlmessage import TLMessage 9 | 10 | 11 | class MessagePacker: 12 | """ 13 | This class packs `RequestState` as outgoing `TLMessages`. 14 | 15 | The purpose of this class is to support putting N `RequestState` into a 16 | queue, and then awaiting for "packed" `TLMessage` in the other end. The 17 | simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but 18 | for efficiency purposes it's ``States -> Container`` (N-to-1). 19 | 20 | This addresses several needs: outgoing messages will be smaller, so the 21 | encryption and network overhead also is smaller. It's also a central 22 | point where outgoing requests are put, and where ready-messages are get. 23 | """ 24 | 25 | def __init__(self, state, loggers): 26 | self._state = state 27 | self._deque = collections.deque() 28 | self._ready = asyncio.Event() 29 | self._log = loggers[__name__] 30 | 31 | def append(self, state): 32 | self._deque.append(state) 33 | self._ready.set() 34 | 35 | def extend(self, states): 36 | self._deque.extend(states) 37 | self._ready.set() 38 | 39 | async def get(self): 40 | """ 41 | Returns (batch, data) if one or more items could be retrieved. 42 | 43 | If the cancellation occurs or only invalid items were in the 44 | queue, (None, None) will be returned instead. 45 | """ 46 | if not self._deque: 47 | self._ready.clear() 48 | await self._ready.wait() 49 | 50 | buffer = io.BytesIO() 51 | batch = [] 52 | size = 0 53 | 54 | # Fill a new batch to return while the size is small enough, 55 | # as long as we don't exceed the maximum length of messages. 56 | while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH: 57 | state = self._deque.popleft() 58 | size += len(state.data) + TLMessage.SIZE_OVERHEAD 59 | 60 | if size <= MessageContainer.MAXIMUM_SIZE: 61 | state.msg_id = self._state.write_data_as_message( 62 | buffer, state.data, isinstance(state.request, TLRequest), 63 | after_id=state.after.msg_id if state.after else None 64 | ) 65 | batch.append(state) 66 | self._log.debug('Assigned msg_id = %d to %s (%x)', 67 | state.msg_id, state.request.__class__.__name__, 68 | id(state.request)) 69 | continue 70 | 71 | if batch: 72 | # Put the item back since it can't be sent in this batch 73 | self._deque.appendleft(state) 74 | break 75 | 76 | # If a single message exceeds the maximum size, then the 77 | # message payload cannot be sent. Telegram would forcibly 78 | # close the connection; message would never be confirmed. 79 | # 80 | # We don't put the item back because it can never be sent. 81 | # If we did, we would loop again and reach this same path. 82 | # Setting the exception twice results in `InvalidStateError` 83 | # and this method should never return with error, which we 84 | # really want to avoid. 85 | self._log.warning( 86 | 'Message payload for %s is too long (%d) and cannot be sent', 87 | state.request.__class__.__name__, len(state.data) 88 | ) 89 | state.future.set_exception( 90 | ValueError('Request payload is too big')) 91 | 92 | size = 0 93 | continue 94 | 95 | if not batch: 96 | return None, None 97 | 98 | if len(batch) > 1: 99 | # Inlined code to pack several messages into a container 100 | data = struct.pack( 101 | '` of the channel you want to join 25 | to, you can make use of the :tl:`JoinChannelRequest` to join such channel: 26 | 27 | .. code-block:: python 28 | 29 | from telethon.tl.functions.channels import JoinChannelRequest 30 | await client(JoinChannelRequest(channel)) 31 | 32 | # In the same way, you can also leave such channel 33 | from telethon.tl.functions.channels import LeaveChannelRequest 34 | await client(LeaveChannelRequest(input_channel)) 35 | 36 | 37 | For more on channels, check the `channels namespace`__. 38 | 39 | 40 | __ https://tl.telethon.dev/methods/channels/index.html 41 | 42 | 43 | Joining a private chat or channel 44 | ================================= 45 | 46 | If all you have is a link like this one: 47 | ``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have 48 | enough information to join! The part after the 49 | ``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this 50 | example, is the ``hash`` of the chat or channel. Now you can use 51 | :tl:`ImportChatInviteRequest` as follows: 52 | 53 | .. code-block:: python 54 | 55 | from telethon.tl.functions.messages import ImportChatInviteRequest 56 | updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg')) 57 | 58 | 59 | Adding someone else to such chat or channel 60 | =========================================== 61 | 62 | If you don't want to add yourself, maybe because you're already in, 63 | you can always add someone else with the :tl:`AddChatUserRequest`, which 64 | use is very straightforward, or :tl:`InviteToChannelRequest` for channels: 65 | 66 | .. code-block:: python 67 | 68 | # For normal chats 69 | from telethon.tl.functions.messages import AddChatUserRequest 70 | 71 | # Note that ``user_to_add`` is NOT the name of the parameter. 72 | # It's the user you want to add (``user_id=user_to_add``). 73 | await client(AddChatUserRequest( 74 | chat_id, 75 | user_to_add, 76 | fwd_limit=10 # Allow the user to see the 10 last messages 77 | )) 78 | 79 | # For channels (which includes megagroups) 80 | from telethon.tl.functions.channels import InviteToChannelRequest 81 | 82 | await client(InviteToChannelRequest( 83 | channel, 84 | [users_to_add] 85 | )) 86 | 87 | Note that this method will only really work for friends or bot accounts. 88 | Trying to mass-add users with this approach will not work, and can put both 89 | your account and group to risk, possibly being flagged as spam and limited. 90 | 91 | 92 | Checking a link without joining 93 | =============================== 94 | 95 | If you don't need to join but rather check whether it's a group or a 96 | channel, you can use the :tl:`CheckChatInviteRequest`, which takes in 97 | the hash of said channel or group. 98 | 99 | 100 | Increasing View Count in a Channel 101 | ================================== 102 | 103 | It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and 104 | while I don't understand why so many people ask this, the solution is to 105 | use :tl:`GetMessagesViewsRequest`, setting ``increment=True``: 106 | 107 | .. code-block:: python 108 | 109 | 110 | # Obtain `channel' through dialogs or through client.get_entity() or anyhow. 111 | # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list. 112 | 113 | await client(GetMessagesViewsRequest( 114 | peer=channel, 115 | id=msg_ids, 116 | increment=True 117 | )) 118 | 119 | 120 | Note that you can only do this **once or twice a day** per account, 121 | running this in a loop will obviously not increase the views forever 122 | unless you wait a day between each iteration. If you run it any sooner 123 | than that, the views simply won't be increased. 124 | 125 | __ https://github.com/LonamiWebs/Telethon/issues/233 126 | __ https://github.com/LonamiWebs/Telethon/issues/305 127 | __ https://github.com/LonamiWebs/Telethon/issues/409 128 | __ https://github.com/LonamiWebs/Telethon/issues/447 129 | -------------------------------------------------------------------------------- /telethon/tl/custom/qrlogin.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import datetime 4 | 5 | from .. import types, functions 6 | from ... import events 7 | 8 | 9 | class QRLogin: 10 | """ 11 | QR login information. 12 | 13 | Most of the time, you will present the `url` as a QR code to the user, 14 | and while it's being shown, call `wait`. 15 | """ 16 | def __init__(self, client, ignored_ids): 17 | self._client = client 18 | self._request = functions.auth.ExportLoginTokenRequest( 19 | self._client.api_id, self._client.api_hash, ignored_ids) 20 | self._resp = None 21 | 22 | async def recreate(self): 23 | """ 24 | Generates a new token and URL for a new QR code, useful if the code 25 | has expired before it was imported. 26 | """ 27 | self._resp = await self._client(self._request) 28 | 29 | @property 30 | def token(self) -> bytes: 31 | """ 32 | The binary data representing the token. 33 | 34 | It can be used by a previously-authorized client in a call to 35 | :tl:`auth.importLoginToken` to log the client that originally 36 | requested the QR login. 37 | """ 38 | return self._resp.token 39 | 40 | @property 41 | def url(self) -> str: 42 | """ 43 | The ``tg://login`` URI with the token. When opened by a Telegram 44 | application where the user is logged in, it will import the login 45 | token. 46 | 47 | If you want to display a QR code to the user, this is the URL that 48 | should be launched when the QR code is scanned (the URL that should 49 | be contained in the QR code image you generate). 50 | 51 | Whether you generate the QR code image or not is up to you, and the 52 | library can't do this for you due to the vast ways of generating and 53 | displaying the QR code that exist. 54 | 55 | The URL simply consists of `token` base64-encoded. 56 | """ 57 | return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('=')) 58 | 59 | @property 60 | def expires(self) -> datetime.datetime: 61 | """ 62 | The `datetime` at which the QR code will expire. 63 | 64 | If you want to try again, you will need to call `recreate`. 65 | """ 66 | return self._resp.expires 67 | 68 | async def wait(self, timeout: float = None): 69 | """ 70 | Waits for the token to be imported by a previously-authorized client, 71 | either by scanning the QR, launching the URL directly, or calling the 72 | import method. 73 | 74 | This method **must** be called before the QR code is scanned, and 75 | must be executing while the QR code is being scanned. Otherwise, the 76 | login will not complete. 77 | 78 | Will raise `asyncio.TimeoutError` if the login doesn't complete on 79 | time. 80 | 81 | Arguments 82 | timeout (float): 83 | The timeout, in seconds, to wait before giving up. By default 84 | the library will wait until the token expires, which is often 85 | what you want. 86 | 87 | Returns 88 | On success, an instance of :tl:`User`. On failure it will raise. 89 | """ 90 | if timeout is None: 91 | timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds() 92 | 93 | event = asyncio.Event() 94 | 95 | async def handler(_update): 96 | event.set() 97 | 98 | self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken)) 99 | 100 | try: 101 | # Will raise timeout error if it doesn't complete quick enough, 102 | # which we want to let propagate 103 | await asyncio.wait_for(event.wait(), timeout=timeout) 104 | finally: 105 | self._client.remove_event_handler(handler) 106 | 107 | # We got here without it raising timeout error, so we can proceed 108 | resp = await self._client(self._request) 109 | if isinstance(resp, types.auth.LoginTokenMigrateTo): 110 | await self._client._switch_dc(resp.dc_id) 111 | resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token)) 112 | # resp should now be auth.loginTokenSuccess 113 | 114 | if isinstance(resp, types.auth.LoginTokenSuccess): 115 | user = resp.authorization.user 116 | await self._client._on_login(user) 117 | return user 118 | 119 | raise TypeError('Login token response was unexpected: {}'.format(resp)) 120 | --------------------------------------------------------------------------------