├── 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_helpers.py │ └── test_utils.py └── readthedocs │ ├── __init__.py │ ├── quick_references │ ├── __init__.py │ └── test_client_reference.py │ └── conftest.py ├── readthedocs ├── requirements.txt ├── modules │ ├── helpers.rst │ ├── utils.rst │ ├── errors.rst │ ├── sessions.rst │ ├── network.rst │ ├── events.rst │ ├── client.rst │ └── custom.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 ├── examples │ ├── word-of-warning.rst │ ├── working-with-messages.rst │ ├── users.rst │ └── chats-and-channels.rst ├── make.bat ├── basic │ ├── next-steps.rst │ ├── installation.rst │ └── quick-start.rst ├── custom_roles.py ├── misc │ └── wall-of-shame.rst ├── concepts │ └── strings.rst ├── index.rst └── quick-references │ └── client-reference.rst ├── 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 ├── dev-requirements.txt ├── telethon ├── tl │ ├── __init__.py │ ├── custom │ │ ├── inputsizedfile.py │ │ ├── __init__.py │ │ ├── forward.py │ │ ├── inlineresults.py │ │ ├── sendergetter.py │ │ ├── qrlogin.py │ │ ├── participantpermissions.py │ │ └── file.py │ ├── patched │ │ └── __init__.py │ └── core │ │ ├── __init__.py │ │ ├── tlmessage.py │ │ ├── rpcresult.py │ │ ├── gzippacked.py │ │ └── messagecontainer.py ├── version.py ├── sessions │ ├── __init__.py │ └── string.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 │ │ ├── tcpfull.py │ │ ├── tcpintermediate.py │ │ └── tcpobfuscated.py │ ├── __init__.py │ ├── requeststate.py │ └── mtprotoplainsender.py ├── client │ ├── telegramclient.py │ ├── __init__.py │ ├── bots.py │ └── buttons.py ├── errors │ ├── __init__.py │ └── rpcbaseerrors.py ├── hints.py ├── events │ ├── raw.py │ ├── messageedited.py │ ├── messagedeleted.py │ └── __init__.py └── sync.py ├── optional-requirements.txt ├── .coveragerc ├── telethon_examples ├── screenshot-gui.jpg ├── print_updates.py ├── print_messages.py ├── assistant.py └── replier.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature-request.md │ ├── config.yml │ └── bug-report.md └── workflows │ └── python.yml ├── logo.svg ├── update-docs.sh ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE └── pyproject.toml /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 | -------------------------------------------------------------------------------- /readthedocs/requirements.txt: -------------------------------------------------------------------------------- 1 | telethon -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyaes 2 | rsa 3 | -------------------------------------------------------------------------------- /telethon_generator/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/telethon/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/readthedocs/quick_references/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | tgcrypto 2 | cryptg 3 | pysocks 4 | python-socks[asyncio] 5 | hachoir 6 | pillow 7 | -------------------------------------------------------------------------------- /.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.28.5' 4 | -------------------------------------------------------------------------------- /telethon_examples/screenshot-gui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disk6969/Telethon/HEAD/telethon_examples/screenshot-gui.jpg -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest ideas, changes or other enhancements for the library 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please describe your idea. Would you like another friendly method? Renaming them to something more appropriated? Changing the way something works? 11 | -------------------------------------------------------------------------------- /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 master 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Ask questions in StackOverflow 3 | url: https://stackoverflow.com/questions/ask?tags=telethon 4 | about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed 5 | - name: Find about updates and our Telegram groups 6 | url: https://t.me/s/TelethonUpdates 7 | about: Be notified of updates, chat with other people about the library or ask questions in these groups 8 | -------------------------------------------------------------------------------- /telethon/__init__.py: -------------------------------------------------------------------------------- 1 | from .client.telegramclient import TelegramClient 2 | from .network import connection 3 | from .tl import types, functions, custom 4 | from .tl.custom import Button 5 | from .tl import patched as _ # import for its side-effects 6 | from . import version, events, utils, errors 7 | 8 | __version__ = version.__version__ 9 | 10 | __all__ = [ 11 | 'TelegramClient', 'Button', 12 | 'types', 'functions', 'custom', 'errors', 13 | 'events', 'utils', 'connection' 14 | ] 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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('_'): 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/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report about a bug inside the library or issues with the documentation 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Checklist** 11 | * [ ] The error is in the library's code, and not in my own. 12 | * [ ] I have searched for this issue before posting it and there isn't a duplicate. 13 | * [ ] I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip` and triggered the bug in the latest version. 14 | 15 | **Code that causes the issue** 16 | ```python 17 | from telethon.sync import TelegramClient 18 | ... 19 | 20 | ``` 21 | 22 | **Traceback** 23 | ``` 24 | Traceback (most recent call last): 25 | File "code.py", line 1, in 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/telethon/client/test_messages.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import pytest 4 | 5 | from telethon import TelegramClient 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_send_message_with_file_forwards_args(): 10 | arguments = {} 11 | sentinel = object() 12 | 13 | for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters): 14 | if name in {'self', 'entity', 'file'}: 15 | continue # positional 16 | 17 | if name in {'message'}: 18 | continue # renamed 19 | 20 | if name in {'link_preview'}: 21 | continue # make no sense in send_file 22 | 23 | arguments[name] = value 24 | 25 | class MockedClient(TelegramClient): 26 | # noinspection PyMissingConstructor 27 | def __init__(self): 28 | pass 29 | 30 | async def send_file(self, entity, file, **kwargs): 31 | assert entity == 'a' 32 | assert file == 'b' 33 | for k, v in arguments.items(): 34 | assert k in kwargs 35 | assert kwargs[k] == v 36 | 37 | return sentinel 38 | 39 | client = MockedClient() 40 | assert (await client.send_message('a', file='b', **arguments)) == sentinel 41 | -------------------------------------------------------------------------------- /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/tcpfull.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from zlib import crc32 3 | 4 | from .connection import Connection, PacketCodec 5 | from ...errors import InvalidChecksumError 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(' 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 | -------------------------------------------------------------------------------- /readthedocs/examples/working-with-messages.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Working with messages 3 | ===================== 4 | 5 | 6 | .. note:: 7 | 8 | These examples assume you have read :ref:`full-api`. 9 | 10 | .. contents:: 11 | 12 | 13 | Sending stickers 14 | ================ 15 | 16 | Stickers are nothing else than ``files``, and when you successfully retrieve 17 | the stickers for a certain sticker set, all you will have are ``handles`` to 18 | these files. Remember, the files Telegram holds on their servers can be 19 | referenced through this pair of ID/hash (unique per user), and you need to 20 | use this handle when sending a "document" message. This working example will 21 | send yourself the very first sticker you have: 22 | 23 | .. code-block:: python 24 | 25 | # Get all the sticker sets this user has 26 | from telethon.tl.functions.messages import GetAllStickersRequest 27 | sticker_sets = await client(GetAllStickersRequest(0)) 28 | 29 | # Choose a sticker set 30 | from telethon.tl.functions.messages import GetStickerSetRequest 31 | from telethon.tl.types import InputStickerSetID 32 | sticker_set = sticker_sets.sets[0] 33 | 34 | # Get the stickers for this sticker set 35 | stickers = await client(GetStickerSetRequest( 36 | stickerset=InputStickerSetID( 37 | id=sticker_set.id, access_hash=sticker_set.access_hash 38 | ) 39 | )) 40 | 41 | # Stickers are nothing more than files, so send that 42 | await client.send_file('me', stickers.documents[0]) 43 | 44 | 45 | .. _issues: https://github.com/LonamiWebs/Telethon/issues/215 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 11 | 12 | def test_strip_text(): 13 | assert helpers.strip_text(" text ", []) == "text" 14 | # I can't interpret the rest of the code well enough yet 15 | 16 | 17 | class TestSyncifyAsyncContext: 18 | class NoopContextManager: 19 | def __init__(self, loop): 20 | self.count = 0 21 | self.loop = loop 22 | 23 | async def __aenter__(self): 24 | self.count += 1 25 | return self 26 | 27 | async def __aexit__(self, exc_type, *args): 28 | assert exc_type is None 29 | self.count -= 1 30 | 31 | __enter__ = helpers._sync_enter 32 | __exit__ = helpers._sync_exit 33 | 34 | def test_sync_acontext(self, event_loop): 35 | contm = self.NoopContextManager(event_loop) 36 | assert contm.count == 0 37 | 38 | with contm: 39 | assert contm.count == 1 40 | 41 | assert contm.count == 0 42 | 43 | @pytest.mark.asyncio 44 | async def test_async_acontext(self, event_loop): 45 | contm = self.NoopContextManager(event_loop) 46 | assert contm.count == 0 47 | 48 | async with contm: 49 | assert contm.count == 1 50 | 51 | assert contm.count == 0 52 | 53 | 54 | def test_generate_key_data_from_nonce(): 55 | gkdfn = helpers.generate_key_data_from_nonce 56 | 57 | key_expect = b64decode(b'NFwRFB8Knw/kAmvPWjtrQauWysHClVfQh0UOAaABqZA=') 58 | nonce_expect = b64decode(b'1AgjhU9eDvJRjFik73bjR2zZEATzL/jLu9yodYfWEgA=') 59 | assert gkdfn(123456789, 1234567) == (key_expect, nonce_expect) 60 | -------------------------------------------------------------------------------- /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, 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/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 | ] 49 | 50 | # Can't use `typing.Type` in Python 3.5.2 51 | # See https://github.com/python/typing/issues/266 52 | try: 53 | OutFileLike = typing.Union[ 54 | str, 55 | typing.Type[bytes], 56 | typing.BinaryIO 57 | ] 58 | except TypeError: 59 | OutFileLike = typing.Union[ 60 | str, 61 | typing.BinaryIO 62 | ] 63 | 64 | MessageLike = typing.Union[str, types.Message] 65 | MessageIDLike = typing.Union[int, types.Message, types.TypeInputMessage] 66 | 67 | ProgressCallback = typing.Callable[[int, int], None] 68 | -------------------------------------------------------------------------------- /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.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,sign_up,auth.signUp 5 | auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode 6 | auth.AuthMethods,log_out,auth.logOut 7 | auth.AuthMethods,edit_2fa,account.updatePasswordSettings 8 | bots.BotMethods,inline_query,messages.getInlineBotResults 9 | chats.ChatMethods,action,messages.setTyping 10 | chats.ChatMethods,edit_admin,channels.editAdmin messages.editChatAdmin 11 | chats.ChatMethods,edit_permissions,channels.editBanned messages.editChatDefaultBannedRights 12 | chats.ChatMethods,iter_participants,channels.getParticipants 13 | chats.ChatMethods,iter_admin_log,channels.getAdminLog 14 | dialogs.DialogMethods,iter_dialogs,messages.getDialogs 15 | dialogs.DialogMethods,iter_drafts,messages.getAllDrafts 16 | dialogs.DialogMethods,edit_folder,folders.deleteFolder folders.editPeerFolders 17 | downloads.DownloadMethods,download_media,upload.getFile 18 | messages.MessageMethods,iter_messages,messages.searchGlobal messages.search messages.getHistory channels.getMessages messages.getMessages 19 | messages.MessageMethods,send_message,messages.sendMessage 20 | messages.MessageMethods,forward_messages,messages.forwardMessages 21 | messages.MessageMethods,edit_message,messages.editInlineBotMessage messages.editMessage 22 | messages.MessageMethods,delete_messages,channels.deleteMessages messages.deleteMessages 23 | messages.MessageMethods,send_read_acknowledge,messages.readMentions channels.readHistory messages.readHistory 24 | updates.UpdateMethods,catch_up,updates.getDifference updates.getChannelDifference 25 | uploads.UploadMethods,send_file,messages.sendMedia messages.sendMultiMedia messages.uploadMedia 26 | uploads.UploadMethods,upload_file,upload.saveFilePart upload.saveBigFilePart 27 | users.UserMethods,get_entity,users.getUsers messages.getChats channels.getChannels contacts.resolveUsername 28 | -------------------------------------------------------------------------------- /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_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 | 13 | class MethodInfo: 14 | def __init__(self, name, usability, errors, friendly): 15 | self.name = name 16 | self.errors = errors 17 | self.friendly = friendly 18 | try: 19 | self.usability = { 20 | 'unknown': Usability.UNKNOWN, 21 | 'user': Usability.USER, 22 | 'bot': Usability.BOT, 23 | 'both': Usability.BOTH, 24 | }[usability.lower()] 25 | except KeyError: 26 | raise ValueError('Usability must be either user, bot, both or ' 27 | 'unknown, not {}'.format(usability)) from None 28 | 29 | 30 | def parse_methods(csv_file, friendly_csv_file, errors_dict): 31 | """ 32 | Parses the input CSV file with columns (method, usability, errors) 33 | and yields `MethodInfo` instances as a result. 34 | """ 35 | raw_to_friendly = {} 36 | with friendly_csv_file.open(newline='') as f: 37 | f = csv.reader(f) 38 | next(f, None) # header 39 | for ns, friendly, raw_list in f: 40 | for raw in raw_list.split(): 41 | raw_to_friendly[raw] = (ns, friendly) 42 | 43 | with csv_file.open(newline='') as f: 44 | f = csv.reader(f) 45 | next(f, None) # header 46 | for line, (method, usability, errors) in enumerate(f, start=2): 47 | try: 48 | errors = [errors_dict[x] for x in errors.split()] 49 | except KeyError: 50 | raise ValueError('Method {} references unknown errors {}' 51 | .format(method, errors)) from None 52 | 53 | friendly = raw_to_friendly.pop(method, None) 54 | yield MethodInfo(method, usability, errors, friendly) 55 | 56 | if raw_to_friendly: 57 | warnings.warn('note: unknown raw methods in friendly mapping: {}' 58 | .format(', '.join(raw_to_friendly))) 59 | -------------------------------------------------------------------------------- /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/tcpobfuscated.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .tcpabridged import AbridgedPacketCodec 4 | from .connection import ObfuscatedConnection 5 | 6 | from ...crypto import AESModeCTR 7 | 8 | 9 | class ObfuscatedIO: 10 | header = None 11 | 12 | def __init__(self, connection): 13 | self._reader = connection._reader 14 | self._writer = connection._writer 15 | 16 | (self.header, 17 | self._encrypt, 18 | self._decrypt) = self.init_header(connection.packet_codec) 19 | 20 | @staticmethod 21 | def init_header(packet_codec): 22 | # Obfuscated messages secrets cannot start with any of these 23 | keywords = (b'PVrG', b'GET ', b'POST', b'\xee\xee\xee\xee') 24 | while True: 25 | random = os.urandom(64) 26 | if (random[0] != 0xef and 27 | random[:4] not in keywords and 28 | random[4:8] != b'\0\0\0\0'): 29 | break 30 | 31 | random = bytearray(random) 32 | random_reversed = random[55:7:-1] # Reversed (8, len=48) 33 | 34 | # Encryption has "continuous buffer" enabled 35 | encrypt_key = bytes(random[8:40]) 36 | encrypt_iv = bytes(random[40:56]) 37 | decrypt_key = bytes(random_reversed[:32]) 38 | decrypt_iv = bytes(random_reversed[32:48]) 39 | 40 | encryptor = AESModeCTR(encrypt_key, encrypt_iv) 41 | decryptor = AESModeCTR(decrypt_key, decrypt_iv) 42 | 43 | random[56:60] = packet_codec.obfuscate_tag 44 | random[56:64] = encryptor.encrypt(bytes(random))[56:64] 45 | return (random, encryptor, decryptor) 46 | 47 | async def readexactly(self, n): 48 | return self._decrypt.encrypt(await self._reader.readexactly(n)) 49 | 50 | def write(self, data): 51 | self._writer.write(self._encrypt.encrypt(data)) 52 | 53 | 54 | class ConnectionTcpObfuscated(ObfuscatedConnection): 55 | """ 56 | Mode that Telegram defines as "obfuscated2". Encodes the packet 57 | just like `ConnectionTcpAbridged`, but encrypts every message with 58 | a randomly generated key using the AES-CTR mode so the packets are 59 | harder to discern. 60 | """ 61 | obfuscated_io = ObfuscatedIO 62 | packet_codec = AbridgedPacketCodec 63 | -------------------------------------------------------------------------------- /readthedocs/basic/next-steps.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Next Steps 3 | ========== 4 | 5 | These basic first steps should have gotten you started with the library. 6 | 7 | By now, you should know how to call friendly methods and how to work with 8 | the returned objects, how things work inside event handlers, etc. 9 | 10 | Next, we will see a quick reference summary of *all* the methods and 11 | properties that you will need when using the library. If you follow 12 | the links there, you will expand the documentation for the method 13 | and property, with more examples on how to use them. 14 | 15 | Therefore, **you can find an example on every method** of the client 16 | to learn how to use it, as well as a description of all the arguments. 17 | 18 | After that, we will go in-depth with some other important concepts 19 | that are worth learning and understanding. 20 | 21 | From now on, you can keep pressing the "Next" button if you want, 22 | or use the menu on the left, since some pages are quite lengthy. 23 | 24 | A note on developing applications 25 | ================================= 26 | 27 | If you're using the library to make an actual application (and not just 28 | automate things), you should make sure to `comply with the ToS`__: 29 | 30 | […] when logging in as an existing user, apps are supposed to call 31 | [:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of 32 | Service; this call should be repeated after ``expires`` seconds have 33 | elapsed. If an update to the Terms Of Service is available, clients are 34 | supposed to show a consent popup; if accepted, clients should call 35 | [:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON 36 | object; in case of denial, clients are to delete the account using 37 | [:tl:`DeleteAccount`], providing Decline ToS update as deletion reason. 38 | 39 | .. __: https://core.telegram.org/api/config#terms-of-service 40 | 41 | However, if you use the library to automate or enhance your Telegram 42 | experience, it's very likely that you are using other applications doing this 43 | check for you (so you wouldn't run the risk of violating the ToS). 44 | 45 | The library itself will not automatically perform this check or accept the ToS 46 | because it should require user action (the only exception is during sign-up). 47 | -------------------------------------------------------------------------------- /telethon/tl/custom/forward.py: -------------------------------------------------------------------------------- 1 | from .chatgetter import ChatGetter 2 | from .sendergetter import SenderGetter 3 | from ... import utils, helpers 4 | from ...tl import types 5 | 6 | 7 | class Forward(ChatGetter, SenderGetter): 8 | """ 9 | Custom class that encapsulates a :tl:`MessageFwdHeader` providing an 10 | abstraction to easily access information like the original sender. 11 | 12 | Remember that this class implements `ChatGetter 13 | ` 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._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._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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_offset_at_emoji(): 57 | """ 58 | Tests that an entity starting at a emoji preserves the emoji. 59 | """ 60 | text = 'Hi\n👉 See example' 61 | entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] 62 | parsed = '**Hi**\n__👉__ See **example**' 63 | 64 | assert markdown.parse(parsed) == (text, entities) 65 | assert markdown.unparse(text, entities) == parsed 66 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_offset_at_emoji(): 57 | """ 58 | Tests that an entity starting at a emoji preserves the emoji. 59 | """ 60 | text = 'Hi\n👉 See example' 61 | entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)] 62 | parsed = 'Hi\n👉 See example' 63 | 64 | assert html.parse(parsed) == (text, entities) 65 | assert html.unparse(text, entities) == parsed 66 | -------------------------------------------------------------------------------- /readthedocs/misc/wall-of-shame.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Wall of Shame 3 | ============= 4 | 5 | 6 | This project has an 7 | `issues `__ section for 8 | you to file **issues** whenever you encounter any when working with the 9 | library. Said section is **not** for issues on *your* program but rather 10 | issues with Telethon itself. 11 | 12 | If you have not made the effort to 1. read through the docs and 2. 13 | `look for the method you need `__, 14 | you will end up on the `Wall of 15 | Shame `__, 16 | i.e. all issues labeled 17 | `"RTFM" `__: 18 | 19 | **rtfm** 20 | Literally "Read The F--king Manual"; a term showing the 21 | frustration of being bothered with questions so trivial that the asker 22 | could have quickly figured out the answer on their own with minimal 23 | effort, usually by reading readily-available documents. People who 24 | say"RTFM!" might be considered rude, but the true rude ones are the 25 | annoying people who take absolutely no self-responibility and expect to 26 | have all the answers handed to them personally. 27 | 28 | *"Damn, that's the twelveth time that somebody posted this question 29 | to the messageboard today! RTFM, already!"* 30 | 31 | *by Bill M. July 27, 2004* 32 | 33 | If you have indeed read the docs, and have tried looking for the method, 34 | and yet you didn't find what you need, **that's fine**. Telegram's API 35 | can have some obscure names at times, and for this reason, there is a 36 | `"question" 37 | label `__ 38 | with questions that are okay to ask. Just state what you've tried so 39 | that we know you've made an effort, or you'll go to the Wall of Shame. 40 | 41 | Of course, if the issue you're going to open is not even a question but 42 | a real issue with the library (thankfully, most of the issues have been 43 | that!), you won't end up here. Don't worry. 44 | 45 | Current winner 46 | -------------- 47 | 48 | The current winner is `issue 49 | 213 `__: 50 | 51 | **Issue:** 52 | 53 | .. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg 54 | 55 | :alt: Winner issue 56 | 57 | Winner issue 58 | 59 | **Answer:** 60 | 61 | .. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg 62 | 63 | :alt: Winner issue answer 64 | 65 | Winner issue answer 66 | -------------------------------------------------------------------------------- /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 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 = asyncio.get_event_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.str_code = name 48 | self.subclass = _get_class_name(codes[0]) 49 | self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES 50 | self.description = description 51 | 52 | self.has_captures = '_X' in name 53 | if self.has_captures: 54 | self.name = _get_class_name(name.replace('_X', '_')) 55 | self.pattern = name.replace('_X', r'_(\d+)') 56 | self.capture_name = re.search(r'{(\w+)}', description).group(1) 57 | else: 58 | self.name = _get_class_name(name) 59 | self.pattern = name 60 | self.capture_name = None 61 | 62 | 63 | def parse_errors(csv_file): 64 | """ 65 | Parses the input CSV file with columns (name, error codes, description) 66 | and yields `Error` instances as a result. 67 | """ 68 | with csv_file.open(newline='') as f: 69 | f = csv.reader(f) 70 | next(f, None) # header 71 | for line, tup in enumerate(f, start=2): 72 | try: 73 | name, codes, description = tup 74 | except ValueError: 75 | raise ValueError('Columns count mismatch, unquoted comma in ' 76 | 'desc? (line {})'.format(line)) from None 77 | 78 | try: 79 | codes = [int(x) for x in codes.split()] or [400] 80 | except ValueError: 81 | raise ValueError('Not all codes are integers ' 82 | '(line {})'.format(line)) from None 83 | 84 | yield Error([int(x) for x in codes], name, description) 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/master.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/wall-of-shame.rst 107 | misc/compatibility-and-convenience 108 | 109 | .. toctree:: 110 | :hidden: 111 | :caption: Telethon Modules 112 | 113 | modules/client 114 | modules/events 115 | modules/custom 116 | modules/utils 117 | modules/errors 118 | modules/sessions 119 | modules/network 120 | modules/helpers 121 | -------------------------------------------------------------------------------- /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/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 | inline_only: bool = False) -> '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 | inline_only (`bool`, optional): 30 | Whether the buttons **must** be inline buttons only or not. 31 | 32 | Example 33 | .. code-block:: python 34 | 35 | from telethon import Button 36 | 37 | markup = client.build_reply_markup(Button.inline('hi')) 38 | # later 39 | await client.send_message(chat, 'click me', buttons=markup) 40 | """ 41 | if buttons is None: 42 | return None 43 | 44 | try: 45 | if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: 46 | return buttons # crc32(b'ReplyMarkup'): 47 | except AttributeError: 48 | pass 49 | 50 | if not utils.is_list_like(buttons): 51 | buttons = [[buttons]] 52 | elif not buttons or not utils.is_list_like(buttons[0]): 53 | buttons = [buttons] 54 | 55 | is_inline = False 56 | is_normal = False 57 | resize = None 58 | single_use = None 59 | selective = None 60 | 61 | rows = [] 62 | for row in buttons: 63 | current = [] 64 | for button in row: 65 | if isinstance(button, custom.Button): 66 | if button.resize is not None: 67 | resize = button.resize 68 | if button.single_use is not None: 69 | single_use = button.single_use 70 | if button.selective is not None: 71 | selective = button.selective 72 | 73 | button = button.button 74 | elif isinstance(button, custom.MessageButton): 75 | button = button.button 76 | 77 | inline = custom.Button._is_inline(button) 78 | is_inline |= inline 79 | is_normal |= not inline 80 | 81 | if button.SUBCLASS_OF_ID == 0xbad74a3: 82 | # 0xbad74a3 == crc32(b'KeyboardButton') 83 | current.append(button) 84 | 85 | if current: 86 | rows.append(types.KeyboardButtonRow(current)) 87 | 88 | if inline_only and is_normal: 89 | raise ValueError('You cannot use non-inline buttons here') 90 | elif is_inline == is_normal and is_normal: 91 | raise ValueError('You cannot mix inline with normal buttons') 92 | elif is_inline: 93 | return types.ReplyInlineMarkup(rows) 94 | # elif is_normal: 95 | return types.ReplyKeyboardMarkup( 96 | rows, resize=resize, single_use=single_use, selective=selective) 97 | -------------------------------------------------------------------------------- /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_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/tl/custom/sendergetter.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class SenderGetter(abc.ABC): 5 | """ 6 | Helper base class that introduces the `sender`, `input_sender` 7 | and `sender_id` properties and `get_sender` and `get_input_sender` 8 | methods. 9 | """ 10 | def __init__(self, sender_id=None, *, sender=None, input_sender=None): 11 | self._sender_id = sender_id 12 | self._sender = sender 13 | self._input_sender = input_sender 14 | self._client = None 15 | 16 | @property 17 | def sender(self): 18 | """ 19 | Returns the :tl:`User` or :tl:`Channel` that sent this object. 20 | It may be `None` if Telegram didn't send the sender. 21 | 22 | If you only need the ID, use `sender_id` instead. 23 | 24 | If you need to call a method which needs 25 | this chat, use `input_sender` instead. 26 | 27 | If you're using `telethon.events`, use `get_sender()` instead. 28 | """ 29 | return self._sender 30 | 31 | async def get_sender(self): 32 | """ 33 | Returns `sender`, but will make an API call to find the 34 | sender unless it's already cached. 35 | 36 | If you only need the ID, use `sender_id` instead. 37 | 38 | If you need to call a method which needs 39 | this sender, use `get_input_sender()` instead. 40 | """ 41 | # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`. 42 | # It's a flag that will be set if only minimal information is 43 | # available (such as display name, but username may be missing), 44 | # in which case we want to force fetch the entire thing because 45 | # the user explicitly called a method. If the user is okay with 46 | # cached information, they may use the property instead. 47 | if (self._sender is None or getattr(self._sender, 'min', None)) \ 48 | and await self.get_input_sender(): 49 | try: 50 | self._sender =\ 51 | await self._client.get_entity(self._input_sender) 52 | except ValueError: 53 | await self._refetch_sender() 54 | return self._sender 55 | 56 | @property 57 | def input_sender(self): 58 | """ 59 | This :tl:`InputPeer` is the input version of the user/channel who 60 | sent the message. Similarly to `input_chat 61 | `, this doesn't 62 | have things like username or similar, but still useful in some cases. 63 | 64 | Note that this might not be available if the library can't 65 | find the input chat, or if the message a broadcast on a channel. 66 | """ 67 | if self._input_sender is None and self._sender_id and self._client: 68 | try: 69 | self._input_sender = \ 70 | self._client._entity_cache[self._sender_id] 71 | except KeyError: 72 | pass 73 | return self._input_sender 74 | 75 | async def get_input_sender(self): 76 | """ 77 | Returns `input_sender`, but will make an API call to find the 78 | input sender unless it's already cached. 79 | """ 80 | if self.input_sender is None and self._sender_id and self._client: 81 | await self._refetch_sender() 82 | return self._input_sender 83 | 84 | @property 85 | def sender_id(self): 86 | """ 87 | Returns the marked sender integer ID, if present. 88 | 89 | If there is a sender in the object, `sender_id` will *always* be set, 90 | which is why you should use it instead of `sender.id `. 91 | """ 92 | return self._sender_id 93 | 94 | async def _refetch_sender(self): 95 | """ 96 | Re-fetches sender information through other means. 97 | """ 98 | -------------------------------------------------------------------------------- /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 | try: 17 | import tgcrypto 18 | __log__.info('tgcrypto detected, it will be used for encryption') 19 | except ImportError: 20 | tgcrypto = None 21 | try: 22 | import cryptg 23 | __log__.info('cryptg detected, it will be used for encryption') 24 | except ImportError: 25 | cryptg = None 26 | if libssl.encrypt_ige and libssl.decrypt_ige: 27 | __log__.info('libssl detected, it will be used for encryption') 28 | else: 29 | __log__.info('tgcrypto or cryptg module not installed and libssl not found, ' 30 | 'falling back to (slower) Python encryption') 31 | 32 | 33 | class AES: 34 | """ 35 | Class that servers as an interface to encrypt and decrypt 36 | text through the AES IGE mode. 37 | """ 38 | @staticmethod 39 | def decrypt_ige(cipher_text, key, iv): 40 | """ 41 | Decrypts the given text in 16-bytes blocks by using the 42 | given key and 32-bytes initialization vector. 43 | """ 44 | if tgcrypto: 45 | return tgcrypto.ige256_decrypt(cipher_text, key, iv) 46 | if cryptg: 47 | return cryptg.decrypt_ige(cipher_text, key, iv) 48 | if libssl.decrypt_ige: 49 | return libssl.decrypt_ige(cipher_text, key, iv) 50 | 51 | iv1 = iv[:len(iv) // 2] 52 | iv2 = iv[len(iv) // 2:] 53 | 54 | aes = pyaes.AES(key) 55 | 56 | plain_text = [] 57 | blocks_count = len(cipher_text) // 16 58 | 59 | cipher_text_block = [0] * 16 60 | for block_index in range(blocks_count): 61 | for i in range(16): 62 | cipher_text_block[i] = \ 63 | cipher_text[block_index * 16 + i] ^ iv2[i] 64 | 65 | plain_text_block = aes.decrypt(cipher_text_block) 66 | 67 | for i in range(16): 68 | plain_text_block[i] ^= iv1[i] 69 | 70 | iv1 = cipher_text[block_index * 16:block_index * 16 + 16] 71 | iv2 = plain_text_block 72 | 73 | plain_text.extend(plain_text_block) 74 | 75 | return bytes(plain_text) 76 | 77 | @staticmethod 78 | def encrypt_ige(plain_text, key, iv): 79 | """ 80 | Encrypts the given text in 16-bytes blocks by using the 81 | given key and 32-bytes initialization vector. 82 | """ 83 | padding = len(plain_text) % 16 84 | if padding: 85 | plain_text += os.urandom(16 - padding) 86 | 87 | if tgcrypto: 88 | return tgcrypto.ige256_encrypt(plain_text, key, iv) 89 | if cryptg: 90 | return cryptg.encrypt_ige(plain_text, key, iv) 91 | if libssl.encrypt_ige: 92 | return libssl.encrypt_ige(plain_text, key, iv) 93 | 94 | iv1 = iv[:len(iv) // 2] 95 | iv2 = iv[len(iv) // 2:] 96 | 97 | aes = pyaes.AES(key) 98 | 99 | cipher_text = [] 100 | blocks_count = len(plain_text) // 16 101 | 102 | for block_index in range(blocks_count): 103 | plain_text_block = list( 104 | plain_text[block_index * 16:block_index * 16 + 16] 105 | ) 106 | for i in range(16): 107 | plain_text_block[i] ^= iv1[i] 108 | 109 | cipher_text_block = aes.encrypt(plain_text_block) 110 | 111 | for i in range(16): 112 | cipher_text_block[i] ^= iv2[i] 113 | 114 | iv1 = cipher_text_block 115 | iv2 = plain_text[block_index * 16:block_index * 16 + 16] 116 | 117 | cipher_text.extend(cipher_text_block) 118 | 119 | return bytes(cipher_text) 120 | -------------------------------------------------------------------------------- /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 | sign_up 36 | log_out 37 | edit_2fa 38 | 39 | Base 40 | ---- 41 | 42 | .. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient 43 | 44 | .. autosummary:: 45 | :nosignatures: 46 | 47 | connect 48 | disconnect 49 | is_connected 50 | disconnected 51 | loop 52 | set_proxy 53 | 54 | Messages 55 | -------- 56 | 57 | .. py:currentmodule:: telethon.client.messages.MessageMethods 58 | 59 | .. autosummary:: 60 | :nosignatures: 61 | 62 | send_message 63 | edit_message 64 | delete_messages 65 | forward_messages 66 | iter_messages 67 | get_messages 68 | pin_message 69 | unpin_message 70 | send_read_acknowledge 71 | 72 | Uploads 73 | ------- 74 | 75 | .. py:currentmodule:: telethon.client.uploads.UploadMethods 76 | 77 | .. autosummary:: 78 | :nosignatures: 79 | 80 | send_file 81 | upload_file 82 | 83 | Downloads 84 | --------- 85 | 86 | .. currentmodule:: telethon.client.downloads.DownloadMethods 87 | 88 | .. autosummary:: 89 | :nosignatures: 90 | 91 | download_media 92 | download_profile_photo 93 | download_file 94 | iter_download 95 | 96 | Dialogs 97 | ------- 98 | 99 | .. py:currentmodule:: telethon.client.dialogs.DialogMethods 100 | 101 | .. autosummary:: 102 | :nosignatures: 103 | 104 | iter_dialogs 105 | get_dialogs 106 | edit_folder 107 | iter_drafts 108 | get_drafts 109 | delete_dialog 110 | conversation 111 | 112 | Users 113 | ----- 114 | 115 | .. py:currentmodule:: telethon.client.users.UserMethods 116 | 117 | .. autosummary:: 118 | :nosignatures: 119 | 120 | get_me 121 | is_bot 122 | is_user_authorized 123 | get_entity 124 | get_input_entity 125 | get_peer_id 126 | 127 | Chats 128 | ----- 129 | 130 | .. currentmodule:: telethon.client.chats.ChatMethods 131 | 132 | .. autosummary:: 133 | :nosignatures: 134 | 135 | iter_participants 136 | get_participants 137 | kick_participant 138 | iter_admin_log 139 | get_admin_log 140 | iter_profile_photos 141 | get_profile_photos 142 | edit_admin 143 | edit_permissions 144 | get_permissions 145 | get_stats 146 | action 147 | 148 | Parse Mode 149 | ---------- 150 | 151 | .. py:currentmodule:: telethon.client.messageparse.MessageParseMethods 152 | 153 | .. autosummary:: 154 | :nosignatures: 155 | 156 | parse_mode 157 | 158 | Updates 159 | ------- 160 | 161 | .. py:currentmodule:: telethon.client.updates.UpdateMethods 162 | 163 | .. autosummary:: 164 | :nosignatures: 165 | 166 | on 167 | run_until_disconnected 168 | add_event_handler 169 | remove_event_handler 170 | list_event_handlers 171 | catch_up 172 | set_receive_updates 173 | 174 | Bots 175 | ---- 176 | 177 | .. currentmodule:: telethon.client.bots.BotMethods 178 | 179 | .. autosummary:: 180 | :nosignatures: 181 | 182 | inline_query 183 | 184 | Buttons 185 | ------- 186 | 187 | .. currentmodule:: telethon.client.buttons.ButtonMethods 188 | 189 | .. autosummary:: 190 | :nosignatures: 191 | 192 | build_reply_markup 193 | 194 | Account 195 | ------- 196 | 197 | .. currentmodule:: telethon.client.account.AccountMethods 198 | 199 | .. autosummary:: 200 | :nosignatures: 201 | 202 | takeout 203 | end_takeout 204 | -------------------------------------------------------------------------------- /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 | self._client._on_login(user) 117 | return user 118 | 119 | raise TypeError('Login token response was unexpected: {}'.format(resp)) 120 | -------------------------------------------------------------------------------- /telethon/tl/custom/participantpermissions.py: -------------------------------------------------------------------------------- 1 | from .. import types 2 | 3 | 4 | def _admin_prop(field_name, doc): 5 | """ 6 | Helper method to build properties that return `True` if the user is an 7 | administrator of a normal chat, or otherwise return `True` if the user 8 | has a specific permission being an admin of a channel. 9 | """ 10 | def fget(self): 11 | if not self.is_admin: 12 | return False 13 | if self.is_chat: 14 | return True 15 | 16 | return getattr(self.participant.admin_rights, field_name) 17 | 18 | return {'fget': fget, 'doc': doc} 19 | 20 | 21 | class ParticipantPermissions: 22 | """ 23 | Participant permissions information. 24 | 25 | The properties in this objects are boolean values indicating whether the 26 | user has the permission or not. 27 | 28 | Example 29 | .. code-block:: python 30 | 31 | permissions = ... 32 | 33 | if permissions.is_banned: 34 | "this user is banned" 35 | elif permissions.is_admin: 36 | "this user is an administrator" 37 | """ 38 | def __init__(self, participant, chat: bool): 39 | self.participant = participant 40 | self.is_chat = chat 41 | 42 | @property 43 | def is_admin(self): 44 | """ 45 | Whether the user is an administrator of the chat or not. The creator 46 | also counts as begin an administrator, since they have all permissions. 47 | """ 48 | return self.is_creator or isinstance(self.participant, ( 49 | types.ChannelParticipantAdmin, 50 | types.ChatParticipantAdmin 51 | )) 52 | 53 | @property 54 | def is_creator(self): 55 | """ 56 | Whether the user is the creator of the chat or not. 57 | """ 58 | return isinstance(self.participant, ( 59 | types.ChannelParticipantCreator, 60 | types.ChatParticipantCreator 61 | )) 62 | 63 | @property 64 | def has_default_permissions(self): 65 | """ 66 | Whether the user is a normal user of the chat (not administrator, but 67 | not banned either, and has no restrictions applied). 68 | """ 69 | return isinstance(self.participant, ( 70 | types.ChannelParticipant, 71 | types.ChatParticipant, 72 | types.ChannelParticipantSelf 73 | )) 74 | 75 | @property 76 | def is_banned(self): 77 | """ 78 | Whether the user is banned in the chat. 79 | """ 80 | return isinstance(self.participant, types.ChannelParticipantBanned) 81 | 82 | @property 83 | def has_left(self): 84 | """ 85 | Whether the user left the chat. 86 | """ 87 | return isinstance(self.participant, types.ChannelParticipantLeft) 88 | 89 | @property 90 | def add_admins(self): 91 | """ 92 | Whether the administrator can add new administrators with the same or 93 | less permissions than them. 94 | """ 95 | if not self.is_admin: 96 | return False 97 | 98 | if self.is_chat: 99 | return self.is_creator 100 | 101 | return self.participant.admin_rights.add_admins 102 | 103 | ban_users = property(**_admin_prop('ban_users', """ 104 | Whether the administrator can ban other users or not. 105 | """)) 106 | 107 | pin_messages = property(**_admin_prop('pin_messages', """ 108 | Whether the administrator can pin messages or not. 109 | """)) 110 | 111 | invite_users = property(**_admin_prop('invite_users', """ 112 | Whether the administrator can add new users to the chat. 113 | """)) 114 | 115 | delete_messages = property(**_admin_prop('delete_messages', """ 116 | Whether the administrator can delete messages from other participants. 117 | """)) 118 | 119 | edit_messages = property(**_admin_prop('edit_messages', """ 120 | Whether the administrator can edit messages. 121 | """)) 122 | 123 | post_messages = property(**_admin_prop('post_messages', """ 124 | Whether the administrator can post messages in the broadcast channel. 125 | """)) 126 | 127 | change_info = property(**_admin_prop('change_info', """ 128 | Whether the administrator can change the information about the chat, 129 | such as title or description. 130 | """)) 131 | 132 | anonymous = property(**_admin_prop('anonymous', """ 133 | Whether the administrator will remain anonymous when sending messages. 134 | """)) 135 | 136 | manage_call = property(**_admin_prop('manage_call', """ 137 | Whether the user will be able to manage group calls. 138 | """)) 139 | -------------------------------------------------------------------------------- /telethon/tl/custom/file.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | 4 | from ... import utils 5 | from ...tl import types 6 | 7 | 8 | class File: 9 | """ 10 | Convenience class over media like photos or documents, which 11 | supports accessing the attributes in a more convenient way. 12 | 13 | If any of the attributes are not present in the current media, 14 | the properties will be `None`. 15 | 16 | The original media is available through the ``media`` attribute. 17 | """ 18 | def __init__(self, media): 19 | self.media = media 20 | 21 | @property 22 | def id(self): 23 | """ 24 | The bot-API style ``file_id`` representing this file. 25 | 26 | .. note:: 27 | 28 | This file ID may not work under user accounts, 29 | but should still be usable by bot accounts. 30 | 31 | You can, however, still use it to identify 32 | a file in for example a database. 33 | """ 34 | return utils.pack_bot_file_id(self.media) 35 | 36 | @property 37 | def name(self): 38 | """ 39 | The file name of this document. 40 | """ 41 | return self._from_attr(types.DocumentAttributeFilename, 'file_name') 42 | 43 | @property 44 | def ext(self): 45 | """ 46 | The extension from the mime type of this file. 47 | 48 | If the mime type is unknown, the extension 49 | from the file name (if any) will be used. 50 | """ 51 | return ( 52 | mimetypes.guess_extension(self.mime_type) 53 | or os.path.splitext(self.name or '')[-1] 54 | or None 55 | ) 56 | 57 | @property 58 | def mime_type(self): 59 | """ 60 | The mime-type of this file. 61 | """ 62 | if isinstance(self.media, types.Photo): 63 | return 'image/jpeg' 64 | elif isinstance(self.media, types.Document): 65 | return self.media.mime_type 66 | 67 | @property 68 | def width(self): 69 | """ 70 | The width in pixels of this media if it's a photo or a video. 71 | """ 72 | if isinstance(self.media, types.Photo): 73 | return max(getattr(s, 'w', 0) for s in self.media.sizes) 74 | 75 | return self._from_attr(( 76 | types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'w') 77 | 78 | @property 79 | def height(self): 80 | """ 81 | The height in pixels of this media if it's a photo or a video. 82 | """ 83 | if isinstance(self.media, types.Photo): 84 | return max(getattr(s, 'h', 0) for s in self.media.sizes) 85 | 86 | return self._from_attr(( 87 | types.DocumentAttributeImageSize, types.DocumentAttributeVideo), 'h') 88 | 89 | @property 90 | def duration(self): 91 | """ 92 | The duration in seconds of the audio or video. 93 | """ 94 | return self._from_attr(( 95 | types.DocumentAttributeAudio, types.DocumentAttributeVideo), 'duration') 96 | 97 | @property 98 | def title(self): 99 | """ 100 | The title of the song. 101 | """ 102 | return self._from_attr(types.DocumentAttributeAudio, 'title') 103 | 104 | @property 105 | def performer(self): 106 | """ 107 | The performer of the song. 108 | """ 109 | return self._from_attr(types.DocumentAttributeAudio, 'performer') 110 | 111 | @property 112 | def emoji(self): 113 | """ 114 | A string with all emoji that represent the current sticker. 115 | """ 116 | return self._from_attr((types.DocumentAttributeCustomEmoji, types.DocumentAttributeSticker), 'alt') 117 | 118 | @property 119 | def sticker_set(self): 120 | """ 121 | The :tl:`InputStickerSet` to which the sticker file belongs. 122 | """ 123 | return self._from_attr((types.DocumentAttributeSticker, types.DocumentAttributeCustomEmoji), 'stickerset') 124 | 125 | @property 126 | def size(self): 127 | """ 128 | The size in bytes of this file. 129 | 130 | For photos, this is the heaviest thumbnail, as it often repressents the largest dimensions. 131 | """ 132 | if isinstance(self.media, types.Photo): 133 | return max(filter(None, map(utils._photo_size_byte_count, self.media.sizes)), default=None) 134 | elif isinstance(self.media, types.Document): 135 | return self.media.size 136 | 137 | def _from_attr(self, cls, field): 138 | if isinstance(self.media, types.Document): 139 | for attr in self.media.attributes: 140 | if isinstance(attr, cls): 141 | return getattr(attr, field, None) 142 | -------------------------------------------------------------------------------- /telethon/events/__init__.py: -------------------------------------------------------------------------------- 1 | from .raw import Raw 2 | from .album import Album 3 | from .chataction import ChatAction 4 | from .messagedeleted import MessageDeleted 5 | from .messageedited import MessageEdited 6 | from .messageread import MessageRead 7 | from .newmessage import NewMessage 8 | from .userupdate import UserUpdate 9 | from .callbackquery import CallbackQuery 10 | from .inlinequery import InlineQuery 11 | 12 | 13 | _HANDLERS_ATTRIBUTE = '__tl.handlers' 14 | 15 | 16 | class StopPropagation(Exception): 17 | """ 18 | If this exception is raised in any of the handlers for a given event, 19 | it will stop the execution of all other registered event handlers. 20 | It can be seen as the ``StopIteration`` in a for loop but for events. 21 | 22 | Example usage: 23 | 24 | >>> from telethon import TelegramClient, events 25 | >>> client = TelegramClient(...) 26 | >>> 27 | >>> @client.on(events.NewMessage) 28 | ... async def delete(event): 29 | ... await event.delete() 30 | ... # No other event handler will have a chance to handle this event 31 | ... raise StopPropagation 32 | ... 33 | >>> @client.on(events.NewMessage) 34 | ... async def _(event): 35 | ... # Will never be reached, because it is the second handler 36 | ... pass 37 | """ 38 | # For some reason Sphinx wants the silly >>> or 39 | # it will show warnings and look bad when generated. 40 | pass 41 | 42 | 43 | def register(event=None): 44 | """ 45 | Decorator method to *register* event handlers. This is the client-less 46 | `add_event_handler() 47 | ` variant. 48 | 49 | Note that this method only registers callbacks as handlers, 50 | and does not attach them to any client. This is useful for 51 | external modules that don't have access to the client, but 52 | still want to define themselves as a handler. Example: 53 | 54 | >>> from telethon import events 55 | >>> @events.register(events.NewMessage) 56 | ... async def handler(event): 57 | ... ... 58 | ... 59 | >>> # (somewhere else) 60 | ... 61 | >>> from telethon import TelegramClient 62 | >>> client = TelegramClient(...) 63 | >>> client.add_event_handler(handler) 64 | 65 | Remember that you can use this as a non-decorator 66 | through ``register(event)(callback)``. 67 | 68 | Args: 69 | event (`_EventBuilder` | `type`): 70 | The event builder class or instance to be used, 71 | for instance ``events.NewMessage``. 72 | """ 73 | if isinstance(event, type): 74 | event = event() 75 | elif not event: 76 | event = Raw() 77 | 78 | def decorator(callback): 79 | handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) 80 | handlers.append(event) 81 | setattr(callback, _HANDLERS_ATTRIBUTE, handlers) 82 | return callback 83 | 84 | return decorator 85 | 86 | 87 | def unregister(callback, event=None): 88 | """ 89 | Inverse operation of `register` (though not a decorator). Client-less 90 | `remove_event_handler 91 | ` 92 | variant. **Note that this won't remove handlers from the client**, 93 | because it simply can't, so you would generally use this before 94 | adding the handlers to the client. 95 | 96 | This method is here for symmetry. You will rarely need to 97 | unregister events, since you can simply just not add them 98 | to any client. 99 | 100 | If no event is given, all events for this callback are removed. 101 | Returns how many callbacks were removed. 102 | """ 103 | found = 0 104 | if event and not isinstance(event, type): 105 | event = type(event) 106 | 107 | handlers = getattr(callback, _HANDLERS_ATTRIBUTE, []) 108 | handlers.append((event, callback)) 109 | i = len(handlers) 110 | while i: 111 | i -= 1 112 | ev = handlers[i] 113 | if not event or isinstance(ev, event): 114 | del handlers[i] 115 | found += 1 116 | 117 | return found 118 | 119 | 120 | def is_handler(callback): 121 | """ 122 | Returns `True` if the given callback is an 123 | event handler (i.e. you used `register` on it). 124 | """ 125 | return hasattr(callback, _HANDLERS_ATTRIBUTE) 126 | 127 | 128 | def list(callback): 129 | """ 130 | Returns a list containing the registered event 131 | builders inside the specified callback handler. 132 | """ 133 | return getattr(callback, _HANDLERS_ATTRIBUTE, [])[:] 134 | 135 | 136 | def _get_handlers(callback): 137 | """ 138 | Like ``list`` but returns `None` if the callback was never registered. 139 | """ 140 | return getattr(callback, _HANDLERS_ATTRIBUTE, None) 141 | --------------------------------------------------------------------------------