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 |
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 |
--------------------------------------------------------------------------------