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 |
49 |
50 | ```
51 |
52 | - type: input
53 | id: telethon-version
54 | attributes:
55 | label: Telethon version
56 | description: The output of `python -c "import telethon; print(telethon.__version__)"`.
57 | placeholder: "1.x"
58 | validations:
59 | required: true
60 |
61 | - type: input
62 | id: python-version
63 | attributes:
64 | label: Python version
65 | description: The output of `python --version`.
66 | placeholder: "3.x"
67 | validations:
68 | required: true
69 |
70 | - type: input
71 | id: os
72 | attributes:
73 | label: Operating system (including distribution name and version)
74 | placeholder: Windows 11, macOS 13.4, Ubuntu 23.04...
75 | validations:
76 | required: true
77 |
78 | - type: textarea
79 | id: other-details
80 | attributes:
81 | label: Other details
82 | placeholder: |
83 | Additional details and attachments. Is it a server? Network condition?
84 |
85 | - type: checkboxes
86 | id: checklist
87 | attributes:
88 | label: Checklist
89 | description: Read this carefully, we will close and ignore your issue if you skimmed through this.
90 | options:
91 | - label: The error is in the library's code, and not in my own.
92 | required: true
93 | - label: I have searched for this issue before posting it and there isn't an open duplicate.
94 | required: true
95 | - label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version.
96 | required: true
97 |
--------------------------------------------------------------------------------
/readthedocs/concepts/strings.rst:
--------------------------------------------------------------------------------
1 | ======================
2 | String-based Debugging
3 | ======================
4 |
5 | Debugging is *really* important. Telegram's API is really big and there
6 | are a lot of things that you should know. Such as, what attributes or fields
7 | does a result have? Well, the easiest thing to do is printing it:
8 |
9 | .. code-block:: python
10 |
11 | entity = await client.get_entity('username')
12 | print(entity)
13 |
14 | That will show a huge **string** similar to the following:
15 |
16 | .. code-block:: python
17 |
18 | Channel(id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty(), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None)
19 |
20 | That's a lot of text. But as you can see, all the properties are there.
21 | So if you want the title you **don't use regex** or anything like
22 | splitting ``str(entity)`` to get what you want. You just access the
23 | attribute you need:
24 |
25 | .. code-block:: python
26 |
27 | title = entity.title
28 |
29 | Can we get better than the shown string, though? Yes!
30 |
31 | .. code-block:: python
32 |
33 | print(entity.stringify())
34 |
35 | Will show a much better representation:
36 |
37 | .. code-block:: python
38 |
39 | Channel(
40 | id=1066197625,
41 | title='Telegram Usernames',
42 | photo=ChatPhotoEmpty(
43 | ),
44 | date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc),
45 | version=0,
46 | creator=False,
47 | left=True,
48 | broadcast=True,
49 | verified=True,
50 | megagroup=False,
51 | restricted=False,
52 | signatures=False,
53 | min=False,
54 | scam=False,
55 | has_link=False,
56 | has_geo=False,
57 | slowmode_enabled=False,
58 | access_hash=-6309373984955162244,
59 | username='username',
60 | restriction_reason=[
61 | ],
62 | admin_rights=None,
63 | banned_rights=None,
64 | default_banned_rights=None,
65 | participants_count=None
66 | )
67 |
68 |
69 | Now it's easy to see how we could get, for example,
70 | the ``year`` value. It's inside ``date``:
71 |
72 | .. code-block:: python
73 |
74 | channel_year = entity.date.year
75 |
76 | You don't need to print everything to see what all the possible values
77 | can be. You can just search in http://tl.telethon.dev/.
78 |
79 | Remember that you can use Python's `isinstance
80 | `_
81 | to check the type of something. For example:
82 |
83 | .. code-block:: python
84 |
85 | from telethon import types
86 |
87 | if isinstance(entity.photo, types.ChatPhotoEmpty):
88 | print('Channel has no photo')
89 |
--------------------------------------------------------------------------------
/tests/telethon/extensions/test_html.py:
--------------------------------------------------------------------------------
1 | """
2 | Tests for `telethon.extensions.html`.
3 | """
4 | from telethon.extensions import html
5 | from telethon.tl.types import MessageEntityBold, MessageEntityItalic, MessageEntityTextUrl
6 |
7 |
8 | def test_entity_edges():
9 | """
10 | Test that entities at the edges (start and end) don't crash.
11 | """
12 | text = 'Hello, world'
13 | entities = [MessageEntityBold(0, 5), MessageEntityBold(7, 5)]
14 | result = html.unparse(text, entities)
15 | assert result == 'Hello, world'
16 |
17 |
18 | def test_malformed_entities():
19 | """
20 | Test that malformed entity offsets from bad clients
21 | don't crash and produce the expected results.
22 | """
23 | text = '🏆Telegram Official Android Challenge is over🏆.'
24 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]
25 | result = html.unparse(text, entities)
26 | assert result == '🏆Telegram Official Android Challenge is over🏆.'
27 |
28 |
29 | def test_trailing_malformed_entities():
30 | """
31 | Similar to `test_malformed_entities`, but for the edge
32 | case where the malformed entity offset is right at the end
33 | (note the lack of a trailing dot in the text string).
34 | """
35 | text = '🏆Telegram Official Android Challenge is over🏆'
36 | entities = [MessageEntityTextUrl(offset=2, length=43, url='https://example.com')]
37 | result = html.unparse(text, entities)
38 | assert result == '🏆Telegram Official Android Challenge is over🏆'
39 |
40 |
41 | def test_entities_together():
42 | """
43 | Test that an entity followed immediately by a different one behaves well.
44 | """
45 | original = '⚙️Settings'
46 | stripped = '⚙️Settings'
47 |
48 | text, entities = html.parse(original)
49 | assert text == stripped
50 | assert entities == [MessageEntityBold(0, 2), MessageEntityItalic(2, 8)]
51 |
52 | text = html.unparse(text, entities)
53 | assert text == original
54 |
55 |
56 | def test_nested_entities():
57 | """
58 | Test that an entity nested inside another one behaves well.
59 | """
60 | original = 'Example'
61 | original_entities = [MessageEntityTextUrl(0, 7, url='https://example.com'), MessageEntityBold(0, 7)]
62 | stripped = 'Example'
63 |
64 | text, entities = html.parse(original)
65 | assert text == stripped
66 | assert entities == original_entities
67 |
68 | text = html.unparse(text, entities)
69 | assert text == original
70 |
71 |
72 | def test_offset_at_emoji():
73 | """
74 | Tests that an entity starting at a emoji preserves the emoji.
75 | """
76 | text = 'Hi\n👉 See example'
77 | entities = [MessageEntityBold(0, 2), MessageEntityItalic(3, 2), MessageEntityBold(10, 7)]
78 | parsed = 'Hi\n👉 See example'
79 |
80 | assert html.parse(parsed) == (text, entities)
81 | assert html.unparse(text, entities) == parsed
82 |
--------------------------------------------------------------------------------
/telethon_generator/parsers/errors.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import re
3 |
4 | from ..utils import snake_to_camel_case
5 |
6 | # Core base classes depending on the integer error code
7 | KNOWN_BASE_CLASSES = {
8 | 303: 'InvalidDCError',
9 | 400: 'BadRequestError',
10 | 401: 'UnauthorizedError',
11 | 403: 'ForbiddenError',
12 | 404: 'NotFoundError',
13 | 406: 'AuthKeyError',
14 | 420: 'FloodError',
15 | 500: 'ServerError',
16 | 503: 'TimedOutError'
17 | }
18 |
19 |
20 | def _get_class_name(error_code):
21 | """
22 | Gets the corresponding class name for the given error code,
23 | this either being an integer (thus base error name) or str.
24 | """
25 | if isinstance(error_code, int):
26 | return KNOWN_BASE_CLASSES.get(
27 | abs(error_code), 'RPCError' + str(error_code).replace('-', 'Neg')
28 | )
29 |
30 | if error_code.startswith('2'):
31 | error_code = re.sub(r'2', 'TWO_', error_code, count=1)
32 |
33 | if re.match(r'\d+', error_code):
34 | raise RuntimeError('error code starting with a digit cannot have valid Python name: {}'.format(error_code))
35 |
36 | return snake_to_camel_case(
37 | error_code.replace('FIRSTNAME', 'FIRST_NAME')\
38 | .replace('SLOWMODE', 'SLOW_MODE').lower(), suffix='Error')
39 |
40 |
41 | class Error:
42 | def __init__(self, codes, name, description):
43 | # TODO Some errors have the same name but different integer codes
44 | # Should these be split into different files or doesn't really matter?
45 | # Telegram isn't exactly consistent with returned errors anyway.
46 | self.int_code = codes[0]
47 | self.int_codes = codes
48 | self.str_code = name
49 | self.subclass = _get_class_name(codes[0])
50 | self.subclass_exists = abs(codes[0]) in KNOWN_BASE_CLASSES
51 | self.description = description
52 |
53 | self.has_captures = '_X' in name
54 | if self.has_captures:
55 | self.name = _get_class_name(name.replace('_X', '_'))
56 | self.pattern = name.replace('_X', r'_(\d+)')
57 | self.capture_name = re.search(r'{(\w+)}', description).group(1)
58 | else:
59 | self.name = _get_class_name(name)
60 | self.pattern = name
61 | self.capture_name = None
62 |
63 |
64 | def parse_errors(csv_file):
65 | """
66 | Parses the input CSV file with columns (name, error codes, description)
67 | and yields `Error` instances as a result.
68 | """
69 | with csv_file.open(newline='') as f:
70 | f = csv.reader(f)
71 | next(f, None) # header
72 | for line, tup in enumerate(f, start=2):
73 | try:
74 | name, codes, description = tup
75 | except ValueError:
76 | raise ValueError('Columns count mismatch, unquoted comma in '
77 | 'desc? (line {})'.format(line)) from None
78 |
79 | try:
80 | codes = [int(x) for x in codes.split()] or [400]
81 | except ValueError:
82 | raise ValueError('Not all codes are integers '
83 | '(line {})'.format(line)) from None
84 |
85 | yield Error([int(x) for x in codes], name, description)
86 |
--------------------------------------------------------------------------------
/tests/telethon/client/test_messages.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from unittest import mock
3 | from unittest.mock import MagicMock
4 |
5 | import pytest
6 |
7 | from telethon import TelegramClient
8 | from telethon.client import MessageMethods
9 | from telethon.tl.types import PeerChat, MessageMediaDocument, Message, MessageEntityBold
10 |
11 |
12 | @pytest.mark.asyncio
13 | async def test_send_message_with_file_forwards_args():
14 | arguments = {}
15 | sentinel = object()
16 |
17 | for value, name in enumerate(inspect.signature(TelegramClient.send_message).parameters):
18 | if name in {'self', 'entity', 'file'}:
19 | continue # positional
20 |
21 | if name in {'message'}:
22 | continue # renamed
23 |
24 | if name in {'link_preview'}:
25 | continue # make no sense in send_file
26 |
27 | arguments[name] = value
28 |
29 | class MockedClient(TelegramClient):
30 | # noinspection PyMissingConstructor
31 | def __init__(self):
32 | pass
33 |
34 | async def send_file(self, entity, file, **kwargs):
35 | assert entity == 'a'
36 | assert file == 'b'
37 | for k, v in arguments.items():
38 | assert k in kwargs
39 | assert kwargs[k] == v
40 |
41 | return sentinel
42 |
43 | client = MockedClient()
44 | assert (await client.send_message('a', file='b', **arguments)) == sentinel
45 |
46 |
47 | class TestMessageMethods:
48 | @pytest.mark.asyncio
49 | @pytest.mark.parametrize(
50 | 'formatting_entities',
51 | ([MessageEntityBold(offset=0, length=0)], None)
52 | )
53 | async def test_send_msg_and_file(self, formatting_entities):
54 | async def async_func(result): # AsyncMock was added only in 3.8
55 | return result
56 | msg_methods = MessageMethods()
57 | expected_result = Message(
58 | id=0, peer_id=PeerChat(chat_id=0), message='', date=None,
59 | )
60 | entity = 'test_entity'
61 | message = Message(
62 | id=1, peer_id=PeerChat(chat_id=0), message='expected_caption', date=None,
63 | entities=[MessageEntityBold(offset=9, length=9)],
64 | )
65 | media_file = MessageMediaDocument()
66 |
67 | with mock.patch.object(
68 | target=MessageMethods, attribute='send_file',
69 | new=MagicMock(return_value=async_func(expected_result)), create=True,
70 | ) as mock_obj:
71 | result = await msg_methods.send_message(
72 | entity=entity, message=message, file=media_file,
73 | formatting_entities=formatting_entities,
74 | )
75 | mock_obj.assert_called_once_with(
76 | entity, media_file, caption=message.message,
77 | formatting_entities=formatting_entities or message.entities,
78 | reply_to=None, silent=None, attributes=None, parse_mode=(),
79 | force_document=False, thumb=None, buttons=None,
80 | clear_draft=False, schedule=None, supports_streaming=False,
81 | comment_to=None, background=None, nosound_video=None,
82 | send_as=None, message_effect_id=None,
83 | )
84 | assert result == expected_result
85 |
--------------------------------------------------------------------------------
/readthedocs/modules/custom.rst:
--------------------------------------------------------------------------------
1 | ==============
2 | Custom package
3 | ==============
4 |
5 | The `telethon.tl.custom` package contains custom classes that the library
6 | uses in order to make working with Telegram easier. Only those that you
7 | are supposed to use will be documented here. You can use undocumented ones
8 | at your own risk.
9 |
10 | More often than not, you don't need to import these (unless you want
11 | type hinting), nor do you need to manually create instances of these
12 | classes. They are returned by client methods.
13 |
14 | .. contents::
15 |
16 | .. automodule:: telethon.tl.custom
17 | :members:
18 | :undoc-members:
19 | :show-inheritance:
20 |
21 |
22 | AdminLogEvent
23 | =============
24 |
25 | .. automodule:: telethon.tl.custom.adminlogevent
26 | :members:
27 | :undoc-members:
28 | :show-inheritance:
29 |
30 |
31 | Button
32 | ======
33 |
34 | .. automodule:: telethon.tl.custom.button
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 |
40 | ChatGetter
41 | ==========
42 |
43 | .. automodule:: telethon.tl.custom.chatgetter
44 | :members:
45 | :undoc-members:
46 | :show-inheritance:
47 |
48 |
49 | Conversation
50 | ============
51 |
52 | .. automodule:: telethon.tl.custom.conversation
53 | :members:
54 | :undoc-members:
55 | :show-inheritance:
56 |
57 |
58 | Dialog
59 | ======
60 |
61 | .. automodule:: telethon.tl.custom.dialog
62 | :members:
63 | :undoc-members:
64 | :show-inheritance:
65 |
66 |
67 | Draft
68 | =====
69 |
70 | .. automodule:: telethon.tl.custom.draft
71 | :members:
72 | :undoc-members:
73 | :show-inheritance:
74 |
75 |
76 | File
77 | ====
78 |
79 | .. automodule:: telethon.tl.custom.file
80 | :members:
81 | :undoc-members:
82 | :show-inheritance:
83 |
84 |
85 | Forward
86 | =======
87 |
88 | .. automodule:: telethon.tl.custom.forward
89 | :members:
90 | :undoc-members:
91 | :show-inheritance:
92 |
93 |
94 | InlineBuilder
95 | =============
96 |
97 | .. automodule:: telethon.tl.custom.inlinebuilder
98 | :members:
99 | :undoc-members:
100 | :show-inheritance:
101 |
102 |
103 | InlineResult
104 | ============
105 |
106 | .. automodule:: telethon.tl.custom.inlineresult
107 | :members:
108 | :undoc-members:
109 | :show-inheritance:
110 |
111 |
112 | InlineResults
113 | =============
114 |
115 | .. automodule:: telethon.tl.custom.inlineresults
116 | :members:
117 | :undoc-members:
118 | :show-inheritance:
119 |
120 |
121 | Message
122 | =======
123 |
124 | .. automodule:: telethon.tl.custom.message
125 | :members:
126 | :undoc-members:
127 | :show-inheritance:
128 |
129 |
130 | MessageButton
131 | =============
132 |
133 | .. automodule:: telethon.tl.custom.messagebutton
134 | :members:
135 | :undoc-members:
136 | :show-inheritance:
137 |
138 |
139 | ParticipantPermissions
140 | ======================
141 |
142 | .. automodule:: telethon.tl.custom.participantpermissions
143 | :members:
144 | :undoc-members:
145 | :show-inheritance:
146 |
147 |
148 | QRLogin
149 | =======
150 |
151 | .. automodule:: telethon.tl.custom.qrlogin
152 | :members:
153 | :undoc-members:
154 | :show-inheritance:
155 |
156 |
157 | SenderGetter
158 | ============
159 |
160 | .. automodule:: telethon.tl.custom.sendergetter
161 | :members:
162 | :undoc-members:
163 | :show-inheritance:
164 |
--------------------------------------------------------------------------------
/readthedocs/basic/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 | Telethon is a Python library, which means you need to download and install
8 | Python from https://www.python.org/downloads/ if you haven't already. Once
9 | you have Python installed, `upgrade pip`__ and run:
10 |
11 | .. code-block:: sh
12 |
13 | python3 -m pip install --upgrade pip
14 | python3 -m pip install --upgrade telethon
15 |
16 | …to install or upgrade the library to the latest version.
17 |
18 | .. __: https://pythonspeed.com/articles/upgrade-pip/
19 |
20 | Installing Development Versions
21 | ===============================
22 |
23 | If you want the *latest* unreleased changes,
24 | you can run the following command instead:
25 |
26 | .. code-block:: sh
27 |
28 | python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip
29 |
30 | .. note::
31 |
32 | The development version may have bugs and is not recommended for production
33 | use. However, when you are `reporting a library bug`__, you should try if the
34 | bug still occurs in this version.
35 |
36 | .. __: https://github.com/LonamiWebs/Telethon/issues/
37 |
38 |
39 | Verification
40 | ============
41 |
42 | To verify that the library is installed correctly, run the following command:
43 |
44 | .. code-block:: sh
45 |
46 | python3 -c "import telethon; print(telethon.__version__)"
47 |
48 | The version number of the library should show in the output.
49 |
50 |
51 | Optional Dependencies
52 | =====================
53 |
54 | If cryptg_ is installed, **the library will work a lot faster**, since
55 | encryption and decryption will be made in C instead of Python. If your
56 | code deals with a lot of updates or you are downloading/uploading a lot
57 | of files, you will notice a considerable speed-up (from a hundred kilobytes
58 | per second to several megabytes per second, if your connection allows it).
59 | If it's not installed, pyaes_ will be used (which is pure Python, so it's
60 | much slower).
61 |
62 | If pillow_ is installed, large images will be automatically resized when
63 | sending photos to prevent Telegram from failing with "invalid image".
64 | Official clients also do this.
65 |
66 | If aiohttp_ is installed, the library will be able to download
67 | :tl:`WebDocument` media files (otherwise you will get an error).
68 |
69 | If hachoir_ is installed, it will be used to extract metadata from files
70 | when sending documents. Telegram uses this information to show the song's
71 | performer, artist, title, duration, and for videos too (including size).
72 | Otherwise, they will default to empty values, and you can set the attributes
73 | manually.
74 |
75 | .. note::
76 |
77 | Some of the modules may require additional dependencies before being
78 | installed through ``pip``. If you have an ``apt``-based system, consider
79 | installing the most commonly missing dependencies (with the right ``pip``):
80 |
81 | .. code-block:: sh
82 |
83 | apt update
84 | apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev
85 | pip install -U --user setuptools
86 | pip install -U --user telethon cryptg pillow
87 |
88 | Thanks to `@bb010g`_ for writing down this nice list.
89 |
90 |
91 | .. _cryptg: https://github.com/cher-nov/cryptg
92 | .. _pyaes: https://github.com/ricmoo/pyaes
93 | .. _pillow: https://python-pillow.org
94 | .. _aiohttp: https://docs.aiohttp.org
95 | .. _hachoir: https://hachoir.readthedocs.io
96 | .. _@bb010g: https://static.bb010g.com
97 |
--------------------------------------------------------------------------------
/readthedocs/index.rst:
--------------------------------------------------------------------------------
1 | ========================
2 | Telethon's Documentation
3 | ========================
4 |
5 | .. code-block:: python
6 |
7 | from telethon.sync import TelegramClient, events
8 |
9 | with TelegramClient('name', api_id, api_hash) as client:
10 | client.send_message('me', 'Hello, myself!')
11 | print(client.download_profile_photo('me'))
12 |
13 | @client.on(events.NewMessage(pattern='(?i).*Hello'))
14 | async def handler(event):
15 | await event.reply('Hey!')
16 |
17 | client.run_until_disconnected()
18 |
19 |
20 | * Are you new here? Jump straight into :ref:`installation`!
21 | * Looking for the method reference? See :ref:`client-ref`.
22 | * Did you upgrade the library? Please read :ref:`changelog`.
23 | * Used Telethon before v1.0? See :ref:`compatibility-and-convenience`.
24 | * Coming from Bot API or want to create new bots? See :ref:`botapi`.
25 | * Need the full API reference? https://tl.telethon.dev/.
26 |
27 |
28 | What is this?
29 | -------------
30 |
31 | Telegram is a popular messaging application. This library is meant
32 | to make it easy for you to write Python programs that can interact
33 | with Telegram. Think of it as a wrapper that has already done the
34 | heavy job for you, so you can focus on developing an application.
35 |
36 |
37 | How should I use the documentation?
38 | -----------------------------------
39 |
40 | If you are getting started with the library, you should follow the
41 | documentation in order by pressing the "Next" button at the bottom-right
42 | of every page.
43 |
44 | You can also use the menu on the left to quickly skip over sections.
45 |
46 | .. toctree::
47 | :hidden:
48 | :caption: First Steps
49 |
50 | basic/installation
51 | basic/signing-in
52 | basic/quick-start
53 | basic/updates
54 | basic/next-steps
55 |
56 | .. toctree::
57 | :hidden:
58 | :caption: Quick References
59 |
60 | quick-references/faq
61 | quick-references/client-reference
62 | quick-references/events-reference
63 | quick-references/objects-reference
64 |
65 | .. toctree::
66 | :hidden:
67 | :caption: Concepts
68 |
69 | concepts/strings
70 | concepts/entities
71 | concepts/chats-vs-channels
72 | concepts/updates
73 | concepts/sessions
74 | concepts/full-api
75 | concepts/errors
76 | concepts/botapi-vs-mtproto
77 | concepts/asyncio
78 |
79 | .. toctree::
80 | :hidden:
81 | :caption: Full API Examples
82 |
83 | examples/word-of-warning
84 | examples/chats-and-channels
85 | examples/users
86 | examples/working-with-messages
87 |
88 | .. toctree::
89 | :hidden:
90 | :caption: Developing
91 |
92 | developing/philosophy.rst
93 | developing/test-servers.rst
94 | developing/project-structure.rst
95 | developing/coding-style.rst
96 | developing/testing.rst
97 | developing/understanding-the-type-language.rst
98 | developing/tips-for-porting-the-project.rst
99 | developing/telegram-api-in-other-languages.rst
100 |
101 | .. toctree::
102 | :hidden:
103 | :caption: Miscellaneous
104 |
105 | misc/changelog
106 | misc/compatibility-and-convenience
107 |
108 | .. toctree::
109 | :hidden:
110 | :caption: Telethon Modules
111 |
112 | modules/client
113 | modules/events
114 | modules/custom
115 | modules/utils
116 | modules/errors
117 | modules/sessions
118 | modules/network
119 | modules/helpers
120 |
--------------------------------------------------------------------------------
/telethon_examples/replier.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | A example script to automatically send messages based on certain triggers.
4 |
5 | This script assumes that you have certain files on the working directory,
6 | such as "xfiles.m4a" or "anytime.png" for some of the automated replies.
7 | """
8 | import os
9 | import sys
10 | import time
11 | from collections import defaultdict
12 |
13 | from telethon import TelegramClient, events
14 |
15 | import logging
16 | logging.basicConfig(level=logging.WARNING)
17 |
18 | # "When did we last react?" dictionary, 0.0 by default
19 | recent_reacts = defaultdict(float)
20 |
21 |
22 | def get_env(name, message, cast=str):
23 | if name in os.environ:
24 | return os.environ[name]
25 | while True:
26 | value = input(message)
27 | try:
28 | return cast(value)
29 | except ValueError as e:
30 | print(e, file=sys.stderr)
31 | time.sleep(1)
32 |
33 |
34 | def can_react(chat_id):
35 | # Get the time when we last sent a reaction (or 0)
36 | last = recent_reacts[chat_id]
37 |
38 | # Get the current time
39 | now = time.time()
40 |
41 | # If 10 minutes as seconds have passed, we can react
42 | if now - last < 10 * 60:
43 | # Make sure we updated the last reaction time
44 | recent_reacts[chat_id] = now
45 | return True
46 | else:
47 | return False
48 |
49 |
50 | # Register `events.NewMessage` before defining the client.
51 | # Once you have a client, `add_event_handler` will use this event.
52 | @events.register(events.NewMessage)
53 | async def handler(event):
54 | # There are better ways to do this, but this is simple.
55 | # If the message is not outgoing (i.e. someone else sent it)
56 | if not event.out:
57 | if 'emacs' in event.raw_text:
58 | if can_react(event.chat_id):
59 | await event.reply('> emacs\nneeds more vim')
60 |
61 | elif 'vim' in event.raw_text:
62 | if can_react(event.chat_id):
63 | await event.reply('> vim\nneeds more emacs')
64 |
65 | elif 'chrome' in event.raw_text:
66 | if can_react(event.chat_id):
67 | await event.reply('> chrome\nneeds more firefox')
68 |
69 | # Reply always responds as a reply. We can respond without replying too
70 | if 'shrug' in event.raw_text:
71 | if can_react(event.chat_id):
72 | await event.respond(r'¯\_(ツ)_/¯')
73 |
74 | # We can also use client methods from here
75 | client = event.client
76 |
77 | # If we sent the message, we are replying to someone,
78 | # and we said "save pic" in the message
79 | if event.out and event.is_reply and 'save pic' in event.raw_text:
80 | reply_msg = await event.get_reply_message()
81 | replied_to_user = await reply_msg.get_input_sender()
82 |
83 | message = await event.reply('Downloading your profile photo...')
84 | file = await client.download_profile_photo(replied_to_user)
85 | await message.edit('I saved your photo in {}'.format(file))
86 |
87 |
88 | client = TelegramClient(
89 | os.environ.get('TG_SESSION', 'replier'),
90 | get_env('TG_API_ID', 'Enter your API ID: ', int),
91 | get_env('TG_API_HASH', 'Enter your API hash: '),
92 | proxy=None
93 | )
94 |
95 | with client:
96 | # This remembers the events.NewMessage we registered before
97 | client.add_event_handler(handler)
98 |
99 | print('(Press Ctrl+C to stop this)')
100 | client.run_until_disconnected()
101 |
--------------------------------------------------------------------------------
/telethon/crypto/aes.py:
--------------------------------------------------------------------------------
1 | """
2 | AES IGE implementation in Python.
3 |
4 | If available, cryptg will be used instead, otherwise
5 | if available, libssl will be used instead, otherwise
6 | the Python implementation will be used.
7 | """
8 | import os
9 | import pyaes
10 | import logging
11 | from . import libssl
12 |
13 |
14 | __log__ = logging.getLogger(__name__)
15 |
16 |
17 | try:
18 | import cryptg
19 | __log__.info('cryptg detected, it will be used for encryption')
20 | except ImportError:
21 | cryptg = None
22 | if libssl.encrypt_ige and libssl.decrypt_ige:
23 | __log__.info('libssl detected, it will be used for encryption')
24 | else:
25 | __log__.info('cryptg module not installed and libssl not found, '
26 | 'falling back to (slower) Python encryption')
27 |
28 |
29 | class AES:
30 | """
31 | Class that servers as an interface to encrypt and decrypt
32 | text through the AES IGE mode.
33 | """
34 | @staticmethod
35 | def decrypt_ige(cipher_text, key, iv):
36 | """
37 | Decrypts the given text in 16-bytes blocks by using the
38 | given key and 32-bytes initialization vector.
39 | """
40 | if cryptg:
41 | return cryptg.decrypt_ige(cipher_text, key, iv)
42 | if libssl.decrypt_ige:
43 | return libssl.decrypt_ige(cipher_text, key, iv)
44 |
45 | iv1 = iv[:len(iv) // 2]
46 | iv2 = iv[len(iv) // 2:]
47 |
48 | aes = pyaes.AES(key)
49 |
50 | plain_text = []
51 | blocks_count = len(cipher_text) // 16
52 |
53 | cipher_text_block = [0] * 16
54 | for block_index in range(blocks_count):
55 | for i in range(16):
56 | cipher_text_block[i] = \
57 | cipher_text[block_index * 16 + i] ^ iv2[i]
58 |
59 | plain_text_block = aes.decrypt(cipher_text_block)
60 |
61 | for i in range(16):
62 | plain_text_block[i] ^= iv1[i]
63 |
64 | iv1 = cipher_text[block_index * 16:block_index * 16 + 16]
65 | iv2 = plain_text_block
66 |
67 | plain_text.extend(plain_text_block)
68 |
69 | return bytes(plain_text)
70 |
71 | @staticmethod
72 | def encrypt_ige(plain_text, key, iv):
73 | """
74 | Encrypts the given text in 16-bytes blocks by using the
75 | given key and 32-bytes initialization vector.
76 | """
77 | padding = len(plain_text) % 16
78 | if padding:
79 | plain_text += os.urandom(16 - padding)
80 |
81 | if cryptg:
82 | return cryptg.encrypt_ige(plain_text, key, iv)
83 | if libssl.encrypt_ige:
84 | return libssl.encrypt_ige(plain_text, key, iv)
85 |
86 | iv1 = iv[:len(iv) // 2]
87 | iv2 = iv[len(iv) // 2:]
88 |
89 | aes = pyaes.AES(key)
90 |
91 | cipher_text = []
92 | blocks_count = len(plain_text) // 16
93 |
94 | for block_index in range(blocks_count):
95 | plain_text_block = list(
96 | plain_text[block_index * 16:block_index * 16 + 16]
97 | )
98 | for i in range(16):
99 | plain_text_block[i] ^= iv1[i]
100 |
101 | cipher_text_block = aes.encrypt(plain_text_block)
102 |
103 | for i in range(16):
104 | cipher_text_block[i] ^= iv2[i]
105 |
106 | iv1 = cipher_text_block
107 | iv2 = plain_text[block_index * 16:block_index * 16 + 16]
108 |
109 | cipher_text.extend(cipher_text_block)
110 |
111 | return bytes(cipher_text)
112 |
--------------------------------------------------------------------------------
/telethon_generator/data/html/css/docs.light.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Nunito', sans-serif;
3 | color: #333;
4 | background-color:#eee;
5 | font-size: 16px;
6 | }
7 |
8 | a {
9 | color: #329add;
10 | text-decoration: none;
11 | }
12 |
13 | pre {
14 | font-family: 'Source Code Pro', monospace;
15 | padding: 8px;
16 | color: #567;
17 | background: #e0e4e8;
18 | border-radius: 0;
19 | overflow-x: auto;
20 | }
21 |
22 | a:hover {
23 | color: #64bbdd;
24 | text-decoration: underline;
25 | }
26 |
27 | table {
28 | width: 100%;
29 | max-width: 100%;
30 | }
31 |
32 | table td {
33 | border-top: 1px solid #ddd;
34 | padding: 8px;
35 | }
36 |
37 | .horizontal {
38 | margin-bottom: 16px;
39 | list-style: none;
40 | background: #e0e4e8;
41 | border-radius: 4px;
42 | padding: 8px 16px;
43 | }
44 |
45 | .horizontal li {
46 | display: inline-block;
47 | margin: 0 8px 0 0;
48 | }
49 |
50 | .horizontal img {
51 | display: inline-block;
52 | margin: 0 8px -2px 0;
53 | }
54 |
55 | h1, summary.title {
56 | font-size: 24px;
57 | }
58 |
59 | h3 {
60 | font-size: 20px;
61 | }
62 |
63 | #main_div {
64 | padding: 20px 0;
65 | max-width: 800px;
66 | margin: 0 auto;
67 | }
68 |
69 | pre::-webkit-scrollbar {
70 | visibility: visible;
71 | display: block;
72 | height: 12px;
73 | }
74 |
75 | pre::-webkit-scrollbar-track:horizontal {
76 | background: #def;
77 | border-radius: 0;
78 | height: 12px;
79 | }
80 |
81 | pre::-webkit-scrollbar-thumb:horizontal {
82 | background: #bdd;
83 | border-radius: 0;
84 | height: 12px;
85 | }
86 |
87 | :target {
88 | border: 2px solid #f8f800;
89 | background: #f8f8f8;
90 | padding: 4px;
91 | }
92 |
93 | /* 'sh' stands for Syntax Highlight */
94 | span.sh1 {
95 | color: #f70;
96 | }
97 |
98 | span.tooltip {
99 | border-bottom: 1px dashed #444;
100 | }
101 |
102 | #searchBox {
103 | width: 100%;
104 | border: none;
105 | height: 20px;
106 | padding: 8px;
107 | font-size: 16px;
108 | border-radius: 2px;
109 | border: 2px solid #ddd;
110 | }
111 |
112 | #searchBox:placeholder-shown {
113 | font-style: italic;
114 | }
115 |
116 | button {
117 | border-radius: 2px;
118 | font-size: 16px;
119 | padding: 8px;
120 | color: #000;
121 | background-color: #f7f7f7;
122 | border: 2px solid #329add;
123 | transition-duration: 300ms;
124 | }
125 |
126 | button:hover {
127 | background-color: #329add;
128 | color: #f7f7f7;
129 | }
130 |
131 | /* https://www.w3schools.com/css/css_navbar.asp */
132 | ul.together {
133 | list-style-type: none;
134 | margin: 0;
135 | padding: 0;
136 | overflow: hidden;
137 | }
138 |
139 | ul.together li {
140 | float: left;
141 | }
142 |
143 | ul.together li a {
144 | display: block;
145 | border-radius: 8px;
146 | background: #e0e4e8;
147 | padding: 4px 8px;
148 | margin: 8px;
149 | }
150 |
151 | /* https://stackoverflow.com/a/30810322 */
152 | .invisible {
153 | left: 0;
154 | top: -99px;
155 | padding: 0;
156 | width: 2em;
157 | height: 2em;
158 | border: none;
159 | outline: none;
160 | position: fixed;
161 | box-shadow: none;
162 | color: transparent;
163 | background: transparent;
164 | }
165 |
166 | @media (max-width: 640px) {
167 | h1, summary.title {
168 | font-size: 18px;
169 | }
170 | h3 {
171 | font-size: 16px;
172 | }
173 |
174 | #dev_page_content_wrap {
175 | padding-top: 12px;
176 | }
177 |
178 | #dev_page_title {
179 | margin-top: 10px;
180 | margin-bottom: 20px;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/telethon/client/buttons.py:
--------------------------------------------------------------------------------
1 | import typing
2 |
3 | from .. import utils, hints
4 | from ..tl import types, custom
5 |
6 |
7 | class ButtonMethods:
8 | @staticmethod
9 | def build_reply_markup(
10 | buttons: 'typing.Optional[hints.MarkupLike]'
11 | ) -> 'typing.Optional[types.TypeReplyMarkup]':
12 | """
13 | Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
14 | the given buttons.
15 |
16 | Does nothing if either no buttons are provided or the provided
17 | argument is already a reply markup.
18 |
19 | You should consider using this method if you are going to reuse
20 | the markup very often. Otherwise, it is not necessary.
21 |
22 | This method is **not** asynchronous (don't use ``await`` on it).
23 |
24 | Arguments
25 | buttons (`hints.MarkupLike`):
26 | The button, list of buttons, array of buttons or markup
27 | to convert into a markup.
28 |
29 | Example
30 | .. code-block:: python
31 |
32 | from telethon import Button
33 |
34 | markup = client.build_reply_markup(Button.inline('hi'))
35 | # later
36 | await client.send_message(chat, 'click me', buttons=markup)
37 | """
38 | if buttons is None:
39 | return None
40 |
41 | try:
42 | if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'):
43 | return buttons
44 | except AttributeError:
45 | pass
46 |
47 | if not utils.is_list_like(buttons):
48 | buttons = [[buttons]]
49 | elif not buttons or not utils.is_list_like(buttons[0]):
50 | buttons = [buttons]
51 |
52 | is_inline = False
53 | is_normal = False
54 | resize = None
55 | single_use = None
56 | selective = None
57 | persistent = None
58 | placeholder = None
59 |
60 | rows = []
61 | for row in buttons:
62 | current = []
63 | for button in row:
64 | if isinstance(button, custom.Button):
65 | if button.resize is not None:
66 | resize = button.resize
67 | if button.single_use is not None:
68 | single_use = button.single_use
69 | if button.selective is not None:
70 | selective = button.selective
71 | if button.persistent is not None:
72 | persistent = button.persistent
73 | if button.placeholder is not None:
74 | placeholder = button.placeholder
75 |
76 | button = button.button
77 | elif isinstance(button, custom.MessageButton):
78 | button = button.button
79 |
80 | inline = custom.Button._is_inline(button)
81 | is_inline |= inline
82 | is_normal |= not inline
83 |
84 | if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton')
85 | current.append(button)
86 |
87 | if current:
88 | rows.append(types.KeyboardButtonRow(current))
89 |
90 | if is_inline and is_normal:
91 | raise ValueError('You cannot mix inline with normal buttons')
92 | elif is_inline:
93 | return types.ReplyInlineMarkup(rows)
94 | return types.ReplyKeyboardMarkup(
95 | rows=rows,
96 | resize=resize,
97 | single_use=single_use,
98 | selective=selective,
99 | persistent=persistent,
100 | placeholder=placeholder
101 | )
102 |
--------------------------------------------------------------------------------
/telethon_generator/data/html/css/docs.dark.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Nunito', sans-serif;
3 | color: #bbb;
4 | background-color:#000;
5 | font-size: 16px;
6 | }
7 |
8 | a {
9 | color: #42aaed;
10 | text-decoration: none;
11 | }
12 |
13 | pre {
14 | font-family: 'Source Code Pro', monospace;
15 | padding: 8px;
16 | color: #567;
17 | background: #080a0c;
18 | border-radius: 0;
19 | overflow-x: auto;
20 | }
21 |
22 | a:hover {
23 | color: #64bbdd;
24 | text-decoration: underline;
25 | }
26 |
27 | table {
28 | width: 100%;
29 | max-width: 100%;
30 | }
31 |
32 | table td {
33 | border-top: 1px solid #111;
34 | padding: 8px;
35 | }
36 |
37 | .horizontal {
38 | margin-bottom: 16px;
39 | list-style: none;
40 | background: #080a0c;
41 | border-radius: 4px;
42 | padding: 8px 16px;
43 | }
44 |
45 | .horizontal li {
46 | display: inline-block;
47 | margin: 0 8px 0 0;
48 | }
49 |
50 | .horizontal img {
51 | display: inline-block;
52 | margin: 0 8px -2px 0;
53 | }
54 |
55 | h1, summary.title {
56 | font-size: 24px;
57 | }
58 |
59 | h3 {
60 | font-size: 20px;
61 | }
62 |
63 | #main_div {
64 | padding: 20px 0;
65 | max-width: 800px;
66 | margin: 0 auto;
67 | }
68 |
69 | pre::-webkit-scrollbar {
70 | visibility: visible;
71 | display: block;
72 | height: 12px;
73 | }
74 |
75 | pre::-webkit-scrollbar-track:horizontal {
76 | background: #222;
77 | border-radius: 0;
78 | height: 12px;
79 | }
80 |
81 | pre::-webkit-scrollbar-thumb:horizontal {
82 | background: #444;
83 | border-radius: 0;
84 | height: 12px;
85 | }
86 |
87 | :target {
88 | border: 2px solid #149;
89 | background: #246;
90 | padding: 4px;
91 | }
92 |
93 | /* 'sh' stands for Syntax Highlight */
94 | span.sh1 {
95 | color: #f93;
96 | }
97 |
98 | span.tooltip {
99 | border-bottom: 1px dashed #ddd;
100 | }
101 |
102 | #searchBox {
103 | width: 100%;
104 | border: none;
105 | height: 20px;
106 | padding: 8px;
107 | font-size: 16px;
108 | border-radius: 2px;
109 | border: 2px solid #222;
110 | background: #000;
111 | color: #eee;
112 | }
113 |
114 | #searchBox:placeholder-shown {
115 | color: #bbb;
116 | font-style: italic;
117 | }
118 |
119 | button {
120 | border-radius: 2px;
121 | font-size: 16px;
122 | padding: 8px;
123 | color: #bbb;
124 | background-color: #111;
125 | border: 2px solid #146;
126 | transition-duration: 300ms;
127 | }
128 |
129 | button:hover {
130 | background-color: #146;
131 | color: #fff;
132 | }
133 |
134 | /* https://www.w3schools.com/css/css_navbar.asp */
135 | ul.together {
136 | list-style-type: none;
137 | margin: 0;
138 | padding: 0;
139 | overflow: hidden;
140 | }
141 |
142 | ul.together li {
143 | float: left;
144 | }
145 |
146 | ul.together li a {
147 | display: block;
148 | border-radius: 8px;
149 | background: #111;
150 | padding: 4px 8px;
151 | margin: 8px;
152 | }
153 |
154 | /* https://stackoverflow.com/a/30810322 */
155 | .invisible {
156 | left: 0;
157 | top: -99px;
158 | padding: 0;
159 | width: 2em;
160 | height: 2em;
161 | border: none;
162 | outline: none;
163 | position: fixed;
164 | box-shadow: none;
165 | color: transparent;
166 | background: transparent;
167 | }
168 |
169 | @media (max-width: 640px) {
170 | h1, summary.title {
171 | font-size: 18px;
172 | }
173 | h3 {
174 | font-size: 16px;
175 | }
176 |
177 | #dev_page_content_wrap {
178 | padding-top: 12px;
179 | }
180 |
181 | #dev_page_title {
182 | margin-top: 10px;
183 | margin-bottom: 20px;
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/telethon/errors/rpcbaseerrors.py:
--------------------------------------------------------------------------------
1 | from ..tl import functions
2 |
3 | _NESTS_QUERY = (
4 | functions.InvokeAfterMsgRequest,
5 | functions.InvokeAfterMsgsRequest,
6 | functions.InitConnectionRequest,
7 | functions.InvokeWithLayerRequest,
8 | functions.InvokeWithoutUpdatesRequest,
9 | functions.InvokeWithMessagesRangeRequest,
10 | functions.InvokeWithTakeoutRequest,
11 | )
12 |
13 | class RPCError(Exception):
14 | """Base class for all Remote Procedure Call errors."""
15 | code = None
16 | message = None
17 |
18 | def __init__(self, request, message, code=None):
19 | super().__init__('RPCError {}: {}{}'.format(
20 | code or self.code, message, self._fmt_request(request)))
21 |
22 | self.request = request
23 | self.code = code
24 | self.message = message
25 |
26 | @staticmethod
27 | def _fmt_request(request):
28 | n = 0
29 | reason = ''
30 | while isinstance(request, _NESTS_QUERY):
31 | n += 1
32 | reason += request.__class__.__name__ + '('
33 | request = request.query
34 | reason += request.__class__.__name__ + ')' * n
35 |
36 | return ' (caused by {})'.format(reason)
37 |
38 | def __reduce__(self):
39 | return type(self), (self.request, self.message, self.code)
40 |
41 |
42 | class InvalidDCError(RPCError):
43 | """
44 | The request must be repeated, but directed to a different data center.
45 | """
46 | code = 303
47 | message = 'ERROR_SEE_OTHER'
48 |
49 |
50 | class BadRequestError(RPCError):
51 | """
52 | The query contains errors. In the event that a request was created
53 | using a form and contains user generated data, the user should be
54 | notified that the data must be corrected before the query is repeated.
55 | """
56 | code = 400
57 | message = 'BAD_REQUEST'
58 |
59 |
60 | class UnauthorizedError(RPCError):
61 | """
62 | There was an unauthorized attempt to use functionality available only
63 | to authorized users.
64 | """
65 | code = 401
66 | message = 'UNAUTHORIZED'
67 |
68 |
69 | class ForbiddenError(RPCError):
70 | """
71 | Privacy violation. For example, an attempt to write a message to
72 | someone who has blacklisted the current user.
73 | """
74 | code = 403
75 | message = 'FORBIDDEN'
76 |
77 |
78 | class NotFoundError(RPCError):
79 | """
80 | An attempt to invoke a non-existent object, such as a method.
81 | """
82 | code = 404
83 | message = 'NOT_FOUND'
84 |
85 |
86 | class AuthKeyError(RPCError):
87 | """
88 | Errors related to invalid authorization key, like
89 | AUTH_KEY_DUPLICATED which can cause the connection to fail.
90 | """
91 | code = 406
92 | message = 'AUTH_KEY'
93 |
94 |
95 | class FloodError(RPCError):
96 | """
97 | The maximum allowed number of attempts to invoke the given method
98 | with the given input parameters has been exceeded. For example, in an
99 | attempt to request a large number of text messages (SMS) for the same
100 | phone number.
101 | """
102 | code = 420
103 | message = 'FLOOD'
104 |
105 |
106 | class ServerError(RPCError):
107 | """
108 | An internal server error occurred while a request was being processed
109 | for example, there was a disruption while accessing a database or file
110 | storage.
111 | """
112 | code = 500 # Also witnessed as -500
113 | message = 'INTERNAL'
114 |
115 |
116 | class TimedOutError(RPCError):
117 | """
118 | Clicking the inline buttons of bots that never (or take to long to)
119 | call ``answerCallbackQuery`` will result in this "special" RPCError.
120 | """
121 | code = 503 # Only witnessed as -503
122 | message = 'Timeout'
123 |
124 |
125 | BotTimeout = TimedOutError
126 |
127 |
128 | base_errors = {x.code: x for x in (
129 | InvalidDCError, BadRequestError, UnauthorizedError, ForbiddenError,
130 | NotFoundError, AuthKeyError, FloodError, ServerError, TimedOutError
131 | )}
132 |
--------------------------------------------------------------------------------
/readthedocs/developing/testing.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Tests
3 | =====
4 |
5 | Telethon uses `Pytest `__, for testing, `Tox
6 | `__ for environment setup, and
7 | `pytest-asyncio `__ and `pytest-cov
8 | `__ for asyncio and
9 | `coverage `__ integration.
10 |
11 | While reading the full documentation for these is probably a good idea, there
12 | is a lot to read, so a brief summary of these tools is provided below for
13 | convienience.
14 |
15 | Brief Introduction to Pytest
16 | ============================
17 |
18 | `Pytest `__ is a tool for discovering and running python
19 | tests, as well as allowing modular reuse of test setup code using fixtures.
20 |
21 | Most Pytest tests will look something like this::
22 |
23 | from module import my_thing, my_other_thing
24 |
25 | def test_my_thing(fixture):
26 | assert my_thing(fixture) == 42
27 |
28 | @pytest.mark.asyncio
29 | async def test_my_thing(event_loop):
30 | assert await my_other_thing(loop=event_loop) == 42
31 |
32 | Note here:
33 |
34 | 1. The test imports one specific function. The role of unit tests is to test
35 | that the implementation of some unit, like a function or class, works.
36 | It's role is not so much to test that components interact well with each
37 | other. I/O, such as connecting to remote servers, should be avoided. This
38 | helps with quickly identifying the source of an error, finding silent
39 | breakage, and makes it easier to cover all possible code paths.
40 |
41 | System or integration tests can also be useful, but are currently out of
42 | scope of Telethon's automated testing.
43 |
44 | 2. A function ``test_my_thing`` is declared. Pytest searches for files
45 | starting with ``test_``, classes starting with ``Test`` and executes any
46 | functions or methods starting with ``test_`` it finds.
47 |
48 | 3. The function is declared with a parameter ``fixture``. Fixtures are used to
49 | request things required to run the test, such as temporary directories,
50 | free TCP ports, Connections, etc. Fixtures are declared by simply adding
51 | the fixture name as parameter. A full list of available fixtures can be
52 | found with the ``pytest --fixtures`` command.
53 |
54 | 4. The test uses a simple ``assert`` to test some condition is valid. Pytest
55 | uses some magic to ensure that the errors from this are readable and easy
56 | to debug.
57 |
58 | 5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It
59 | starts a loop and executes a test function as coroutine. This should be
60 | used for testing asyncio code. It also declares the ``event_loop``
61 | fixture, which will request an ``asyncio`` event loop.
62 |
63 | Brief Introduction to Tox
64 | =========================
65 |
66 | `Tox `__ is a tool for automated setup
67 | of virtual environments for testing. While the tests can be run directly by
68 | just running ``pytest``, this only tests one specific python version in your
69 | existing environment, which will not catch e.g. undeclared dependencies, or
70 | version incompatabilities.
71 |
72 | Tox environments are declared in the ``tox.ini`` file. The default
73 | environments, declared at the top, can be simply run with ``tox``. The option
74 | ``tox -e py36,flake`` can be used to request specific environments to be run.
75 |
76 | Brief Introduction to Pytest-cov
77 | ================================
78 |
79 | Coverage is a useful metric for testing. It measures the lines of code and
80 | branches that are exercised by the tests. The higher the coverage, the more
81 | likely it is that any coding errors will be caught by the tests.
82 |
83 | A brief coverage report can be generated with the ``--cov`` option to ``tox``,
84 | which will be passed on to ``pytest``. Additionally, the very useful HTML
85 | report can be generated with ``--cov --cov-report=html``, which contains a
86 | browsable copy of the source code, annotated with coverage information for each
87 | line.
88 |
--------------------------------------------------------------------------------
/readthedocs/quick-references/client-reference.rst:
--------------------------------------------------------------------------------
1 | .. _client-ref:
2 |
3 | ================
4 | Client Reference
5 | ================
6 |
7 | This page contains a summary of all the important methods and properties that
8 | you may need when using Telethon. They are sorted by relevance and are not in
9 | alphabetical order.
10 |
11 | You should use this page to learn about which methods are available, and
12 | if you need a usage example or further description of the arguments, be
13 | sure to follow the links.
14 |
15 | .. contents::
16 |
17 | TelegramClient
18 | ==============
19 |
20 | This is a summary of the methods and
21 | properties you will find at :ref:`telethon-client`.
22 |
23 | Auth
24 | ----
25 |
26 | .. currentmodule:: telethon.client.auth.AuthMethods
27 |
28 | .. autosummary::
29 | :nosignatures:
30 |
31 | start
32 | send_code_request
33 | sign_in
34 | qr_login
35 | log_out
36 | edit_2fa
37 |
38 | Base
39 | ----
40 |
41 | .. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient
42 |
43 | .. autosummary::
44 | :nosignatures:
45 |
46 | connect
47 | disconnect
48 | is_connected
49 | disconnected
50 | loop
51 | set_proxy
52 |
53 | Messages
54 | --------
55 |
56 | .. py:currentmodule:: telethon.client.messages.MessageMethods
57 |
58 | .. autosummary::
59 | :nosignatures:
60 |
61 | send_message
62 | edit_message
63 | delete_messages
64 | forward_messages
65 | iter_messages
66 | get_messages
67 | pin_message
68 | unpin_message
69 | send_read_acknowledge
70 |
71 | Uploads
72 | -------
73 |
74 | .. py:currentmodule:: telethon.client.uploads.UploadMethods
75 |
76 | .. autosummary::
77 | :nosignatures:
78 |
79 | send_file
80 | upload_file
81 |
82 | Downloads
83 | ---------
84 |
85 | .. currentmodule:: telethon.client.downloads.DownloadMethods
86 |
87 | .. autosummary::
88 | :nosignatures:
89 |
90 | download_media
91 | download_profile_photo
92 | download_file
93 | iter_download
94 |
95 | Dialogs
96 | -------
97 |
98 | .. py:currentmodule:: telethon.client.dialogs.DialogMethods
99 |
100 | .. autosummary::
101 | :nosignatures:
102 |
103 | iter_dialogs
104 | get_dialogs
105 | edit_folder
106 | iter_drafts
107 | get_drafts
108 | delete_dialog
109 | conversation
110 |
111 | Users
112 | -----
113 |
114 | .. py:currentmodule:: telethon.client.users.UserMethods
115 |
116 | .. autosummary::
117 | :nosignatures:
118 |
119 | get_me
120 | is_bot
121 | is_user_authorized
122 | get_entity
123 | get_input_entity
124 | get_peer_id
125 |
126 | Chats
127 | -----
128 |
129 | .. currentmodule:: telethon.client.chats.ChatMethods
130 |
131 | .. autosummary::
132 | :nosignatures:
133 |
134 | iter_participants
135 | get_participants
136 | kick_participant
137 | iter_admin_log
138 | get_admin_log
139 | iter_profile_photos
140 | get_profile_photos
141 | edit_admin
142 | edit_permissions
143 | get_permissions
144 | get_stats
145 | action
146 |
147 | Parse Mode
148 | ----------
149 |
150 | .. py:currentmodule:: telethon.client.messageparse.MessageParseMethods
151 |
152 | .. autosummary::
153 | :nosignatures:
154 |
155 | parse_mode
156 |
157 | Updates
158 | -------
159 |
160 | .. py:currentmodule:: telethon.client.updates.UpdateMethods
161 |
162 | .. autosummary::
163 | :nosignatures:
164 |
165 | on
166 | run_until_disconnected
167 | add_event_handler
168 | remove_event_handler
169 | list_event_handlers
170 | catch_up
171 | set_receive_updates
172 |
173 | Bots
174 | ----
175 |
176 | .. currentmodule:: telethon.client.bots.BotMethods
177 |
178 | .. autosummary::
179 | :nosignatures:
180 |
181 | inline_query
182 |
183 | Buttons
184 | -------
185 |
186 | .. currentmodule:: telethon.client.buttons.ButtonMethods
187 |
188 | .. autosummary::
189 | :nosignatures:
190 |
191 | build_reply_markup
192 |
193 | Account
194 | -------
195 |
196 | .. currentmodule:: telethon.client.account.AccountMethods
197 |
198 | .. autosummary::
199 | :nosignatures:
200 |
201 | takeout
202 | end_takeout
203 |
--------------------------------------------------------------------------------
/telethon/tl/custom/sendergetter.py:
--------------------------------------------------------------------------------
1 | import abc
2 |
3 | from ... import utils
4 |
5 |
6 | class SenderGetter(abc.ABC):
7 | """
8 | Helper base class that introduces the `sender`, `input_sender`
9 | and `sender_id` properties and `get_sender` and `get_input_sender`
10 | methods.
11 | """
12 | def __init__(self, sender_id=None, *, sender=None, input_sender=None):
13 | self._sender_id = sender_id
14 | self._sender = sender
15 | self._input_sender = input_sender
16 | self._client = None
17 |
18 | @property
19 | def sender(self):
20 | """
21 | Returns the :tl:`User` or :tl:`Channel` that sent this object.
22 | It may be `None` if Telegram didn't send the sender.
23 |
24 | If you only need the ID, use `sender_id` instead.
25 |
26 | If you need to call a method which needs
27 | this chat, use `input_sender` instead.
28 |
29 | If you're using `telethon.events`, use `get_sender()` instead.
30 | """
31 | return self._sender
32 |
33 | async def get_sender(self):
34 | """
35 | Returns `sender`, but will make an API call to find the
36 | sender unless it's already cached.
37 |
38 | If you only need the ID, use `sender_id` instead.
39 |
40 | If you need to call a method which needs
41 | this sender, use `get_input_sender()` instead.
42 | """
43 | # ``sender.min`` is present both in :tl:`User` and :tl:`Channel`.
44 | # It's a flag that will be set if only minimal information is
45 | # available (such as display name, but username may be missing),
46 | # in which case we want to force fetch the entire thing because
47 | # the user explicitly called a method. If the user is okay with
48 | # cached information, they may use the property instead.
49 | if (self._sender is None or getattr(self._sender, 'min', None)) \
50 | and await self.get_input_sender():
51 | # self.get_input_sender may refresh in which case the sender may no longer be min
52 | # However it could still incur a cost so the cheap check is done twice instead.
53 | if self._sender is None or getattr(self._sender, 'min', None):
54 | try:
55 | self._sender =\
56 | await self._client.get_entity(self._input_sender)
57 | except ValueError:
58 | await self._refetch_sender()
59 | return self._sender
60 |
61 | @property
62 | def input_sender(self):
63 | """
64 | This :tl:`InputPeer` is the input version of the user/channel who
65 | sent the message. Similarly to `input_chat
66 | `, this doesn't
67 | have things like username or similar, but still useful in some cases.
68 |
69 | Note that this might not be available if the library can't
70 | find the input chat, or if the message a broadcast on a channel.
71 | """
72 | if self._input_sender is None and self._sender_id and self._client:
73 | try:
74 | self._input_sender = self._client._mb_entity_cache.get(
75 | utils.resolve_id(self._sender_id)[0])._as_input_peer()
76 | except AttributeError:
77 | pass
78 | return self._input_sender
79 |
80 | async def get_input_sender(self):
81 | """
82 | Returns `input_sender`, but will make an API call to find the
83 | input sender unless it's already cached.
84 | """
85 | if self.input_sender is None and self._sender_id and self._client:
86 | await self._refetch_sender()
87 | return self._input_sender
88 |
89 | @property
90 | def sender_id(self):
91 | """
92 | Returns the marked sender integer ID, if present.
93 |
94 | If there is a sender in the object, `sender_id` will *always* be set,
95 | which is why you should use it instead of `sender.id `.
96 | """
97 | return self._sender_id
98 |
99 | async def _refetch_sender(self):
100 | """
101 | Re-fetches sender information through other means.
102 | """
103 |
--------------------------------------------------------------------------------
/telethon/crypto/cdndecrypter.py:
--------------------------------------------------------------------------------
1 | """
2 | This module holds the CdnDecrypter utility class.
3 | """
4 | from hashlib import sha256
5 |
6 | from ..tl.functions.upload import GetCdnFileRequest, ReuploadCdnFileRequest
7 | from ..tl.types.upload import CdnFileReuploadNeeded, CdnFile
8 | from ..crypto import AESModeCTR
9 | from ..errors import CdnFileTamperedError
10 |
11 |
12 | class CdnDecrypter:
13 | """
14 | Used when downloading a file results in a 'FileCdnRedirect' to
15 | both prepare the redirect, decrypt the file as it downloads, and
16 | ensure the file hasn't been tampered. https://core.telegram.org/cdn
17 | """
18 | def __init__(self, cdn_client, file_token, cdn_aes, cdn_file_hashes):
19 | """
20 | Initializes the CDN decrypter.
21 |
22 | :param cdn_client: a client connected to a CDN.
23 | :param file_token: the token of the file to be used.
24 | :param cdn_aes: the AES CTR used to decrypt the file.
25 | :param cdn_file_hashes: the hashes the decrypted file must match.
26 | """
27 | self.client = cdn_client
28 | self.file_token = file_token
29 | self.cdn_aes = cdn_aes
30 | self.cdn_file_hashes = cdn_file_hashes
31 |
32 | @staticmethod
33 | async def prepare_decrypter(client, cdn_client, cdn_redirect):
34 | """
35 | Prepares a new CDN decrypter.
36 |
37 | :param client: a TelegramClient connected to the main servers.
38 | :param cdn_client: a new client connected to the CDN.
39 | :param cdn_redirect: the redirect file object that caused this call.
40 | :return: (CdnDecrypter, first chunk file data)
41 | """
42 | cdn_aes = AESModeCTR(
43 | key=cdn_redirect.encryption_key,
44 | # 12 first bytes of the IV..4 bytes of the offset (0, big endian)
45 | iv=cdn_redirect.encryption_iv[:12] + bytes(4)
46 | )
47 |
48 | # We assume that cdn_redirect.cdn_file_hashes are ordered by offset,
49 | # and that there will be enough of these to retrieve the whole file.
50 | decrypter = CdnDecrypter(
51 | cdn_client, cdn_redirect.file_token,
52 | cdn_aes, cdn_redirect.cdn_file_hashes
53 | )
54 |
55 | cdn_file = await cdn_client(GetCdnFileRequest(
56 | file_token=cdn_redirect.file_token,
57 | offset=cdn_redirect.cdn_file_hashes[0].offset,
58 | limit=cdn_redirect.cdn_file_hashes[0].limit
59 | ))
60 | if isinstance(cdn_file, CdnFileReuploadNeeded):
61 | # We need to use the original client here
62 | await client(ReuploadCdnFileRequest(
63 | file_token=cdn_redirect.file_token,
64 | request_token=cdn_file.request_token
65 | ))
66 |
67 | # We want to always return a valid upload.CdnFile
68 | cdn_file = decrypter.get_file()
69 | else:
70 | cdn_file.bytes = decrypter.cdn_aes.encrypt(cdn_file.bytes)
71 | cdn_hash = decrypter.cdn_file_hashes.pop(0)
72 | decrypter.check(cdn_file.bytes, cdn_hash)
73 |
74 | return decrypter, cdn_file
75 |
76 | def get_file(self):
77 | """
78 | Calls GetCdnFileRequest and decrypts its bytes.
79 | Also ensures that the file hasn't been tampered.
80 |
81 | :return: the CdnFile result.
82 | """
83 | if self.cdn_file_hashes:
84 | cdn_hash = self.cdn_file_hashes.pop(0)
85 | cdn_file = self.client(GetCdnFileRequest(
86 | self.file_token, cdn_hash.offset, cdn_hash.limit
87 | ))
88 | cdn_file.bytes = self.cdn_aes.encrypt(cdn_file.bytes)
89 | self.check(cdn_file.bytes, cdn_hash)
90 | else:
91 | cdn_file = CdnFile(bytes(0))
92 |
93 | return cdn_file
94 |
95 | @staticmethod
96 | def check(data, cdn_hash):
97 | """
98 | Checks the integrity of the given data.
99 | Raises CdnFileTamperedError if the integrity check fails.
100 |
101 | :param data: the data to be hashed.
102 | :param cdn_hash: the expected hash.
103 | """
104 | if sha256(data).digest() != cdn_hash.hash:
105 | raise CdnFileTamperedError()
106 |
--------------------------------------------------------------------------------
/readthedocs/basic/quick-start.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Quick-Start
3 | ===========
4 |
5 | Let's see a longer example to learn some of the methods that the library
6 | has to offer. These are known as "friendly methods", and you should always
7 | use these if possible.
8 |
9 | .. code-block:: python
10 |
11 | from telethon import TelegramClient
12 |
13 | # Remember to use your own values from my.telegram.org!
14 | api_id = 12345
15 | api_hash = '0123456789abcdef0123456789abcdef'
16 | client = TelegramClient('anon', api_id, api_hash)
17 |
18 | async def main():
19 | # Getting information about yourself
20 | me = await client.get_me()
21 |
22 | # "me" is a user object. You can pretty-print
23 | # any Telegram object with the "stringify" method:
24 | print(me.stringify())
25 |
26 | # When you print something, you see a representation of it.
27 | # You can access all attributes of Telegram objects with
28 | # the dot operator. For example, to get the username:
29 | username = me.username
30 | print(username)
31 | print(me.phone)
32 |
33 | # You can print all the dialogs/conversations that you are part of:
34 | async for dialog in client.iter_dialogs():
35 | print(dialog.name, 'has ID', dialog.id)
36 |
37 | # You can send messages to yourself...
38 | await client.send_message('me', 'Hello, myself!')
39 | # ...to some chat ID
40 | await client.send_message(-100123456, 'Hello, group!')
41 | # ...to your contacts
42 | await client.send_message('+34600123123', 'Hello, friend!')
43 | # ...or even to any username
44 | await client.send_message('username', 'Testing Telethon!')
45 |
46 | # You can, of course, use markdown in your messages:
47 | message = await client.send_message(
48 | 'me',
49 | 'This message has **bold**, `code`, __italics__ and '
50 | 'a [nice website](https://example.com)!',
51 | link_preview=False
52 | )
53 |
54 | # Sending a message returns the sent message object, which you can use
55 | print(message.raw_text)
56 |
57 | # You can reply to messages directly if you have a message object
58 | await message.reply('Cool!')
59 |
60 | # Or send files, songs, documents, albums...
61 | await client.send_file('me', '/home/me/Pictures/holidays.jpg')
62 |
63 | # You can print the message history of any chat:
64 | async for message in client.iter_messages('me'):
65 | print(message.id, message.text)
66 |
67 | # You can download media from messages, too!
68 | # The method will return the path where the file was saved.
69 | if message.photo:
70 | path = await message.download_media()
71 | print('File saved to', path) # printed after download is done
72 |
73 | with client:
74 | client.loop.run_until_complete(main())
75 |
76 |
77 | Here, we show how to sign in, get information about yourself, send
78 | messages, files, getting chats, printing messages, and downloading
79 | files.
80 |
81 | You should make sure that you understand what the code shown here
82 | does, take note on how methods are called and used and so on before
83 | proceeding. We will see all the available methods later on.
84 |
85 | .. important::
86 |
87 | Note that Telethon is an asynchronous library, and as such, you should
88 | get used to it and learn a bit of basic `asyncio`. This will help a lot.
89 | As a quick start, this means you generally want to write all your code
90 | inside some ``async def`` like so:
91 |
92 | .. code-block:: python
93 |
94 | client = ...
95 |
96 | async def do_something(me):
97 | ...
98 |
99 | async def main():
100 | # Most of your code should go here.
101 | # You can of course make and use your own async def (do_something).
102 | # They only need to be async if they need to await things.
103 | me = await client.get_me()
104 | await do_something(me)
105 |
106 | with client:
107 | client.loop.run_until_complete(main())
108 |
109 | After you understand this, you may use the ``telethon.sync`` hack if you
110 | want do so (see :ref:`compatibility-and-convenience`), but note you may
111 | run into other issues (iPython, Anaconda, etc. have some issues with it).
112 |
--------------------------------------------------------------------------------
/telethon/extensions/messagepacker.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import collections
3 | import io
4 | import struct
5 |
6 | from ..tl import TLRequest
7 | from ..tl.core.messagecontainer import MessageContainer
8 | from ..tl.core.tlmessage import TLMessage
9 |
10 |
11 | class MessagePacker:
12 | """
13 | This class packs `RequestState` as outgoing `TLMessages`.
14 |
15 | The purpose of this class is to support putting N `RequestState` into a
16 | queue, and then awaiting for "packed" `TLMessage` in the other end. The
17 | simplest case would be ``State -> TLMessage`` (1-to-1 relationship) but
18 | for efficiency purposes it's ``States -> Container`` (N-to-1).
19 |
20 | This addresses several needs: outgoing messages will be smaller, so the
21 | encryption and network overhead also is smaller. It's also a central
22 | point where outgoing requests are put, and where ready-messages are get.
23 | """
24 |
25 | def __init__(self, state, loggers):
26 | self._state = state
27 | self._deque = collections.deque()
28 | self._ready = asyncio.Event()
29 | self._log = loggers[__name__]
30 |
31 | def append(self, state):
32 | self._deque.append(state)
33 | self._ready.set()
34 |
35 | def extend(self, states):
36 | self._deque.extend(states)
37 | self._ready.set()
38 |
39 | async def get(self):
40 | """
41 | Returns (batch, data) if one or more items could be retrieved.
42 |
43 | If the cancellation occurs or only invalid items were in the
44 | queue, (None, None) will be returned instead.
45 | """
46 | if not self._deque:
47 | self._ready.clear()
48 | await self._ready.wait()
49 |
50 | buffer = io.BytesIO()
51 | batch = []
52 | size = 0
53 |
54 | # Fill a new batch to return while the size is small enough,
55 | # as long as we don't exceed the maximum length of messages.
56 | while self._deque and len(batch) <= MessageContainer.MAXIMUM_LENGTH:
57 | state = self._deque.popleft()
58 | size += len(state.data) + TLMessage.SIZE_OVERHEAD
59 |
60 | if size <= MessageContainer.MAXIMUM_SIZE:
61 | state.msg_id = self._state.write_data_as_message(
62 | buffer, state.data, isinstance(state.request, TLRequest),
63 | after_id=state.after.msg_id if state.after else None
64 | )
65 | batch.append(state)
66 | self._log.debug('Assigned msg_id = %d to %s (%x)',
67 | state.msg_id, state.request.__class__.__name__,
68 | id(state.request))
69 | continue
70 |
71 | if batch:
72 | # Put the item back since it can't be sent in this batch
73 | self._deque.appendleft(state)
74 | break
75 |
76 | # If a single message exceeds the maximum size, then the
77 | # message payload cannot be sent. Telegram would forcibly
78 | # close the connection; message would never be confirmed.
79 | #
80 | # We don't put the item back because it can never be sent.
81 | # If we did, we would loop again and reach this same path.
82 | # Setting the exception twice results in `InvalidStateError`
83 | # and this method should never return with error, which we
84 | # really want to avoid.
85 | self._log.warning(
86 | 'Message payload for %s is too long (%d) and cannot be sent',
87 | state.request.__class__.__name__, len(state.data)
88 | )
89 | state.future.set_exception(
90 | ValueError('Request payload is too big'))
91 |
92 | size = 0
93 | continue
94 |
95 | if not batch:
96 | return None, None
97 |
98 | if len(batch) > 1:
99 | # Inlined code to pack several messages into a container
100 | data = struct.pack(
101 | '` of the channel you want to join
25 | to, you can make use of the :tl:`JoinChannelRequest` to join such channel:
26 |
27 | .. code-block:: python
28 |
29 | from telethon.tl.functions.channels import JoinChannelRequest
30 | await client(JoinChannelRequest(channel))
31 |
32 | # In the same way, you can also leave such channel
33 | from telethon.tl.functions.channels import LeaveChannelRequest
34 | await client(LeaveChannelRequest(input_channel))
35 |
36 |
37 | For more on channels, check the `channels namespace`__.
38 |
39 |
40 | __ https://tl.telethon.dev/methods/channels/index.html
41 |
42 |
43 | Joining a private chat or channel
44 | =================================
45 |
46 | If all you have is a link like this one:
47 | ``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
48 | enough information to join! The part after the
49 | ``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
50 | example, is the ``hash`` of the chat or channel. Now you can use
51 | :tl:`ImportChatInviteRequest` as follows:
52 |
53 | .. code-block:: python
54 |
55 | from telethon.tl.functions.messages import ImportChatInviteRequest
56 | updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
57 |
58 |
59 | Adding someone else to such chat or channel
60 | ===========================================
61 |
62 | If you don't want to add yourself, maybe because you're already in,
63 | you can always add someone else with the :tl:`AddChatUserRequest`, which
64 | use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
65 |
66 | .. code-block:: python
67 |
68 | # For normal chats
69 | from telethon.tl.functions.messages import AddChatUserRequest
70 |
71 | # Note that ``user_to_add`` is NOT the name of the parameter.
72 | # It's the user you want to add (``user_id=user_to_add``).
73 | await client(AddChatUserRequest(
74 | chat_id,
75 | user_to_add,
76 | fwd_limit=10 # Allow the user to see the 10 last messages
77 | ))
78 |
79 | # For channels (which includes megagroups)
80 | from telethon.tl.functions.channels import InviteToChannelRequest
81 |
82 | await client(InviteToChannelRequest(
83 | channel,
84 | [users_to_add]
85 | ))
86 |
87 | Note that this method will only really work for friends or bot accounts.
88 | Trying to mass-add users with this approach will not work, and can put both
89 | your account and group to risk, possibly being flagged as spam and limited.
90 |
91 |
92 | Checking a link without joining
93 | ===============================
94 |
95 | If you don't need to join but rather check whether it's a group or a
96 | channel, you can use the :tl:`CheckChatInviteRequest`, which takes in
97 | the hash of said channel or group.
98 |
99 |
100 | Increasing View Count in a Channel
101 | ==================================
102 |
103 | It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
104 | while I don't understand why so many people ask this, the solution is to
105 | use :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
106 |
107 | .. code-block:: python
108 |
109 |
110 | # Obtain `channel' through dialogs or through client.get_entity() or anyhow.
111 | # Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list.
112 |
113 | await client(GetMessagesViewsRequest(
114 | peer=channel,
115 | id=msg_ids,
116 | increment=True
117 | ))
118 |
119 |
120 | Note that you can only do this **once or twice a day** per account,
121 | running this in a loop will obviously not increase the views forever
122 | unless you wait a day between each iteration. If you run it any sooner
123 | than that, the views simply won't be increased.
124 |
125 | __ https://github.com/LonamiWebs/Telethon/issues/233
126 | __ https://github.com/LonamiWebs/Telethon/issues/305
127 | __ https://github.com/LonamiWebs/Telethon/issues/409
128 | __ https://github.com/LonamiWebs/Telethon/issues/447
129 |
--------------------------------------------------------------------------------
/telethon/tl/custom/qrlogin.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import base64
3 | import datetime
4 |
5 | from .. import types, functions
6 | from ... import events
7 |
8 |
9 | class QRLogin:
10 | """
11 | QR login information.
12 |
13 | Most of the time, you will present the `url` as a QR code to the user,
14 | and while it's being shown, call `wait`.
15 | """
16 | def __init__(self, client, ignored_ids):
17 | self._client = client
18 | self._request = functions.auth.ExportLoginTokenRequest(
19 | self._client.api_id, self._client.api_hash, ignored_ids)
20 | self._resp = None
21 |
22 | async def recreate(self):
23 | """
24 | Generates a new token and URL for a new QR code, useful if the code
25 | has expired before it was imported.
26 | """
27 | self._resp = await self._client(self._request)
28 |
29 | @property
30 | def token(self) -> bytes:
31 | """
32 | The binary data representing the token.
33 |
34 | It can be used by a previously-authorized client in a call to
35 | :tl:`auth.importLoginToken` to log the client that originally
36 | requested the QR login.
37 | """
38 | return self._resp.token
39 |
40 | @property
41 | def url(self) -> str:
42 | """
43 | The ``tg://login`` URI with the token. When opened by a Telegram
44 | application where the user is logged in, it will import the login
45 | token.
46 |
47 | If you want to display a QR code to the user, this is the URL that
48 | should be launched when the QR code is scanned (the URL that should
49 | be contained in the QR code image you generate).
50 |
51 | Whether you generate the QR code image or not is up to you, and the
52 | library can't do this for you due to the vast ways of generating and
53 | displaying the QR code that exist.
54 |
55 | The URL simply consists of `token` base64-encoded.
56 | """
57 | return 'tg://login?token={}'.format(base64.urlsafe_b64encode(self._resp.token).decode('utf-8').rstrip('='))
58 |
59 | @property
60 | def expires(self) -> datetime.datetime:
61 | """
62 | The `datetime` at which the QR code will expire.
63 |
64 | If you want to try again, you will need to call `recreate`.
65 | """
66 | return self._resp.expires
67 |
68 | async def wait(self, timeout: float = None):
69 | """
70 | Waits for the token to be imported by a previously-authorized client,
71 | either by scanning the QR, launching the URL directly, or calling the
72 | import method.
73 |
74 | This method **must** be called before the QR code is scanned, and
75 | must be executing while the QR code is being scanned. Otherwise, the
76 | login will not complete.
77 |
78 | Will raise `asyncio.TimeoutError` if the login doesn't complete on
79 | time.
80 |
81 | Arguments
82 | timeout (float):
83 | The timeout, in seconds, to wait before giving up. By default
84 | the library will wait until the token expires, which is often
85 | what you want.
86 |
87 | Returns
88 | On success, an instance of :tl:`User`. On failure it will raise.
89 | """
90 | if timeout is None:
91 | timeout = (self._resp.expires - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()
92 |
93 | event = asyncio.Event()
94 |
95 | async def handler(_update):
96 | event.set()
97 |
98 | self._client.add_event_handler(handler, events.Raw(types.UpdateLoginToken))
99 |
100 | try:
101 | # Will raise timeout error if it doesn't complete quick enough,
102 | # which we want to let propagate
103 | await asyncio.wait_for(event.wait(), timeout=timeout)
104 | finally:
105 | self._client.remove_event_handler(handler)
106 |
107 | # We got here without it raising timeout error, so we can proceed
108 | resp = await self._client(self._request)
109 | if isinstance(resp, types.auth.LoginTokenMigrateTo):
110 | await self._client._switch_dc(resp.dc_id)
111 | resp = await self._client(functions.auth.ImportLoginTokenRequest(resp.token))
112 | # resp should now be auth.loginTokenSuccess
113 |
114 | if isinstance(resp, types.auth.LoginTokenSuccess):
115 | user = resp.authorization.user
116 | await self._client._on_login(user)
117 | return user
118 |
119 | raise TypeError('Login token response was unexpected: {}'.format(resp))
120 |
--------------------------------------------------------------------------------