Compare commits

...

386 Commits
v1.24.0 ... v1

Author SHA1 Message Date
Lonami Exo
e77307d0ed Update to layer 211 2025-08-01 23:41:45 +02:00
Humberto Gontijo
d80898ecc5
Add experimental support for async sessions (#4667)
There no plans for this to ever be non-experimental in v1.
2025-07-28 17:03:31 +02:00
Deer-Spangle
45a546a675 Fix spoilers when sending InputPhoto and InputDocument
After this was reported as a problem for MessageMediaPhoto objects in #4584, it was fixed in commit 37e29e8, but the problem exists for InputPhoto and InputDocument, also. This commit fixes that
2025-07-25 21:04:31 +02:00
WildBeeJS
e168602511
Update to layer 210 2025-07-25 16:45:16 +02:00
Humberto Gontijo
01af2fcca3
Reduce data manipulation operations 2025-07-24 21:46:28 +02:00
humbertogontijo
b59c005903 Add isal gzip decompressor 2025-07-24 08:17:43 +02:00
wkpn
5e150ddf1e Update to layer 209 2025-07-15 17:20:11 +02:00
Darskiy
7d0fadea29 Update to layer 207 2025-07-08 19:51:55 +02:00
Egor
aa17aa65ec
Update to layer 206 (#4654) 2025-07-02 20:38:24 +02:00
UZQueen
31e8ceeecc
Handle cases when Message.reply_to is MessageReplyStoryHeader 2025-06-07 12:27:29 +02:00
Devesh Pal
7b00d2f510 Update to layer 204 2025-06-04 17:08:25 +02:00
zeticsce
17a014906e
Support tg-emoji tag when using html parse_mode 2025-06-03 22:47:39 +02:00
Darskiy
f61518274e
Cleanup usage of removed inline_only parameter (#4630) 2025-05-30 23:18:58 +02:00
Darskiy
8bb2ec30fe
Add persistent and placeholder options to buttons (#4629) 2025-05-28 18:21:50 +02:00
Aayco (Coder)
69e4493c04
Add custom error types for new FROZEN codes 2025-05-26 21:04:26 +02:00
yzx23333
1db71f6d7d Add customizable 'mime_type' for send_file 2025-05-25 17:32:41 +02:00
Darskiy
663a1808a1
Update FileLike type hint to better reflect valid types (#4620) 2025-05-20 22:13:43 +02:00
Darskiy
59da66e105 Update to layer 203 2025-05-13 23:21:34 +02:00
wkpn
6625327b4f Add ChannelForbidden check to utils.get_display_name 2025-05-13 23:05:07 +02:00
Maxsim Smirnov
20434e5a9d Update to layer 202 2025-05-05 20:03:56 +02:00
AEIMS
6a7331b7dc
Add open_url param to control browser opening on click (#4607) 2025-05-04 22:01:55 +02:00
Lonami Exo
77b7edcd6e Also process updates entities with session 2025-04-24 17:36:21 +02:00
Lonami Exo
04922fee3c Stop re-saving input peers on disconnect
This was overwriting actual information on the cache.
2025-04-23 19:21:36 +02:00
Lonami Exo
b2809e0b57 Apply seq if all updates were applied
Instead of only checking if *any* update *with pts* was applied.
Should fix #4602.
2025-04-23 18:25:22 +02:00
Lindsay Zhou
ae9c798e2c Add send_as and message_effect_id for client send_file test
new parameters introduced by 859f7423f2
2025-04-22 17:46:50 +02:00
Lonami Exo
3708fd9605 Update to v1.40 2025-04-21 11:05:32 +02:00
Lonami Exo
5f0695d21b Fix time offset check was in opposite branch 2025-04-18 18:40:53 +02:00
Lonami Exo
9545011de6 Update time offset if first message is badMsgNotification
This should prevent the library from getting stuck during connect.
2025-04-17 17:08:08 +02:00
Lonami Exo
11658d3bbf Combine tdlib and tdesktop schemas
It is safer to keep additions than unnecessarily removing.
Additional flags should also be safe to keep, but they may
not be usable in practice.
2025-04-17 16:57:30 +02:00
Lewis
a73e5a8c71
Fix layer 201 (#4596) 2025-04-12 22:13:52 +02:00
orShadxw
3921914a96
Fix issues raising errors, media dl and reply markup (#4592) 2025-04-01 18:00:09 +02:00
Ankit Chaubey
c409d8c605 Update to Layer 201 2025-03-26 18:06:16 +01:00
Lonami Exo
19a27d602c Delete dated documentation section
While it might have persuaded some people to avoid ending here,
there's also the possibility this had no effect, or even made
the situation worse. For that, I apologize for any hurt feelings.
2025-03-22 10:42:08 +01:00
Lonami Exo
37e29e8f13 Add missing param on photo-to-input conversion
Closes #4584.
2025-03-20 16:48:17 +01:00
abcdenis
890bf485c9 skip ChannelParticipantLeft in _ParticipantsIter 2025-03-14 17:27:45 +01:00
Lonami Exo
67765f84a5 Update to layer 200 2025-03-09 13:05:29 +01:00
Lonami Exo
4bfe7849f6 Add missing input conversion for MessageMediaGeoLive
Fixes #4576.
2025-03-09 13:00:15 +01:00
Shrimadhav U K
859f7423f2 Add send_as and effect parameters in high level 2025-03-08 17:21:49 +01:00
Nick80835
6f5556373f Call getDifference on login to ensure qts is up to date 2025-03-07 18:03:01 +01:00
Nick80835
0fc9b14674 Make URL regex match everything except ]
Hopefully this is the last time I touch this.
2025-03-07 08:03:31 +01:00
Nick80835
0c2a3c144b Make URL regex match anything again
Including spaces.
2025-03-06 08:03:44 +01:00
martín
a03a8673e1
Clarify warning when reusing unintended session (#4568) 2025-03-02 19:40:34 +01:00
Lonami Exo
045df418df Update to v1.39 2025-02-20 18:26:00 +01:00
Nick80835
592a899aab Update to layer 199 2025-02-13 17:20:59 +01:00
Nick80835
1cb5ff1dd5 Consider range list-like
This allows you to pass range() to things such as get_messages as ids= without first explicitly converting it to a list.
2025-02-09 08:42:44 +01:00
s1xu
9762a83541 fix: support batch sending of image URLs and video URLs in albums 2025-02-08 15:40:56 +01:00
Nick80835
141b620437 Make markdown URL regex less greedy
Fixes multiple URLs in a single message.
2025-02-05 16:56:04 +01:00
Nick80835
551c24f3e4 Fix overlapping URLs and improve overlapping in md
Also remove the unused overlap function.
2025-02-02 04:11:46 +01:00
Nick80835
38d024312e Unconditionally match text and link text in markdown
Fixes cases where there's a nested [] in the text by matching until "](" is reached. This doesn't match newlines in URLs because that makes no sense.
2025-02-01 21:02:27 +01:00
Nick80835
a2926b548f Update to layer 198 2025-01-28 18:23:18 +01:00
Nick80835
455acc43f6 Improve edit_message message type hint
This also allows utils.get_message_id to get the ID of InputMessageID.
2025-01-19 10:40:11 +01:00
Nick80835
792adb78b3 Respect receive_updates=False 2025-01-18 19:03:57 +01:00
Nick80835
5a0e69693b Document drop_author and add drop_media_captions
drop_author is already supported but is undocumented. drop_media_captions for consistency with drop_author being implemented.
2025-01-17 17:02:42 +01:00
Nick80835
b9aafa3441 Fix IOError with some image modes in photo resize
This fixes image compression with mode "P" (potentially others) which is necessary as the server has erroneous alpha color with some types of images (mode "P" for example). This also properly applies the background argument that may be passed to _resize_photo_if_needed by always compressing images with alpha regardless of whether the server will compress the image for us.
2025-01-13 20:37:36 +01:00
Nick80835
494b20db2d
Add missing parameters to Message constructor (#4529) 2025-01-04 19:29:19 +01:00
Danish
0a6b649ead
Updated to Layer 196 (#4525) 2025-01-04 15:15:50 +01:00
Lonami
cfce68e9ad
Avoid error when trying to reset deadline for no msgbox entries
Closes #4520.
2024-12-22 15:07:35 +01:00
Lonami Exo
b09c8c83f7 Update to layer 195 2024-12-15 12:31:27 +01:00
Danipulok
225ea9c3ab fix(session): persist session after session.set_dc 2024-11-20 17:25:56 +01:00
Lonami Exo
9ca3b599fc Fix Python 3.6 compatibility
Introduced on accident by #4475.
2024-11-18 17:04:07 +01:00
Lonami Exo
63d55bbe3d Bump to v1.38 2024-11-17 22:32:16 +01:00
Lonami Exo
c9cce8aa81 Update to layer 193 2024-11-17 22:23:37 +01:00
Colin Watson
70098c58a5 Fix test broken by removal of imghdr
This has been broken since bd11564579.
2024-11-06 16:23:33 +01:00
Colin Watson
769b65efb1 Exclude sign_up from test_all_methods_present
The sign_up method was deprecated in
07a7a8b404 and removed from the
documentation, but since the method technically still exists (even
though it immediately raises ValueError), that broke a test.
2024-11-06 16:23:33 +01:00
Devesh Pal
f03e4b1137
Update to Layer 192 (#4503) 2024-10-31 22:10:02 +01:00
Greg Eremeev
a77835a7d9
Fix reuse of captions during send msg with file (#4500) 2024-10-25 23:27:58 +02:00
Mohsen
85c4a91317
Fix typos in FAQ section (#4498) 2024-10-24 18:56:15 +02:00
Arkadii Halchenko
3f589b287d
Fix parse_mode with albums (#4494)
Closes #4492.
2024-10-20 19:50:08 +02:00
Lonami Exo
8138be2503 Update to layer 190 2024-10-15 23:20:57 +02:00
Devesh Pal
a0e42c1eb7
Update to layer 189 (#4476) 2024-10-05 21:43:51 +02:00
Arkadii Halchenko
4553f04e49
Fix formatting_entities not working for albums (#4475) 2024-10-05 11:13:20 +02:00
Lonami Exo
f652f3f01a Bump to v1.37 2024-09-23 19:01:20 +02:00
Lonami Exo
693c73ec1d Update to layer 188 2024-09-23 19:01:10 +02:00
Mohsen
a9442ef1be
Remove right-adjust from logging example docs (#4461)
Only works with %-style formatting, but not in the logging module.
2024-09-11 20:54:59 +02:00
Devesh Pal
d37b0f812f
Update to layer 187 (#4457) 2024-09-07 10:06:07 +02:00
kristal
b01d3d7a2f
Fix edge case on get_messages when reverse=True (#4455)
Closes #4453.
2024-09-05 17:00:22 +02:00
delobanov
aec957d62d
Add error message back to proxy errors (#4443) 2024-08-29 23:54:34 +02:00
delobanov
46854a7660
Fix ConnectionError() takes no keyword arguments with proxies (#4440) 2024-08-28 22:41:38 +02:00
Lonami Exo
90f1e5b073 Update to layer 186 2024-08-25 21:14:51 +02:00
灰白草
75408483ad
Support CDN downloads (#4420)
Closes #4327.
2024-08-07 20:25:35 +02:00
Lonami Exo
946f803de7 Handle FloodPremiumWaitError
Closes #4417.
2024-07-24 16:38:34 +02:00
Shrimadhav U K
087191e9c5
Update API Scheme to Layer 184 (#4413) 2024-07-19 17:43:02 +02:00
Lonami Exo
a5c98aec50 Update to layer 183 2024-07-06 21:03:29 +02:00
2ei
cfebb9df05
Remove unused imports (#4397) 2024-06-21 00:06:40 +02:00
Lonami Exo
04aea46fe4 Update to v1.36 2024-06-11 16:42:47 +02:00
Lonami Exo
3def9433b8 qts_count is always assumed to be 1
Per the docs https://core.telegram.org/api/updates:
> events are never grouped,
> so it's assumed that qts_count is always equal to 1.
2024-06-04 23:12:02 +02:00
Lonami Exo
b3e210a1fb Update to layer 181 2024-06-01 11:00:14 +02:00
Lonami Exo
47673680f4 Update to layer 179 2024-05-01 17:43:24 +02:00
Lonami Exo
1974b663a2 Add missing type to _store_own_updates 2024-04-28 10:16:56 +02:00
Lonami Exo
881bfaac5c Update to layer 179 2024-04-27 16:56:38 +02:00
Lonami Exo
0f6dd5987e Add readthedocs dependency on sphinx-rtd-theme 2024-04-24 19:03:26 +02:00
Lonami Exo
d77ac18695 Bump to v1.35 2024-04-24 18:51:29 +02:00
Lonami Exo
8137b12bec Fix readthedocs requirements 2024-04-24 18:46:57 +02:00
Lonami Exo
10a6d16af6 Update to layer 178 2024-04-24 18:45:09 +02:00
Darskiy
3ac11e15ec
Fix get_messages type hint (#4357) 2024-04-23 16:54:41 +02:00
Darskiy
3625bf849d
Fix get_entity type hint (#4352) 2024-04-18 20:52:29 +02:00
Shubham Kumar
d3a201a277
Fix regression on supported Python version (#4347) 2024-04-12 21:06:15 +02:00
CoderX
49a8f111d3
Add missing attributes to Message (#4346) 2024-04-08 21:24:43 +02:00
Lonami Exo
723fbd570f Update to layer 177 2024-04-05 18:28:31 +02:00
Lonami Exo
26aa178cf6 Handle FileReferenceExpiredError during download
May fix #4341.
2024-04-02 11:02:32 +02:00
Lindsay Zhou
9f3e7e4aa8
Fix TelegramClient init with None session (#4339) 2024-03-30 15:10:12 +01:00
Jordan Gillard
75d609ab2a
Fix type hint in start (#4332) 2024-03-26 20:30:01 +01:00
John Bezustally
4d34243b98
Add drop_author param to forward_messages (#4329) 2024-03-18 08:30:38 +01:00
Lonami Exo
7ceb2e0b25 Add missing quick_reply_shortcut_id 2024-03-10 10:53:32 +01:00
Lonami Exo
47178dfaef Update to layer 176 2024-03-08 23:15:54 +01:00
Lonami Exo
d90d0dc00f message parameter must be optional 2024-02-23 20:53:54 +01:00
Lonami Exo
d1518f002a Fix custom Message lacking parameter 2024-02-21 17:01:03 +01:00
Lonami Exo
319db57ccb Update to layer 174 2024-02-16 23:09:44 +01:00
Confused Character
22bf0b4310
Add custom secret support to TcpMTProxy (#4309) 2024-02-16 22:45:38 +01:00
Lonami Exo
2b99ff65c5 Support pathlib.Path as session again
See #3737.
2024-02-13 18:18:52 +01:00
Lonami Exo
39fc5c5fef Update changelog 2024-02-02 18:23:52 +01:00
Lonami Exo
65c27c5ced Bump to v1.34 2024-02-02 18:17:21 +01:00
Lonami Exo
d76f3b7556 Update to layer 173 2024-02-02 18:16:56 +01:00
Just-a-xD
41eb665c9d
Fix custom parse_mode instances (#4304) 2024-02-02 18:16:01 +01:00
Lonami Exo
70201a9ff1 Fix Message finish init for reply_to stories 2024-01-31 21:57:41 +01:00
Prashant Sengar
63d9b267f4
Fix unparsing of message.text (#4301)
Co-authored-by: Disk6969 <disk6969@users.noreply.github.com>
2024-01-20 10:42:13 +01:00
Lonami Exo
6187ff7dcb Update to layer 172 2024-01-18 18:48:36 +01:00
exovoq
6ee2fffce8
Add reply_to_chat and reply_to_sender in Message (#4300) 2024-01-18 18:48:22 +01:00
Lonami Exo
32a4cb82ce Update to layer 171 2024-01-17 17:09:12 +01:00
Lonami Exo
a97a7a5400 Add new config file for readthedocs 2024-01-14 11:49:49 +01:00
Lonami Exo
9dbe9a7669 Add missing saved_peer_id parameter to Message 2024-01-04 13:01:28 +01:00
Lonami Exo
c445684be8 Update to layer 170 2024-01-01 22:07:20 +01:00
Lonami Exo
1241671e72 Update to layer 169 2023-12-26 10:55:15 +01:00
Jahongir Qurbonov
b882348a2b
Fix restriction_reason type hint (#4282) 2023-12-25 10:13:03 +01:00
Allen Calderwood
2082a0e4de
Fix typo in documentation example (#4277) 2023-12-18 17:32:17 +01:00
Lonami Exo
6cf1be93ae Bump to v1.33.1 2023-12-08 17:31:43 +01:00
Lonami
3d58dc355e
Fix ordering of closing tags of sequential entities (#4268) 2023-12-08 08:12:02 +01:00
udf
3b428f97a9
Fix ordering of nested entities 2023-12-07 18:25:11 +02:00
udf
abeb8c4d8d
Prioritise closing tags when sorting tags 2023-12-07 18:09:02 +02:00
Lonami Exo
985d12e169 Bump to v1.33 2023-12-01 20:30:19 +01:00
Lonami Exo
1ef66896bd Update to layer 167 2023-12-01 20:16:56 +01:00
Balázs Triszka
584735afe1
Conditional webbrowser import (#4261) 2023-11-28 00:04:36 +01:00
mario-ttide
cf3bc71e1d
Retry on TimedOutError (#4255) 2023-11-19 18:14:34 +01:00
Lonami Exo
ddc9bef503 Force filename with JPG extension after resizing
Old name does not matter, since we just encoded it as JPEG
2023-11-12 21:13:24 +01:00
Lonami
308f8e8bf8
Add PR template mentioning v1 is feature-frozen
Should prevent efforts like #4244 going to waste in the future.
2023-11-09 17:04:05 +01:00
Lonami Exo
6ccd6b0a41 Bump to v1.32 2023-10-31 19:12:41 +01:00
Lonami Exo
b17e10af1d Fix init of custom Draft after layer update 2023-10-29 11:41:54 +01:00
Lonami Exo
046dbb58b8 Update to layer 166 2023-10-29 11:00:13 +01:00
Lonami Exo
fda6840449 Fix file name could be lost when uploading files
Leading to invalid extension when sending photos.
2023-10-17 20:31:58 +02:00
Lonami Exo
eb67ef1b15 Update to v1.31 2023-10-12 18:39:29 +02:00
Lonami Exo
7d7dbdf47f Update to layer 165 2023-09-29 22:12:41 +02:00
Lonami Exo
6a36066d19 Update to layer 164 2023-09-22 20:49:39 +02:00
Lonami Exo
bd11564579 Remove uses of imghdr
It's deprecated. Closes #4207.
2023-09-20 18:30:57 +02:00
Alexander Goryushkin
ad19987cd6
Fixed sorting of markup entities with the same offsets (#4201) 2023-09-14 18:52:04 +02:00
Lonami Exo
7325718f0e Fix date empty when getting difference 2023-09-13 17:35:15 +02:00
Lonami Exo
7ce0b2f940 Fix invalid date type in UpdateShort 2023-09-12 17:16:55 +02:00
Dingyuan Wang
5ba312555a
Fix generator for pypy (#4198) 2023-09-12 08:28:14 +02:00
Lonami Exo
2cef715921 Bump to v1.30 2023-09-10 12:28:10 +02:00
Lonami Exo
ba99b8b466 Update to layer 162 2023-09-10 12:15:14 +02:00
Non
72faa89361
Remove client-side check in message.edit (#4195)
Fixes #4193.
2023-09-08 18:35:25 +02:00
Kacnep89
e928fbdac0
Fix date empty (#4191) 2023-09-06 16:47:46 +02:00
Shubham Kumar
9b1d9aa672
Fix incorrect param type in apply_channel_difference (#4185) 2023-08-29 18:40:07 +02:00
Lonami Exo
72f16ef73e Update to layer 161
Closes #4184.
2023-08-29 15:53:25 +02:00
Lonami Exo
33f3e27e7d Change apply_deadlines_reset micro-optimization
No need for buffer reuse in Python. It simply complicates the code.
And even then it was not as optimal as it could.
2023-08-29 15:04:04 +02:00
Lonami Exo
ac483e6812 Only update seq if pts changed
This solves UpdateChatParticipant being missed after UpdateChat,
which seems to reliable occur when a bot is in a Chat that gets
deleted.
2023-08-29 15:04:04 +02:00
Lonami Exo
d40aae75f3 Further improve MessageBox trace logging 2023-08-29 15:04:04 +02:00
Lonami Exo
574e8876ec Fix getting_diff_for with empty set was being spammed
Because the above check used >= but the inner check >.
2023-08-29 15:04:04 +02:00
Lonami Exo
2011a329b0 Make MessageBox trace logs more useful 2023-08-29 15:04:04 +02:00
misuzu
0cc9ca9bd9
Fix is_inline check for KeyboardButtonWebView (#4183) 2023-08-28 17:40:23 +02:00
Kacnep89
e617b59d48
Return marked ID from MemorySession.get_entity_rows_by_id (#4177)
Otherwise the unpacking done later won't work.
2023-08-23 16:07:32 +02:00
Lonami Exo
b0f9fd1f25 Except all types of timeout error
Closes #4172.
2023-08-18 18:36:30 +02:00
Lonami Exo
128b707488 Don't treat asyncio.IncompleteReadError as unhandled
The library will behave the same, but the log severity is lowered.
2023-08-03 19:01:10 +02:00
Bernhard M. Wiedemann
6ded164b85
Sort tlobjects before generating their listing (#4163) 2023-08-01 20:23:24 +02:00
Lonami Exo
211238fcd2 Fix reply_to when sending albums 2023-07-24 17:25:03 +02:00
Nick80835
694c78c8e9
Improve image compression heuristics and algorithm used (#4158) 2023-07-23 21:58:10 +02:00
Lonami Exo
ce010e9bfb Fix handling of UpdateShortSentMessage 2023-07-23 17:12:16 +02:00
Lonami Exo
413a2bb9f3 Bump to v1.29.0 2023-07-23 10:48:04 +02:00
novenary
9cf4cd70d1
Disable blank issues in GitHub (#4157) 2023-07-23 10:32:57 +02:00
Lonami Exo
131f021d51 Don't attempt thumb download if there is no thumb 2023-07-22 10:52:03 +02:00
Lonami Exo
438aff3545 Handle FloodWaitError in update loop
Likely temporary server issues, since getDifference should
realistically not fail with flood waits. In any case,
stopping early until the problem is resolved is the correct
approach.
2023-07-21 23:01:12 +02:00
Lonami Exo
4eef9b52c9 Handle sqlite3.OperationalError in update loop 2023-07-21 22:56:16 +02:00
Lonami Exo
a0cda0c37c Remove client-side checks when editing permissions
The server should instead fail with proper RPC errors,
as the rules could change any time (and the local checks
get out of date).
2023-07-21 22:53:38 +02:00
Lonami Exo
816b0bdf9f Fix _get_thumb failed when document had no thumbs 2023-07-21 22:48:12 +02:00
Lonami Exo
164d35681e Fix reply_to can be optional 2023-07-21 22:44:35 +02:00
Lonami Exo
75ed58ad89 Update to layer 160 2023-07-21 21:24:10 +02:00
Lonami
16ed9614f9 Change html.unparse logic to mimic markdown's
It was overcomplicated and had some subtle bugs.
Closes #4133.
2023-06-17 13:11:14 +02:00
Lonami Exo
9267917031 Improve error message when trying to delete inline messages
Closes #4129.
2023-06-09 17:48:08 +02:00
rozha
1e63de9b68
Fix lack of support for anon channel restrictions (#4130) 2023-06-09 17:42:34 +02:00
Lonami Exo
2826c942c0 Support most usernames in VALID_USERNAME_RE
See #4128.
2023-06-09 17:41:45 +02:00
Devesh Pal
65407fc899
Document more RPC errors (#4127) 2023-06-06 22:51:27 +02:00
Lonami Exo
c3bddf9440 Add missing formatting arg in logging call
Noticed in #4123.
2023-06-02 23:04:14 +02:00
Lonami Exo
4ff7ac6b75 Handle CancelledError inside mtprotosender recv loop 2023-06-02 19:04:51 +02:00
novenary
c3ec775607 Clarify OS field in bug report template 2023-06-02 12:20:27 +03:00
iamilya
aab8009a5a
Fix comment_to for a group of messages (#4120) 2023-05-31 17:04:33 +02:00
Lonami
0f0ca6b0d9
Upgrade issue templates to issue forms (#4118) 2023-05-31 00:02:20 +02:00
Lonami
c89644eec4
Update some fields in the GH issue template 2023-05-31 00:01:16 +02:00
novenary
ed825a2c7d Add dedicated form for documentation issues 2023-05-29 21:58:28 +03:00
novenary
9751b356fe Change feature request template to an issue form 2023-05-29 21:53:41 +03:00
novenary
6acc39ac04 Change bug report template to an issue form
Lifted from Tachiyomi and adapted for Telethon.

See: https://github.com/tachiyomiorg/tachiyomi/blob/master/.github/ISSUE_TEMPLATE/report_issue.yml
2023-05-29 21:29:26 +03:00
Lonami Exo
9fe5937ae1 Update FAQ with more common questions 2023-05-24 19:23:12 +02:00
Lonami Exo
16122545ec Add check for asyncio event loop to remain the same 2023-05-24 19:15:46 +02:00
Lonami Exo
6a7a981b7a Fix asyncio.CancelledError was being swallowed by inner except
Closes #4104.
2023-05-08 22:34:12 +02:00
Lonami Exo
980f8b32fc Fix KeyError when ID is in cache but queried without mark
Closes #4084.
2023-05-05 00:04:30 +02:00
Lonami Exo
c4a41adae5 Better document breaking ToS will lead to bans
Closes #4102.
2023-05-04 19:05:07 +02:00
Lonami Exo
2889bd5bf3 except and propagate TypeNotFoundError during update handling 2023-05-03 17:56:13 +02:00
R.T
9c7ac3b210
Fix absolute import should be relative (#4101) 2023-04-30 18:27:09 +02:00
Lonami Exo
ce29f13606 Fix UserUpdate.last_seen check 2023-04-30 10:32:08 +02:00
Lonami Exo
d7bd554ba0 Fix ValueError during connect with catchup on bad cache
Closes #4080.
2023-04-29 13:10:00 +02:00
Lonami Exo
ccf67d0f4f Exit receive loop on IOError or unhandled exceptions
May help with #4088.
2023-04-29 12:53:25 +02:00
Lonami Exo
03ff996ace Improve unhelpful 'readexactly size can not be less than zero'
Technically closes #4092, as the error is now properly handled.
2023-04-29 12:33:32 +02:00
Lonami Exo
9aad453e1a Update to layer 158 2023-04-21 17:11:59 +02:00
Deer-Spangle
6e7423e894
Allowing nosound_video to be specified (#4090) 2023-04-14 22:03:03 +02:00
Nick80835
7b1b33f805
Save photos as progressive when uploading (#4089) 2023-04-13 20:11:52 +02:00
David
d419979406
Declare missing exception variable (#4087) 2023-04-12 17:36:43 +02:00
SsNiPeR1
acec8a776f
Fix documentation typo (#4086) 2023-04-11 17:43:44 +02:00
Nitan Alexandru Marcel
ced36adb03
Fix editing inline messages of type InputBotInlineMessageID64 (#4082) 2023-04-07 17:04:08 +02:00
Lonami Exo
5b1135734b Properly handle PhoneCodeExpiredError in sign_in
Should actually fix #3185 now.
2023-04-06 18:53:12 +02:00
Lonami Exo
10c74f8bab Bump to v1.28.0 2023-04-06 15:05:44 +02:00
Lonami Exo
af18538722 Handle PhoneCodeExpiredError during sign_in
Closes #3185.
2023-04-06 14:36:24 +02:00
Lonami Exo
fd09284598 Update FAQ
Closes #3759.
2023-04-06 14:32:45 +02:00
Lonami Exo
a657ae0134 Save self user ID in session file
Should result in one less request after connecting,
as there is no longer a need to fetch the self user.
2023-04-06 14:18:42 +02:00
Lonami Exo
88bc6a46a6 Store self user in entity cache 2023-04-06 13:58:26 +02:00
Lonami Exo
97b0ba6707 Flush in-memory cache to session after a limit is reached
Should fully close #3989.
Should help with #3235.
2023-04-06 13:45:12 +02:00
Lonami Exo
cb04e269c0 Fix _get_entity_pair could receive None as input 2023-04-06 13:39:56 +02:00
Lonami Exo
d1e3237c41 Remove now-unused EntityCache class 2023-04-06 13:37:40 +02:00
Lonami Exo
f7e38ee6f0 Remove redundant entity cache
Progress towards #3989.
May also help with #3235.
2023-04-06 13:25:48 +02:00
Lonami Exo
3e64ea35ff Update FAQ 2023-04-06 13:25:47 +02:00
Lonami Exo
f9001bc8e0 Include Telethon version on fatal errors during updates
Since a lot of people don't mention the version when reporting
issues, it makes it hard to determine whether it's already been
fixed or not.
2023-04-06 13:25:47 +02:00
Kacnep89
68ea208b43
Periodically save update state (#4071) 2023-03-28 19:00:36 +02:00
Lonami Exo
0f7756ac68 Remove dead code from send_file 2023-03-28 18:17:07 +02:00
Lonami Exo
33c5ee9be4 Implement progress_callback for sending albums
Closes #3190.
2023-03-28 18:15:57 +02:00
Kacnep89
a942b021bc
Fix conversion and time zone issues (#4072) 2023-03-28 17:38:46 +02:00
Lonami Exo
516a2e7435 Handle timeout error during getDifference
Closes #4043.
2023-03-12 17:46:25 +01:00
Lonami Exo
be59c36ed3 Handle errors at connection level
Closes #4042.
2023-03-12 17:43:36 +01:00
Lonami Exo
acd3407418 Propagate errors at the connection level 2023-03-12 17:43:19 +01:00
Lonami Exo
f3414d134a Handle invalid buffers at protocol level
See #4042.
2023-03-12 17:27:22 +01:00
Lonami Exo
177386e755 Update to layer 155 2023-03-12 17:15:28 +01:00
Lonami Exo
1f79f063a2 Expand documentation on using the raw API 2023-03-11 12:45:06 +01:00
Lonami Exo
b87a8d0c1f Remove mentions to methods in generated TL ref
Instead, consistently use the term request, to avoid confusion.
2023-03-11 12:26:17 +01:00
Lonami Exo
b68c1f4f03 Add docs warning to file.id about it not being maintained 2023-03-11 11:56:43 +01:00
Lonami Exo
6bc7245106 Add file IDs section to the FAQ 2023-03-11 11:56:39 +01:00
Lonami Exo
373601500f Update to layer 154 2023-03-09 21:34:48 +01:00
Lonami Exo
f334d5b8fe Move working with messages to the wiki 2023-02-28 21:58:07 +01:00
Lonami Exo
4de1609d4e Fix ChatAction for groups with hidden members 2023-02-26 11:17:12 +01:00
Lonami Exo
07a7a8b404 Deprecate force_sms and sign_up
On the 10th of February, Telegram sent the following message to
those with an application registered on https://my.telegram.org.

--

Telegram API Update. Hello [REDACTED]. Thank you for contributing to the
open Telegram ecosystem by developing your app, [REDACTED].

Please note that due to recent updates to Telegram's handling of SMS and
the integration of new SMS providers like Firebase, we are changing the
way login codes are handled in third-party apps based on the Telegram API.

Starting on 18.02.2023, users logging into third-party apps will only be
able to receive login codes via Telegram. It will no longer be possible
to request an SMS to log into your app - just like when logging into
Telegram's own desktop and web clients.

Exactly like with the Telegram Desktop and Web apps, if a user doesn't
have a Telegram account yet, they will need to create one first using
an official mobile Telegram app.

We kindly ask you to update your app's login and signup interfaces to
reflect these changes before they go live on 18.02.2023 at 13:00 UTC.

This change will not significantly affect users since, according to our
research, the vast majority of third-party app users also use official
Telegram apps. In the coming months, we expect to offer new tools for
third-party developers that will help streamline the login process.
2023-02-26 10:40:53 +01:00
Lonami Exo
0563430314 Slightly improve documentation 2023-02-26 10:10:19 +01:00
Lonami Exo
acfde7132b Support Message without client as inputs to forward_messages
Raw API does not monkey-patch the client on the returned messages,
so the get_input_chat call would return None, causing the forward
to fail. Instead, manually resolve the chat using the message's
peer_id.

The resolve call can also be avoided if from_peer is passed (which
may mismatch with where the messages actually belong, but that's not
really a concern).
2023-02-08 17:35:49 +01:00
Lonami Exo
610b8c34dd Prevent publishing to PyPi if tl.telethon.dev is out-of-date 2023-02-06 17:40:51 +01:00
Lonami Exo
daf21f12d9 Bump to v1.27 2023-02-05 15:19:41 +01:00
Lonami Exo
6dece6e8a1 Update to layer 152 2023-02-05 15:05:05 +01:00
Lonami Exo
9f077e356b Fix downloading vcard and webdoc with Path
Closes #4027.
2023-01-23 08:48:39 +01:00
Nick80835
1f8b59043b
Change maximum photo size to 2560px (#4024) 2023-01-18 17:37:56 +01:00
Lonami Exo
cc3d25eeb8 Wrap init request in invokeWithoutUpdates if requested
This may fix #3743.
2023-01-14 13:31:32 +01:00
Lonami Exo
d81eb0b2e8 Apply pts returned by some additional requests
When a bot account sends a message, deletes it, and sends a new one,
very reliably it would detect a gap, and as a result recover the
second message it sent, processing it itself (because the hack with
`_self_outgoing` cannot possibly work when catching up).

Now certain `rpc_result` are also processed as-if they were updates
(including the ones from deleting messages), which solves this gap
issue. Not entirely sure if it's a hack or the intended way to do it
(since Telegram *does* return proper `updates` for other RPCs), but
it seems to solve this particular problem.

Other requests such as reading history, mentions or reactions also
return an instance of this type, but the `pts_count` should be 0,
and at worst it should simply trigger a gap, which shouldn't be a
big deal.
2023-01-14 12:31:01 +01:00
Lonami Exo
83bafa25e3 Stop using asyncio.get_event_loop()
It is deprecated in newer Python versions.
Closes #4013.
2023-01-11 21:02:29 +01:00
Lonami Exo
fb97a8aa87 Propagate account being logged-out errors
Should close #4016.
2023-01-11 20:30:33 +01:00
Lonami Exo
c932d79ab3 Ignore improperly formatted errors from Telegram's JSON 2023-01-11 20:28:29 +01:00
Lonami Exo
94cc897019 Sync list of known errors with Telegram's JSON 2023-01-11 19:53:35 +01:00
Lonami Exo
7a74dedc48 Add script to sync errors with Telegram's JSON 2023-01-11 19:53:20 +01:00
Lonami Exo
6332690a51 Sort RPC error data 2023-01-11 19:36:30 +01:00
Lonami Exo
2007c83c9e Update to layer 151 2022-12-31 18:38:53 +01:00
Lonami Exo
7288c9933c Update to layer 150 2022-12-22 20:30:38 +01:00
Lonami Exo
979e38152d Bump to v1.26.1 2022-12-22 20:30:38 +01:00
Lonami Exo
6d2a5dada5 Fix PERSISTENT_TIMESTAMP_EMPTY for new entries with pts 1, count 0
Because Telegram can't actually use 0 for the pts, it uses 1, even
if the count is 0. This forces the next update to use 2, or else it
could not be fetched when using an offset of 1 (despite the count
being 0 on the first update, which should not have bumped the second
update to use 2).

This caused Telethon to create an initial state of 0 for the new entry
(and also "incorrectly" detected following updates as gaps, which
would quickly trigger the call to get difference with a bad pts).

Now Telethon is aware of this special-case and will not initialize
state as 0, even if that's not the "correct" thing to do.
2022-12-22 20:30:38 +01:00
yumupdate
061a84bef2
Fix payment.py example (#4005) 2022-12-22 13:42:30 +01:00
Lonami Exo
e750eb7ab5 Add more debug traces to the messagebox 2022-12-18 12:57:51 +01:00
Lonami Exo
59ffad0090 Promote messagebox warnings to errors
No point in making a warning if it immediately is raised.
2022-12-18 12:42:14 +01:00
Lonami Exo
a8ce308b7a Fix messagebox state trace was not logging the object
This state was a TL State, not the MessageBox State with repr.
2022-12-18 12:41:08 +01:00
Lonami Exo
c72c7b160a Introduce trace-level logs to MessageBox
These will log sensitive information.
They are disabled when running with PYTHONOPTIMIZED.
They can only be enabled by setting a level lower than DEBUG,
which is difficult to do on accident.
2022-12-17 23:13:06 +01:00
Lonami Exo
5080715565 Change updates add_done_callback to discard tasks more reliably
See #3235. This should help tone down memory usage a little.
2022-12-16 08:34:01 +01:00
Lonami Exo
b2925f8279 Update documentation and list of known errors 2022-12-14 20:00:55 +01:00
Lonami Exo
4a6ef97910 Fix calls to disconnect after logout
Introduced by 83f13da420.
2022-12-13 08:33:13 +01:00
Lonami Exo
83f13da420 Don't error when calling disconnect after logout 2022-11-27 11:22:30 +01:00
Lonami Exo
4f51604def Fix sending copies of a file message should ignore parse mode
Closes #3983.
2022-11-26 09:12:06 +01:00
Lonami Exo
ba7fc245ab Set documentation language to en 2022-11-26 09:11:49 +01:00
Lonami Exo
bd1ba3bf1e Bump to v1.26 and update layer to 149 2022-11-25 18:23:28 +01:00
Lonami Exo
8ae75db862 Sort updates preemptively
Closes #3936.

There are two changes made to ensure the first update in a channel
cannot be lost, first by always sorting updates before applying pts,
and second by cautiously initializing the local pts if the client
had no pts known beforehand.

It might be possible to cleanup the handling of possible gaps now
that updates are always sorted, but that requires more thought.
2022-11-20 22:51:59 +01:00
Lonami Exo
2c85ffea12 Fix get_dialogs could fail when count % chunk_size = 0
Closes #3971.
2022-11-09 16:14:12 +01:00
Shrimadhav U K
fb43f638ff
Update to layer 148 and document new more errors (#3970) 2022-11-05 20:09:08 +01:00
Lonami Exo
073b87ba1f Fix some typos and add note about BotFather in migration guide 2022-10-26 14:13:30 +02:00
Lonami Exo
0c868065c7 Handle ConnectionError during update handling 2022-10-25 12:28:40 +02:00
Lonami Exo
2ffac2dcdb Handle DestroyAuthKey result more gracefully 2022-10-21 15:52:00 +02:00
Lonami Exo
f902c9293a Fix log message formatting when obj was not updates 2022-10-21 15:50:37 +02:00
Lonami Exo
a7db08d020 Fix sender destroy session handling was not running 2022-10-21 15:49:18 +02:00
Lonami Exo
0980d55c34 Document how to send spoilers and custom emoji 2022-10-21 11:12:55 +02:00
Lonami Exo
b3266fabd8 Fix iter_messages could get stuck on global search
Found thanks to #920. Issue probably introduced in b6d8311.
2022-10-18 16:00:32 +02:00
th3c00lw0lf
ef4f9a962c
Fix MediaEmptyError error when sending some videos (#3951) 2022-10-15 20:01:29 +02:00
Devesh Pal
f819593cbf
Update to layer 146 (#3933) 2022-10-14 18:50:27 +02:00
Lonami Exo
2d237c41fe Revert accidental NO_UPDATES_TIMEOUT change and bump to v1.25.4 2022-10-14 18:43:44 +02:00
Lonami Exo
7f5a1ec5e1 Bump to v1.25.3 2022-10-14 18:41:39 +02:00
Lonami Exo
949b54fdb0 Fix edit_admin failing on small Chat 2022-10-13 17:56:47 +02:00
Lonami Exo
b6d8311a55 Fix iter_messages was stopping too early in some channels
Closes #3949.
2022-10-13 13:40:25 +02:00
Lonami Exo
db29e9b7ef Don't unnecessarily refetch the sender twice 2022-10-03 13:11:48 +02:00
Lonami Exo
299b090cde Let download_profile_photo work with min-User 2022-10-03 13:06:27 +02:00
Lonami Exo
04cf2953f6 Document that disconnect cancels event handlers
Closes #3942.
2022-10-03 10:25:15 +02:00
Lonami Exo
ad2238e788 Shield disconnect from cancellation
Relevant issue: #3942.
2022-10-03 10:25:15 +02:00
Alfian Pangetsu
908375ac42
Fix get_running_loop usage in Python3.6 (#3941)
Closes #3939.
2022-10-02 19:05:11 +02:00
Lonami Exo
7f472ee72c Add CHAT_FORWARDS_RESTRICTED to known errors 2022-10-02 16:25:52 +02:00
Lonami Exo
d2b1c3ec5f Lower severity of some log messages during update handling
Some people were complaining that their logs were being spammed by it.
2022-10-02 16:07:14 +02:00
Lonami Exo
1cf6cf46bd Bump to v1.25.2 2022-09-27 11:32:38 +02:00
Lonami Exo
bb98f4e68c Fix get_dialogs was not filling channels pts 2022-09-27 11:31:41 +02:00
Lonami Exo
105a7a7c56 Log channel ID when getting their difference 2022-09-27 11:31:26 +02:00
Lonami Exo
fd70b5a428 Update list of known errors 2022-09-27 10:49:12 +02:00
Lonami Exo
6fcd7dff38 Bump to v1.25.1 2022-09-24 11:03:09 +02:00
Lonami Exo
346a3f0ef5 Add note on 2FA for qr_login 2022-09-21 12:28:51 +02:00
Lonami Exo
c975b566a1 Handle ServerError while getting difference
Closes #3870.
2022-09-21 12:17:24 +02:00
Lonami Exo
49bdb762c9 Re-raise unhandled errors that occur during update handling
This should help the situation in #3870.
2022-09-21 12:13:21 +02:00
Lonami Exo
a83fe46baf Document the client instance cannot be used after logout
Closes #3780.
2022-09-21 12:00:55 +02:00
Lonami Exo
17516318e6 Add a hard timeout on disconnect
Closes #3917.
2022-09-21 10:58:35 +02:00
Lonami Exo
6d02a1c6ff Update some raw API examples 2022-09-21 10:50:07 +02:00
Lonami Exo
1f42e6e32f del_surrogate for HTML inline URLs
Closes #3693.
2022-09-20 18:18:26 +02:00
Lonami Exo
ff0f9b0e8f Ignore ChannelParticipantLeft during iter_participants
Closes #3231.
2022-09-20 18:12:29 +02:00
Lonami Exo
2d4305db76 Wrap buttons typehint in Optional
Closes #3762.
2022-09-20 18:03:16 +02:00
Lonami Exo
5a17397fc7 Fix events.Album did not have chat in PM
Closes #3773.
2022-09-20 17:54:05 +02:00
Lonami Exo
d7424ccb90 Ignore aggressive parameter in iter_participants
It's broken (it causes flood wait immediately).
Closes #3787.
2022-09-20 17:35:25 +02:00
Lonami Exo
e6ebe6b334 Replace mentions of master branch with v1 2022-09-20 17:08:53 +02:00
Lonami Exo
18da855dd4 Fix get_permissions for small group chats
Closes #3811.
2022-09-20 16:17:45 +02:00
Lonami Exo
75fe90005f Manually construct reply_to for send_message result
Closes #3803.
2022-09-20 16:05:48 +02:00
Lonami Exo
363c2604df Strip 0-length message entities
Closes #3884.
2022-09-20 13:12:01 +02:00
Lonami Exo
7cac3668d6 Make custom, functions and types proper modules
This allows "from telethon.types import Message" to work.
Closes #3929.

Not entirely sure how it used to work before, perhaps
it got changed at some point but this should revert previous
behaviour.
2022-09-20 12:59:36 +02:00
Lonami Exo
2f2a9901e2 Trust pts values during apply_difference
See #3873.
2022-09-20 12:52:24 +02:00
Lonami Exo
64bc73c41e Do not remove ENTRY_SECRET during apply difference
This probably occurs with bot accounts only.
It is strange that the qts is used by bots but is missing from the
results of getDifference. This would need more investigation, but
it might just be the way this thing works.

Closes #3873.
2022-09-20 12:40:48 +02:00
Lonami Exo
243f58c331 Handle auth errors during get difference 2022-09-20 11:35:59 +02:00
Lonami Exo
06536cfb91 Recognize invite links with plus sign prefix 2022-09-20 10:57:52 +02:00
Lonami Exo
299eceb6eb Document new known RPC errors 2022-09-20 10:56:57 +02:00
Lonami Exo
50aa92ebde Handle CancelledError inside update loop
This error is not really unexpected, since the library uses it to
cancel the task during disconnect.

Closes #3921.
2022-09-14 17:11:13 +02:00
Lonami Exo
7d4424ac2b Make use of AlbumHack for all albums
Closes #3916.

The new MessageBox system is not designed with "albums come in the
same updates container" in mind (in fact, there was a note about this).

This version was also not intended to be published to PyPi, but it is,
so a workaround must be made for events.Album to remain working.

In essence, AlbumHack will always be used even if it technically did not
need to be used previously. This will cause a small delay for those
updates, but it should not be a major issue.
2022-09-14 16:53:56 +02:00
Lonami Exo
a66df977f7 Fix UpdateShort from socket was not unboxed
This was causing UpdateShorts to be sent to Raw handlers,
which in turn broke things like QR login.

Should fix #3922.
2022-09-14 15:53:11 +02:00
Shubham Kumar
935be9dd6e
Fix some MD parsing of inline URLs (#3920) 2022-09-09 21:46:06 +02:00
Lonami Exo
6e8bc0d5b9 Fix raw API docs generation 2022-08-30 13:21:17 +02:00
Lonami Exo
8b1bfcdf9c Bump to v1.25 2022-08-30 12:57:34 +02:00
Lonami Exo
48d7dbe90b Remove missed async keywords from the revert
This should've been in 7d21b40401.
This completes the revert of async sessions.
2022-08-30 12:40:05 +02:00
Lonami Exo
e87e6738b5 Revert "Add missing async keywords in SQLiteSession"
This reverts commit 0f5eeb29e7.
2022-08-30 12:32:23 +02:00
Lonami Exo
7d21b40401 Revert "Make sessions async"
This reverts commit d2de0f3aca.
2022-08-30 12:32:21 +02:00
Lonami Exo
88b2b9372d Revert "Mark certain SQLiteSession methods as async"
This reverts commit f913ea6b75.
2022-08-30 12:22:05 +02:00
Lonami Exo
44e3651adf Revert "Add workaround for SQLiteSession needing save after init"
This reverts commit 8190a92aae.
2022-08-30 12:20:20 +02:00
Tulir Asokan
d5c864597c Update to layer 144 2022-08-30 12:11:38 +02:00
Lonami Exo
df96ead0ab Also except ChannelInvalidError during get_diff
This change comes from here:
2166d913e6
2022-08-30 12:07:14 +02:00
Tulir Asokan
809a07edac Fix missing variable and assignment in reset_deadline 2022-08-30 12:05:33 +02:00
Lonami Exo
4b151fbce9 Handle ValueError during get_channel_difference 2022-08-16 12:09:21 +02:00
Lonami Exo
396594060b Fix reset_deadline was not doing its job
This was leading to a soft deadlock, always trying to get difference
but always receiving empty one and not exiting.
2022-08-11 11:04:37 +02:00
Lonami Exo
dd55e7c748 Prevent double-logging of 'timeout for updates' 2022-08-11 10:53:21 +02:00
Devesh Pal
362d06654f
Support sending 4GB files (#3891) 2022-07-28 12:30:46 +02:00
Rongrong
db3faedbfc
Add ENTITY_BOUNDS_INVALID and POSTPONED_TIMEOUT to known errors (#3887) 2022-07-25 11:52:48 +02:00
Rongrong
046e2cb605
Fix HTML/MD parser producing empty MessageEntity (#3885)
Closes #3884. The implementation is also simplified.
2022-07-25 11:11:26 +02:00
Eugene Lam
066820900d
Update to layer 143 (#3862) 2022-07-02 09:27:35 +02:00
Lonami Exo
f90cdf2ffb Fix apply_difference should not end get diff for secret if not active 2022-05-31 11:58:38 +02:00
Lonami Exo
1af6d9a873 Properly log RpcError with no parent request
This should get rid of the unexpected BufferError traceback.
2022-05-31 11:02:34 +02:00
Lonami Exo
0f5eeb29e7 Add missing async keywords in SQLiteSession 2022-05-30 13:39:08 +02:00
Lonami Exo
441fe9d076 Remove TODOs which are no longer relevant in MessageBox 2022-05-30 13:33:56 +02:00
Lonami Exo
7e0639ac57 Add getting_diff_for assertion in get_channel_difference too 2022-05-30 13:31:29 +02:00
Lonami Exo
898e279218 Assert getting_diff_for is not filled when not possible in get_difference 2022-05-30 13:29:58 +02:00
Lonami Exo
a38170d26a Assert reset_deadline is not unnecessarily called 2022-05-30 13:28:22 +02:00
Lonami Exo
6f6b207866 Better fix for reset_deadline
Follow-up to 876af8f27c.
The issue was caused because we called end_get_diff to cleanup a diff
that could not actually be started.

This also enables further cleanup later on.
2022-05-30 13:27:04 +02:00
Lonami Exo
876af8f27c Fix reset_deadline failing when the state map is empty 2022-05-30 13:04:02 +02:00
Lonami Exo
8190a92aae Add workaround for SQLiteSession needing save after init 2022-05-30 12:59:04 +02:00
Lonami Exo
378ccd17bf Call catch_up before processing updates if the user wants so 2022-05-25 15:31:39 +02:00
Lonami Exo
aa7a083444 Add missing begin_get_diff call on updatesTooLong 2022-05-25 15:28:15 +02:00
Lonami
b180b53619
Fix typo in ChannelTooLong code path 2022-05-23 16:53:34 +02:00
Lonami Exo
6005585764 Don't crash if periodic session access fails
If saving every minute or new entities fails, it's not fatal.
Other places are not checked because it is more critical for
information to be saved, such as disconnect, where we want to
crash if the session cannot be accessed.
2022-05-23 14:02:56 +02:00
Lonami Exo
06b0ae56d4 Treat invalid pts as outdated pts 2022-05-23 13:56:10 +02:00
Lonami Exo
c5bf83eb86 Remove unnecessary workaround when updating local pts 2022-05-23 13:52:32 +02:00
Lonami Exo
5a1b9daf4c Add back UpdateChannelTooLong check
Removed in the previous commit 2bcedb98.
2022-05-23 13:17:12 +02:00
Lonami Exo
2bcedb9820 Process diff.other_updates as if they are socket updates
This prevents duplicates since it contains the control flow to check for pts.
2022-05-23 13:13:39 +02:00
Lonami Exo
9dbf3443d0 Better initialization pts for new channels 2022-05-23 12:20:02 +02:00
Lonami Exo
f50b2f5d61 Handle bans when getting difference 2022-05-23 11:41:59 +02:00
Lonami Exo
dfce1f53a8 Handle PersistentTimestampOutdatedError 2022-05-23 11:10:46 +02:00
Lonami Exo
5e46b6365c Use the correct type in apply_channel_difference
This one should not change the behaviour, but it's done for consistency.
2022-05-23 10:27:57 +02:00
Lonami Exo
d5bfb71e10 Handle get_difference case for secret chats 2022-05-20 20:44:36 +02:00
Lonami Exo
af56429e78 Add repr to MessageBox types 2022-05-20 18:15:43 +02:00
Lonami Exo
dfc6d448ed Expose catch_up in client constructor and default it to False 2022-05-20 14:55:47 +02:00
Lonami Exo
3a44f56f64 Also process own updates in MessageBox 2022-05-19 16:40:32 +02:00
Lonami Exo
80685191ab Add a hack to enable StringSession.save be (a)sync 2022-05-18 14:53:04 +02:00
Lonami Exo
184984ac51 Protect against potential replay attacks
See #3753.
2022-05-18 12:24:28 +02:00
Lonami Exo
09b9cd8193 Fix initial session state load 2022-05-16 19:01:05 +02:00
Lonami Exo
c16fb0dae6 Add missing await in qr_login 2022-05-16 18:56:24 +02:00
Lonami Exo
898eb5b82f Call GetState on login to init MessageBox 2022-05-16 10:05:07 +02:00
Lonami Exo
3c7f53802f Fix saving of update state 2022-05-16 09:36:57 +02:00
Lonami Exo
0dff21a80f Add missing async in sqlite 2022-05-16 09:36:41 +02:00
Lonami Exo
7963af1d17 Add repr to updates session types for easier debugging 2022-05-16 09:36:13 +02:00
Lonami Exo
001df933a5 Disable GHA workflows temporarily 2022-05-13 17:53:20 +02:00
Lonami Exo
db7b7fde3f Actually fix references to TL in MessageBox 2022-05-13 17:46:51 +02:00
Lonami Exo
a5c3df2743 Attempt to load and save MessageBox state 2022-05-13 17:40:03 +02:00
Lonami Exo
053a0052c8 Fix references to TL in MessageBox 2022-05-13 17:39:31 +02:00
Lonami Exo
db09a92bc5 Make use of the new MessageBox 2022-05-13 13:17:16 +02:00
Lonami Exo
b5bfe5d9a1 Remove StateCache 2022-05-13 12:43:50 +02:00
Lonami Exo
f4b2fe9540 Backport v2 MessageBox 2022-05-13 12:29:58 +02:00
Lonami Exo
fdb0720fe9 Don't reset the auth_key upon receiving -404 2022-05-12 12:03:48 +02:00
Lonami Exo
f913ea6b75 Mark certain SQLiteSession methods as async
Follow-up to d2de0f3aca.
2022-05-12 11:08:18 +02:00
Tulir Asokan
ecc036c7f4 Add option to clear unread reactions in send_read_acknowledge 2022-05-12 10:40:31 +02:00
Tulir Asokan
dda696cce4 Add INVITE_REQUEST_SENT to known errors 2022-05-12 10:40:04 +02:00
Tulir Asokan
f351d5dcfd Handle expired phone codes. Fixes mautrix/telegram#326 2022-05-12 10:39:27 +02:00
Tulir Asokan
d2de0f3aca Make sessions async
SQLiteSession is not updated, don't try to use it
2022-05-12 10:36:10 +02:00
Lonami Exo
43f629f665 Update to layer 140 2022-05-12 10:21:03 +02:00
Lonami Exo
5feb210442 Add support for flags2 in the TL 2022-05-12 10:20:42 +02:00
Reinier Romero Mir
f9643bf737
Add missing async when downloading from URL (#3222) 2021-12-01 20:28:55 +01:00
113 changed files with 5807 additions and 2052 deletions

View File

@ -1,27 +0,0 @@
---
name: Bug Report
about: Create a report about a bug inside the library or issues with the documentation
title: ''
labels: ''
assignees: ''
---
**Checklist**
* [ ] The error is in the library's code, and not in my own.
* [ ] I have searched for this issue before posting it and there isn't a duplicate.
* [ ] I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip` and triggered the bug in the latest version.
**Code that causes the issue**
```python
from telethon.sync import TelegramClient
...
```
**Traceback**
```
Traceback (most recent call last):
File "code.py", line 1, in <code>
```

96
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@ -0,0 +1,96 @@
name: Bug Report
description: Create a report about a bug inside the library.
body:
- type: textarea
id: reproducing-example
attributes:
label: Code that causes the issue
description: Provide a code example that reproduces the problem. Try to keep it short without other dependencies.
placeholder: |
```python
from telethon.sync import TelegramClient
...
```
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: Explain what you should expect to happen. Include reproduction steps.
placeholder: |
"I was doing... I was expecting the following to happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: Explain what actually happens.
placeholder: |
"This happened instead..."
validations:
required: true
- type: textarea
id: traceback
attributes:
label: Traceback
description: |
The traceback, if the problem is a crash.
placeholder: |
```
Traceback (most recent call last):
File "code.py", line 1, in <code>
```
- type: input
id: telethon-version
attributes:
label: Telethon version
description: The output of `python -c "import telethon; print(telethon.__version__)"`.
placeholder: "1.x"
validations:
required: true
- type: input
id: python-version
attributes:
label: Python version
description: The output of `python --version`.
placeholder: "3.x"
validations:
required: true
- type: input
id: os
attributes:
label: Operating system (including distribution name and version)
placeholder: Windows 11, macOS 13.4, Ubuntu 23.04...
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments. Is it a server? Network condition?
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: The error is in the library's code, and not in my own.
required: true
- label: I have searched for this issue before posting it and there isn't an open duplicate.
required: true
- label: I ran `pip install -U https://github.com/LonamiWebs/Telethon/archive/v1.zip` and triggered the bug in the latest version.
required: true

View File

@ -1,3 +1,4 @@
blank_issues_enabled: false
contact_links:
- name: Ask questions in StackOverflow
url: https://stackoverflow.com/questions/ask?tags=telethon

View File

@ -0,0 +1,22 @@
name: Documentation Issue
description: Report a problem with the documentation.
labels: [documentation]
body:
- type: textarea
id: description
attributes:
label: Description
description: Describe the problem in detail.
placeholder: This part is unclear...
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: This is a documentation problem, not a question or a bug report.
required: true
- label: I have searched for this issue before posting it and there isn't a duplicate.
required: true

View File

@ -1,10 +0,0 @@
---
name: Feature Request
about: Suggest ideas, changes or other enhancements for the library
title: ''
labels: enhancement
assignees: ''
---
Please describe your idea. Would you like another friendly method? Renaming them to something more appropriated? Changing the way something works?

View File

@ -0,0 +1,22 @@
name: Feature Request
description: Suggest ideas, changes or other enhancements for the library.
labels: [enhancement]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: Please describe your idea. Would you like another friendly method? Renaming them to something more appropriate? Changing the way something works?
placeholder: "It should work like this..."
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: Checklist
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
options:
- label: I have searched for this issue before posting it and there isn't a duplicate.
required: true

5
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,5 @@
<!--
Thanks for the PR! Please keep in mind that v1 is *feature frozen*.
New features very likely won't be merged, although fixes can be sent.
All new development should happen in v2. Thanks!
-->

18
.readthedocs.yaml Normal file
View File

@ -0,0 +1,18 @@
# https://docs.readthedocs.io/en/stable/config-file/v2.html
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: readthedocs/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: readthedocs/requirements.txt

View File

@ -12,6 +12,8 @@ as a user or through a bot account (bot API alternative).
If you have code using Telethon before its 1.0 version, you must
read `Compatibility and Convenience`_ to learn how to migrate.
As with any third-party library for Telegram, be careful not to
break `Telegram's ToS`_ or `Telegram can ban the account`_.
What is this?
-------------
@ -75,7 +77,9 @@ useful information.
.. _asyncio: https://docs.python.org/3/library/asyncio.html
.. _MTProto: https://core.telegram.org/mtproto
.. _Telegram: https://telegram.org
.. _Compatibility and Convenience: https://docs.telethon.dev/en/latest/misc/compatibility-and-convenience.html
.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html
.. _Telegram's ToS: https://core.telegram.org/api/terms
.. _Telegram can ban the account: https://docs.telethon.dev/en/stable/quick-references/faq.html#my-account-was-deleted-limited-when-using-the-library
.. _Read The Docs: https://docs.telethon.dev
.. |logo| image:: logo.svg

View File

@ -3,3 +3,4 @@ pysocks
python-socks[asyncio]
hachoir
pillow
isal

View File

@ -25,7 +25,7 @@ you can run the following command instead:
.. code-block:: sh
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/master.zip
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip
.. note::

View File

@ -16,7 +16,7 @@ For that, you can use **events**.
.. code-block:: python
import logging
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',
level=logging.WARNING)

View File

@ -40,22 +40,22 @@ because tasks are smaller than threads, which are smaller than processes.
What are asyncio basics?
========================
The code samples below assume that you have Python 3.7 or greater installed.
.. code-block:: python
# First we need the asyncio library
import asyncio
# Then we need a loop to work with
loop = asyncio.get_event_loop()
# We also need something to run
async def main():
for char in 'Hello, world!\n':
print(char, end='', flush=True)
await asyncio.sleep(0.2)
# Then, we need to run the loop with a task
loop.run_until_complete(main())
# Then, we can create a new asyncio loop and use it to run our coroutine.
# The creation and tear-down of the loop is hidden away from us.
asyncio.run(main())
What does telethon.sync do?
@ -101,7 +101,7 @@ Instead of this:
# or, using asyncio's default loop (it's the same)
import asyncio
loop = asyncio.get_event_loop() # == client.loop
loop = asyncio.get_running_loop() # == client.loop
me = loop.run_until_complete(client.get_me())
print(me.username)
@ -158,13 +158,10 @@ loops or use ``async with``:
print(message.sender.username)
loop = asyncio.get_event_loop()
# ^ this assigns the default event loop from the main thread to a variable
loop.run_until_complete(main())
# ^ this runs the *entire* loop until the main() function finishes.
# While the main() function does not finish, the loop will be running.
# While the loop is running, you can't run it again.
asyncio.run(main())
# ^ this will create a new asyncio loop behind the scenes and tear it down
# once the function returns. It will run the loop untiil main finishes.
# You should only use this function if there is no other loop running.
The ``await`` keyword blocks the *current* task, and the loop can run
@ -184,14 +181,14 @@ concurrently:
await asyncio.sleep(delay) # await tells the loop this task is "busy"
print('world') # eventually the loop finishes all tasks
loop = asyncio.get_event_loop() # get the default loop for the main thread
loop.create_task(world(2)) # create the world task, passing 2 as delay
loop.create_task(hello(delay=1)) # another task, but with delay 1
async def main():
asyncio.create_task(world(2)) # create the world task, passing 2 as delay
asyncio.create_task(hello(delay=1)) # another task, but with delay 1
await asyncio.sleep(3) # wait for three seconds before exiting
try:
# run the event loop forever; ctrl+c to stop it
# we could also run the loop for three seconds:
# loop.run_until_complete(asyncio.sleep(3))
loop.run_forever()
# create a new temporary asyncio loop and use it to run main
asyncio.run(main())
except KeyboardInterrupt:
pass
@ -209,10 +206,15 @@ The same example, but without the comment noise:
await asyncio.sleep(delay)
print('world')
loop = asyncio.get_event_loop()
loop.create_task(world(2))
loop.create_task(hello(1))
loop.run_until_complete(asyncio.sleep(3))
async def main():
asyncio.create_task(world(2))
asyncio.create_task(hello(delay=1))
await asyncio.sleep(3)
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Can I use threads?
@ -250,9 +252,9 @@ You may have seen this error:
RuntimeError: There is no current event loop in thread 'Thread-1'.
It just means you didn't create a loop for that thread, and if you don't
pass a loop when creating the client, it uses ``asyncio.get_event_loop()``,
which only works in the main thread.
It just means you didn't create a loop for that thread. Please refer to
the ``asyncio`` documentation to correctly learn how to set the event loop
for non-main threads.
client.run_until_disconnected() blocks!

View File

@ -28,6 +28,9 @@ their own Telegram bots. Quoting their main page:
Bot API is simply an HTTP endpoint which translates your requests to it into
MTProto calls through tdlib_, their bot backend.
Configuration of your bot, such as its available commands and auto-completion,
is configured through `@BotFather <https://t.me/BotFather>`_.
What is MTProto?
================
@ -296,7 +299,7 @@ After rewriting:
class Subbot(TelegramClient):
def __init__(self, *a, **kw):
await super().__init__(*a, **kw)
super().__init__(*a, **kw)
self.add_event_handler(self.on_update, events.NewMessage)
async def connect():

View File

@ -268,7 +268,7 @@ That means you can do this:
.. code-block:: python
message.user_id
await message.get_input_user()
await message.get_input_sender()
message.user
# ...etc

View File

@ -150,6 +150,6 @@ You can also except it and act as you prefer:
VoIP numbers are very limited, and some countries are more limited too.
.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/master/telethon_generator/data/errors.csv
.. _list of known errors: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_generator/data/errors.csv
.. _raw API page: https://tl.telethon.dev/
.. _messages.sendMessage: https://tl.telethon.dev/methods/messages/send_message.html

View File

@ -10,13 +10,20 @@ The Full API
methods listed on :ref:`client-ref` unless you have a better reason
not to, like a method not existing or you wanting more control.
.. contents::
Introduction
============
The :ref:`telethon-client` doesn't offer a method for every single request
the Telegram API supports. However, it's very simple to *call* or *invoke*
any request. Whenever you need something, don't forget to `check the documentation`_
and look for the `method you need`_. There you can go through a sorted list
of everything you can do.
any request defined in Telegram's API.
This section will teach you how to use what Telethon calls the `TL reference`_.
The linked page contains a list and a way to search through *all* types
generated from the definition of Telegram's API (in ``.tl`` file format,
hence the name). These types include requests and constructors.
.. note::
@ -25,10 +32,193 @@ of everything you can do.
as you type, and a "Copy import" button. If you like namespaces, you
can also do ``from telethon.tl import types, functions``. Both work.
Telegram makes these ``.tl`` files public, which other implementations, such
as Telethon, can also use to generate code. These files are versioned under
what's called "layers". ``.tl`` files consist of thousands of definitions,
and newer layers often add, change, or remove them. Each definition refers
to either a Remote Procedure Call (RPC) function, or a type (which the
`TL reference`_ calls "constructors", as they construct particular type
instances).
You should also refer to the documentation to see what the objects
(constructors) Telegram returns look like. Every constructor inherits
from a common type, and that's the reason for this distinction.
As such, the `TL reference`_ is a good place to go to learn about all possible
requests, types, and what they look like. If you're curious about what's been
changed between layers, you can refer to the `TL diff`_ site.
Navigating the TL reference
===========================
Functions
---------
"Functions" is the term used for the Remote Procedure Calls (RPC) that can be
sent to Telegram to ask it to perform something (e.g. "send message"). These
requests have an associated return type. These can be invoked ("called"):
.. code-block:: python
client = TelegramClient(...)
function_instance = SomeRequest(...)
# Invoke the request
returned_type = await client(function_instance)
Whenever you find the type for a function in the `TL reference`_, the page
will contain the following information:
* What type of account can use the method. This information is regenerated
from time to time (by attempting to invoke the function under both account
types and finding out where it fails). Some requests can only be used by
bot accounts, others by user accounts, and others by both.
* The TL definition. This helps you get a feel for the what the function
looks like. This is not Python code. It just contains the definition in
a concise manner.
* "Copy import" button. Does what it says: it will copy the necessary Python
code to import the function to your system's clipboard for easy access.
* Returns. The returned type. When you invoke the function, this is what the
result will be. It also includes which of the constructors can be returned
inline, to save you a click.
* Parameters. The parameters accepted by the function, including their type,
whether they expect a list, and whether they're optional.
* Known RPC errors. A best-effort list of known errors the request may cause.
This list is not complete and may be out of date, but should provide an
overview of what could go wrong.
* Example. Autogenerated example, showcasing how you may want to call it.
Bear in mind that this is *autogenerated*. It may be spitting out non-sense.
The goal of this example is not to show you everything you can do with the
request, only to give you a feel for what it looks like to use it.
It is very important to click through the links and navigate to get the full
picture. A specific page will show you what the specific function returns and
needs as input parameters. But it may reference other types, so you need to
navigate to those to learn what those contain or need.
Types
-----
"Types" as understood by TL are not actually generated in Telethon.
They would be the "abstract base class" of the constructors, but since Python
is duck-typed, there is hardly any need to generate mostly unnecessary code.
The page for a type contains:
* Constructors. Every type will have one or more constructors. These
constructors *are* generated and can be immported and used.
* Requests returning this type. A helpful way to find out "what requests can
return this?". This is how you may learn what request you need to use to
obtain a particular instance of a type.
* Requests accepting this type as input. A helpful way to find out "what
requests can use this type as one of their input parameters?". This is how
you may learn where a type is used.
* Other types containing this type. A helpful way to find out "where else
does this type appear?". This is how you can walk back through nested
objects.
Constructors
------------
Constructors are used to create instances of a particular type, and are also
returned when invoking requests. You will have to create instances yourself
when invoking requests that need a particular type as input.
The page for a constructor contains:
* Belongs to. The parent type. This is a link back to the types page for the
specific constructor. It also contains the sibling constructors inline, to
save you a click.
* Members. Both the input parameters *and* fields the constructor contains.
Using the TL reference
======================
After you've found a request you want to send, a good start would be to simply
copy and paste the autogenerated example into your script. Then you can simply
tweak it to your needs.
If you want to do it from scratch, first, make sure to import the request into
your code (either using the "Copy import" button near the top, or by manually
spelling out the package under ``telethon.tl.functions.*``).
Then, start reading the parameters one by one. If the parameter cannot be
omitted, you **will** need to specify it, so make sure to spell it out as
an input parameter when constructing the request instance. Let's look at
`PingRequest`_ for example. First, we copy the import:
.. code-block:: python
from telethon.tl.functions import PingRequest
Then, we look at the parameters:
ping_id - long
A single parameter, and it's a long (a integer number with a large range of
values). It doesn't say it can be omitted, so we must provide it, like so:
.. code-block:: python
PingRequest(
ping_id=48641868471
)
(In this case, the ping ID is a random number. You often have to guess what
the parameter needs just by looking at the name.)
Now that we have our request, we can invoke it:
.. code-block:: python
response = await client(PingRequest(
ping_id=48641868471
))
To find out what ``response`` looks like, we can do as the autogenerated
example suggests and "stringify" the result as a pretty-printed string:
.. code-block:: python
print(result.stringify())
This will print out the following:
.. code-block:: python
Pong(
msg_id=781875678118,
ping_id=48641868471
)
Which is a very easy way to get a feel for a response. You should nearly
always print the stringified result, at least once, when trying out requests,
to get a feel for what the response may look like.
But of course, you don't need to do that. Without writing any code, you could
have navigated through the "Returns" link to learn ``PingRequest`` returns a
``Pong``, which only has one constructor, and the constructor has two members,
``msg_id`` and ``ping_id``.
If you wanted to create your own ``Pong``, you would use both members as input
parameters:
.. code-block:: python
my_pong = Pong(
msg_id=781875678118,
ping_id=48641868471
)
(Yes, constructing object instances can use the same code that ``.stringify``
would return!)
And if you wanted to access the ``msg_id`` member, you would simply access it
like any other attribute access in Python:
.. code-block:: python
print(response.msg_id)
Example walkthrough
===================
Say `client.send_message()
<telethon.client.messages.MessageMethods.send_message>` didn't exist,
@ -224,6 +414,7 @@ and still access the successful results:
# The second request failed.
second = e.exceptions[1]
.. _check the documentation: https://tl.telethon.dev
.. _method you need: https://tl.telethon.dev/methods/index.html
.. _TL reference: https://tl.telethon.dev
.. _TL diff: https://diff.telethon.dev
.. _PingRequest: https://tl.telethon.dev/methods/ping.html
.. _use the search: https://tl.telethon.dev/?q=message&redirect=no

View File

@ -143,7 +143,7 @@ output (likely your terminal).
.. warning::
**Keep this string safe!** Anyone with this string can use it
to login into your account and do anything they want to to do.
to login into your account and do anything they want to.
This is similar to leaking your ``*.session`` files online,
but it is easier to leak a string than it is to leak a file.

View File

@ -191,8 +191,7 @@ so the code above and the following are equivalent:
async def main():
await client.disconnected
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())
You could also run `client.disconnected
@ -207,7 +206,7 @@ Notice that unlike `client.disconnected
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,
`client.run_until_disconnected
<telethon.client.updates.UpdateMethods.run_until_disconnected>` will
handle ``KeyboardInterrupt`` with you. This method is special and can
handle ``KeyboardInterrupt`` for you. This method is special and can
also be ran while the loop is running, so you can do this:
.. code-block:: python

View File

@ -85,7 +85,7 @@ release = version
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = 'en'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.

View File

@ -25,7 +25,7 @@ you should use :tl:`GetFullUser`:
# or even
full = await client(GetFullUserRequest('username'))
bio = full.about
bio = full.full_user.about
See :tl:`UserFull` to know what other fields you can access.

View File

@ -2,44 +2,12 @@
Working with messages
=====================
.. note::
These examples assume you have read :ref:`full-api`.
.. contents::
This section has been `moved to the wiki`_, where it can be easily edited as new
features arrive and the API changes. Please refer to the linked page to learn how
to send spoilers, custom emoji, stickers, react to messages, and more things.
Sending stickers
================
Stickers are nothing else than ``files``, and when you successfully retrieve
the stickers for a certain sticker set, all you will have are ``handles`` to
these files. Remember, the files Telegram holds on their servers can be
referenced through this pair of ID/hash (unique per user), and you need to
use this handle when sending a "document" message. This working example will
send yourself the very first sticker you have:
.. code-block:: python
# Get all the sticker sets this user has
from telethon.tl.functions.messages import GetAllStickersRequest
sticker_sets = await client(GetAllStickersRequest(0))
# Choose a sticker set
from telethon.tl.functions.messages import GetStickerSetRequest
from telethon.tl.types import InputStickerSetID
sticker_set = sticker_sets.sets[0]
# Get the stickers for this sticker set
stickers = await client(GetStickerSetRequest(
stickerset=InputStickerSetID(
id=sticker_set.id, access_hash=sticker_set.access_hash
)
))
# Stickers are nothing more than files, so send that
await client.send_file('me', stickers.documents[0])
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages

View File

@ -103,7 +103,6 @@ You can also use the menu on the left to quickly skip over sections.
:caption: Miscellaneous
misc/changelog
misc/wall-of-shame.rst
misc/compatibility-and-convenience
.. toctree::

View File

@ -13,6 +13,495 @@ it can take advantage of new goodies!
.. contents:: List of All Versions
New layer (v1.40)
=================
+------------------------+
| Scheme layer used: 201 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=199&to=201>`__.
Additions
~~~~~~~~~
* ``send_as`` and ``effect`` added to ``send_message`` and related methods.
* :tl:`MessageMediaGeoLive` is now recognized for auto-input conversion.
Enhancements
~~~~~~~~~~~~
* Improved wording when using a likely unintended session file.
* Improved behaviour for matching Markdown links.
* A truly clean update-state is now fetched upon login. This was most notably important for bots.
* Time offset is now updated more reliably after connecting. This should fix legitimate "message too old/new" issues.
Bug fixes
~~~~~~~~~
* :tl:`ChannelParticipantLeft` is now skipped in ``iter_participants``.
* ``spoiler`` flag was lost on :tl:`MessageMediaPhoto` auto-input conversion.
* :tl:`KeyboardButtonCopy` is now recognized as an inline button.
* Downloading web-documents should now work again. Note that this still fetches the file from the original server.
New layer (v1.39)
=================
+------------------------+
| Scheme layer used: 199 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=193&to=199>`__.
Additions
~~~~~~~~~
* ``drop_media_captions`` added to ``forward_messages``, and documented together with ``drop_author``.
* :tl:`InputMediaDocumentExternal` is now recognized when sending albums.
Enhancements
~~~~~~~~~~~~
* ``receive_updates=False`` now covers more cases, however, Telegram is still free to ignore it.
* Better type-hints in several methods.
* Markdown parsing of inline links should cover more cases.
* ``range`` is now considered "list-like" and can be used on e.g. ``ids`` parameters.
Bug fixes
~~~~~~~~~
* Session is now saved after setting the DC.
* Fixed rare crash in entity cache handling when iterating through dialogs.
* Fixed IOError that could occur during automatic resizing of some photos.
New layer (v1.38)
=================
+------------------------+
| Scheme layer used: 193 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=188&to=193>`__.
Bug fixes
~~~~~~~~~
* Formatting entities misbehaved with albums.
* Sending a Message object with a file did not use the new file.
New layer (v1.37)
=================
+------------------------+
| Scheme layer used: 188 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=181&to=188>`__.
Additions
~~~~~~~~~
* Support for CDN downloads should be back. Telethon still prefers no CDN by default.
Enhancements
~~~~~~~~~~~~
* ``FloodWaitPremium`` should now be handled like any other floodwaits.
Bug fixes
~~~~~~~~~
* Fixed edge-case when using ``get_messages(..., reverse=True)``.
* ``ConnectionError`` when using proxies should be raised properly.
New layer (v1.36)
=================
+------------------------+
| Scheme layer used: 181 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=178&to=181>`__.
Bug fixes
~~~~~~~~~
* Certain updates, such as :tl:`UpdateBotStopped`, should now be processed reliably.
New layer (v1.35)
=================
+------------------------+
| Scheme layer used: 178 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=173&to=178>`__.
Additions
~~~~~~~~~
* ``drop_author`` parameter now exposed in ``forward_messages``.
Enhancements
~~~~~~~~~~~~
* "Custom secret support" should work with ``TcpMTProxy``.
* Some type hints should now be more accurate.
Bug fixes
~~~~~~~~~
* Session path couldn't be a ``pathlib.Path`` or ``None``.
* Python versions older than 3.9 should now be supported again.
* Readthedocs should hopefully build the v1 documentation again.
New layer (v1.34)
=================
+------------------------+
| Scheme layer used: 173 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=167&to=173>`__.
Additions
~~~~~~~~~
* ``reply_to_chat`` and ``reply_to_sender`` are now in ``Message``.
This is useful when you lack access to the chat, but Telegram still included some basic information.
Bug fixes
~~~~~~~~~
* ``parse_mode`` with a custom instance containing both ``parse`` and ``unparse`` should now work.
* Parsing and unparsing message entities should now behave better in certain corner-cases.
New layer (v1.33)
=================
+------------------------+
| Scheme layer used: 167 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=166&to=167>`__.
Enhancements
~~~~~~~~~~~~
* ``webbrowser`` is now imported conditionally, to support niche environments.
* Library should now retry on the suddenly-common ``TimedOutError``.
Bug fixes
~~~~~~~~~
* Sending photos which were automatically resized should work again (included in the v1.32 series).
New layer (v1.32)
=================
+------------------------+
| Scheme layer used: 166 |
+------------------------+
`View new and changed raw API methods <https://diff.telethon.dev/?from=165&to=166>`__.
This enables you to use custom languages in preformatted blocks using HTML:
.. code-block:: html
<pre>
<code class='language-python'>from telethon import TelegramClient</code>
</pre>
Note that Telethon v1's markdown is a custom format and won't support language tags.
If you want to set a custom language, you have to use HTML or a custom formatter.
Dropped imghdr support (v1.31)
==============================
+------------------------+
| Scheme layer used: 165 |
+------------------------+
This release contains a breaking change in preparation for Python 3.12.
If you were sending photos from in-memory ``bytes`` or ``BytesIO`` containing images,
you should now use ``BytesIO`` and set the ``.name`` property to a dummy name.
This will allow Telethon to detect the correct extension (and file type).
.. code-block:: python
# before
image_data = b'...'
client.send_file(chat, image_data)
# after
from io import BytesIO
image_data = BytesIO(b'...')
image_data.name = 'a.jpg' # any name, only the extension matters
client.send_file(chat, image_data)
Bug fixes
~~~~~~~~~
* Code generation wasn't working under PyPy.
* Obtaining markdown or HTML from message text could produce unexpected results sometimes.
* Other fixes for bugs from the previous version, which were already fixed in patch versions.
Breaking Changes
~~~~~~~~~~~~~~~~
* ``imghdr`` is deprecated in newer Python versions, so Telethon no longer uses it.
This means there might be some cases where Telethon fails to infer the file extension for buffers containing images.
If you were relying on this, add ``.name = 'a.jpg'`` (or other extension) to the ``BytesIO`` buffers you upload.
Layer bump and small changes (v1.30)
====================================
+------------------------+
| Scheme layer used: 162 |
+------------------------+
Some of the bug fixes were already present in patch versions of ``v1.29``, but
the new layer necessitated a minor bump.
Enhancements
~~~~~~~~~~~~
* Removed client-side checks for editing messages.
This only affects ``Message.edit``, as ``client.edit_message`` already had
no checks.
* Library should not understand more server-side errors during update handling
which should reduce crashes.
* Client-side image compression should behave better now.
Bug fixes
~~~~~~~~~
* Some updates such as ``UpdateChatParticipant`` were being missed due to the
order in which Telegram sent them. The library now more carefully checks for
the sequence and pts contained in them to avoid dropping them.
* Fixed ``is_inline`` check for :tl:`KeyboardButtonWebView`.
* Fixed some issues getting entity from cache by ID.
* ``reply_to`` should now work when sending albums.
More bug fixing (v1.29)
=======================
+------------------------+
| Scheme layer used: 160 |
+------------------------+
This layer introduces the necessary raw API methods to work with stories.
The library is aiming to be "feature-frozen" for as long as v1 is active,
so friendly client methods are not implemented, but example code to use
stories can be found in the GitHub wiki of the project.
Enhancements
~~~~~~~~~~~~
* Removed client-side checks for methods dealing with chat permissions.
In particular, this means you can now ban channels.
* Improved some error messages and added new classes for more RPC errors.
* The client-side check for valid usernames has been loosened, so that
very short premium usernames are no longer considered invalid.
Bug fixes
~~~~~~~~~
* Attempting to download a thumbnail from documnets without one would fail,
rather than do nothing (since nothing can be downloaded if there is no thumb).
* More errors are caught in the update handling loop.
* HTML ``.text`` should now "unparse" any message contents correctly.
* Fixed some problems related to logging.
* ``comment_to`` should now work as expected with albums.
* ``asyncio.CancelledError`` should now correctly propagate from the update loop.
* Removed some absolute imports in favour of relative imports.
* ``UserUpdate.last_seen`` should now behave correctly.
* Fixed a rare ``ValueError`` during ``connect`` if the session cache was bad.
New Layer and housekeeping (v1.28)
==================================
+------------------------+
| Scheme layer used: 155 |
+------------------------+
Plenty of stale issues closed, as well as improvements for some others.
Additions
~~~~~~~~~
* New ``entity_cache_limit`` parameter in the ``TelegramClient`` constructor.
This should help a bit in keeping memory usage in check.
Enhancements
~~~~~~~~~~~~
* ``progress_callback`` is now called when dealing with albums. See the
documentation on `client.send_file() <telethon.client.uploads.UploadMethods.send_file>`
for details.
* Update state and entities are now periodically saved, so that the information
isn't lost in the case of crash or unexpected script terminations. You should
still be calling ``disconnect`` or using the context-manager, though.
* The client should no longer unnecessarily call ``get_me`` every time it's started.
Bug fixes
~~~~~~~~~
* Messages obtained via raw API could not be used in ``forward_messages``.
* ``force_sms`` and ``sign_up`` have been deprecated. See `issue 4050`_ for details.
It is no longer possible for third-party applications, such as those made with
Telethon, to use those features.
* ``events.ChatAction`` should now work in more cases in groups with hidden members.
* Errors that occur at the connection level should now be properly propagated, so that
you can actually have a chance to handle them.
* Update handling should be more resilient.
* ``PhoneCodeExpiredError`` will correctly clear the stored hash if it occurs in ``sign_in``.
* In patch ``v1.28.2``, :tl:`InputBotInlineMessageID64` can now be used
to edit inline messages.
.. _issue 4050: https://github.com/LonamiWebs/Telethon/issues/4050
New Layer and some Bug fixes (v1.27)
====================================
+------------------------+
| Scheme layer used: 152 |
+------------------------+
Bug fixes
~~~~~~~~~
* When the account is logged-out, the library should now correctly propagate
an error through ``run_until_disconnected`` to let you handle it.
* The library no longer uses ``asyncio.get_event_loop()`` in newer Python
versions, which should get rid of some deprecation warnings.
* It could happen that bots would receive messages sent by themselves,
very often right after they deleted a message. This should happen far
less often now (but might still happen with unlucky timings).
* Maximum photo size for automatic image resizing is now larger.
* The initial request is now correctly wrapped in ``invokeWithoutUpdates``
when updates are disabled after constructing the client instance.
* Using a ``pathlib.Path`` to download contacts and web documents should
now work correctly.
New Layer and some Bug fixes (v1.26)
====================================
+------------------------+
| Scheme layer used: 149 |
+------------------------+
This new layer includes things such as emoji status, more admin log events,
forum topics and message reactions, among other things. You can access these
using raw API. It also contains a few bug fixes.
These were fixed in the v1.25 series:
* ``client.edit_admin`` did not work on small group chats.
* ``client.get_messages`` could stop early in some channels.
* ``client.download_profile_photo`` now should work even if ``User.min``.
* ``client.disconnect`` should no longer hang when being called from within
an event handlers.
* ``client.get_dialogs`` now initializes the update state for channels.
* The message sender should not need to be fetched in more cases.
* Lowered the severity of some log messages to be less spammy.
These are new to v1.26.0:
* Layer update.
* New documented RPC errors.
* Sometimes the first message update to a channel could be missed if said
message was read immediately.
* ``client.get_dialogs`` would fail when the total count evenly divided
the chunk size of 100.
* ``client.get_messages`` could get stuck during a global search.
* Potentially fixed some issues when sending certain videos.
* Update handling should be more resilient.
* The client should handle having its auth key destroyed more gracefully.
* Fixed some issues when logging certain messages.
Bug fixes (v1.25.1)
===================
This version should fix some of the problems that came with the revamped
update handling.
* Some inline URLs were not parsing correctly with markdown.
* ``events.Raw`` was handling :tl:`UpdateShort` which it shouldn't do.
* ``events.Album`` should now work again.
* ``CancelledError`` was being incorrectly logged as a fatal error.
* Some fixes to update handling primarly aimed for bot accounts.
* Update handling now can deal with more errors without crashing.
* Unhandled errors from update handling will now be propagated through
``client.run_until_disconnected``.
* Invite links with ``+`` are now recognized.
* Added new known RPC errors.
* ``telethon.types`` could not be used as a module.
* 0-length message entities are now stripped to avoid errors.
* ``client.send_message`` was not returning a message with ``reply_to``
in some cases.
* ``aggressive`` in ``client.iter_participants`` now does nothing (it did
not really work anymore anyway, and this should prevent other errors).
* ``client.iter_participants`` was failing in some groups.
* Text with HTML URLs could sometimes fail to parse.
* Added a hard timeout during disconnect in order to prevent the program
from freezing.
Please be sure to report issues with update handling if you still encounter
some errors!
Update handling overhaul (v1.25)
================================
+------------------------+
| Scheme layer used: 144 |
+------------------------+
I had plans to release v2 way earlier, but my motivation drained off, so that
didn't happen. The reason for another v1 release is that there was a clear
need to fix some things regarding update handling (which were present in v2).
I did not want to make this release. But with the release date for v2 still
being unclear, I find it necessary to release another v1 version. I apologize
for the delay (I should've done this a lot sooner but didn't because in my
head I would've pushed through and finished v2, but I underestimated how much
work that was and I probably experienced burn-out).
I still don't intend to make new additions to the v1 series (beyond updating
the Telegram layer being used). I still have plans to finish v2 some day.
But in the meantime, new features, such as reactions, will have to be used
through raw API.
This update also backports the update overhaul from v2. If you experience
issues with updates, please report them on the GitHub page for the project.
However, this new update handling should be more reliable, and ``catch_up``
should actually work properly.
Breaking Changes
~~~~~~~~~~~~~~~~
* In order for ``catch_up`` to work (new flag in the ``TelegramClient``
constructor), sessions need to impleemnt the new ``get_update_states``.
Third-party session storages won't have this implemented by the time
this version released, so ``catch_up`` may not work with those.
Rushed release to fix login (v1.24)
===================================
@ -1990,7 +2479,7 @@ the scenes! This means you're now able to do both of the following:
async def main():
await client.send_message('me', 'Hello!')
asyncio.get_event_loop().run_until_complete(main())
asyncio.run(main())
# ...can be rewritten as:

View File

@ -161,19 +161,17 @@ just get rid of ``telethon.sync`` and work inside an ``async def``:
await client.run_until_disconnected()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())
The ``telethon.sync`` magic module simply wraps every method behind:
The ``telethon.sync`` magic module essentially wraps every method behind:
.. code-block:: python
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
asyncio.run(main())
So that you don't have to write it yourself every time. That's the
overhead you pay if you import it, and what you save if you don't.
With some other tricks, so that you don't have to write it yourself every time.
That's the overhead you pay if you import it, and what you save if you don't.
Learning
========

View File

@ -1,65 +0,0 @@
=============
Wall of Shame
=============
This project has an
`issues <https://github.com/LonamiWebs/Telethon/issues>`__ section for
you to file **issues** whenever you encounter any when working with the
library. Said section is **not** for issues on *your* program but rather
issues with Telethon itself.
If you have not made the effort to 1. read through the docs and 2.
`look for the method you need <https://tl.telethon.dev/>`__,
you will end up on the `Wall of
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
i.e. all issues labeled
`"RTFM" <http://www.urbandictionary.com/define.php?term=RTFM>`__:
**rtfm**
Literally "Read The F--king Manual"; a term showing the
frustration of being bothered with questions so trivial that the asker
could have quickly figured out the answer on their own with minimal
effort, usually by reading readily-available documents. People who
say"RTFM!" might be considered rude, but the true rude ones are the
annoying people who take absolutely no self-responibility and expect to
have all the answers handed to them personally.
*"Damn, that's the twelveth time that somebody posted this question
to the messageboard today! RTFM, already!"*
*by Bill M. July 27, 2004*
If you have indeed read the docs, and have tried looking for the method,
and yet you didn't find what you need, **that's fine**. Telegram's API
can have some obscure names at times, and for this reason, there is a
`"question"
label <https://github.com/LonamiWebs/Telethon/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20label%3Aquestion%20>`__
with questions that are okay to ask. Just state what you've tried so
that we know you've made an effort, or you'll go to the Wall of Shame.
Of course, if the issue you're going to open is not even a question but
a real issue with the library (thankfully, most of the issues have been
that!), you won't end up here. Don't worry.
Current winner
--------------
The current winner is `issue
213 <https://github.com/LonamiWebs/Telethon/issues/213>`__:
**Issue:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822978-9a9a6ef0-8ccd-11e7-9ec5-934ea0f57681.jpg
:alt: Winner issue
Winner issue
**Answer:**
.. figure:: https://user-images.githubusercontent.com/6297805/29822983-9d523402-8ccd-11e7-9fb1-5783740ee366.jpg
:alt: Winner issue answer
Winner issue answer

View File

@ -32,7 +32,6 @@ Auth
send_code_request
sign_in
qr_login
sign_up
log_out
edit_2fa

View File

@ -20,7 +20,7 @@ To enable logging, at the following code to the top of your main file:
.. code-block:: python
import logging
logging.basicConfig(format='[%(levelname) 5s/%(asctime)s] %(name)s: %(message)s',
logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',
level=logging.WARNING)
You can change the logging level to be something different, from less to more information:
@ -60,6 +60,16 @@ And except them as such:
My account was deleted/limited when using the library
=====================================================
First and foremost, **this is not a problem exclusive to Telethon.
Any third-party library is prone to cause the accounts to appear banned.**
Even official applications can make Telegram ban an account under certain
circumstances. Third-party libraries such as Telethon are a lot easier to
use, and as such, they are misused to spam, which causes Telegram to learn
certain patterns and ban suspicious activity.
There is no point in Telethon trying to circumvent this. Even if it succeeded,
spammers would then abuse the library again, and the cycle would repeat.
The library will only do things that you tell it to do. If you use
the library with bad intentions, Telegram will hopefully ban you.
@ -67,8 +77,7 @@ However, you may also be part of a limited country, such as Iran or Russia.
In that case, we have bad news for you. Telegram is much more likely to ban
these numbers, as they are often used to spam other accounts, likely through
the use of libraries like this one. The best advice we can give you is to not
abuse the API, like calling many requests really quickly, and to sign up with
these phones through an official application.
abuse the API, like calling many requests really quickly.
We have also had reports from Kazakhstan and China, where connecting
would fail. To solve these connection problems, you should use a proxy.
@ -76,6 +85,16 @@ would fail. To solve these connection problems, you should use a proxy.
Telegram may also ban virtual (VoIP) phone numbers,
as again, they're likely to be used for spam.
More recently (year 2023 onwards), Telegram has started putting a lot more
measures to prevent spam (with even additions such as anonymous participants
in groups or the inability to fetch group members at all). This means some
of the anti-spam measures have gotten more aggressive.
The recommendation has usually been to use the library only on well-established
accounts (and not an account you just created), and to not perform actions that
could be seen as abuse. Telegram decides what those actions are, and they're
free to change how they operate at any time.
If you want to check if your account has been limited,
simply send a private message to `@SpamBot`_ through Telegram itself.
You should notice this by getting errors like ``PeerFloodError``,
@ -179,6 +198,137 @@ won't do unnecessary work unless you need to:
sender = await event.get_sender()
File download is slow or sending files takes too long
=====================================================
The communication with Telegram is encrypted. Encryption requires a lot of
math, and doing it in pure Python is very slow. ``cryptg`` is a library which
containns the encryption functions used by Telethon. If it is installed (via
``pip install cryptg``), it will automatically be used and should provide
a considerable speed boost. You can know whether it's used by configuring
``logging`` (at ``INFO`` level or lower) *before* importing ``telethon``.
Note that the library does *not* download or upload files in parallel, which
can also help with the speed of downloading or uploading a single file. There
are snippets online implementing that. The reason why this is not built-in
is because the limiting factor in the long run are ``FloodWaitError``, and
using parallel download or uploads only makes them occur sooner.
What does "Server sent a very new message with ID" mean?
========================================================
You may also see this error as "Server sent a very old message with ID".
This is a security feature from Telethon that cannot be disabled and is
meant to protect you against replay attacks.
When this message is incorrectly reported as a "bug",
the most common patterns seem to be:
* Your system time is incorrect.
* The proxy you're using may be interfering somehow.
* The Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
What does "Server replied with a wrong session ID" mean?
========================================================
This is a security feature from Telethon that cannot be disabled and is
meant to protect you against unwanted session reuse.
When this message is reported as a "bug", the most common patterns seem to be:
* The proxy you're using may be interfering somehow.
* The Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
* You may be using multiple connections to the Telegram server, which seems
to confuse Telegram.
Most of the time it should be safe to ignore this warning. If the library
still doesn't behave correctly, make sure to check if any of the above bullet
points applies in your case and try to work around it.
If the issue persists and there is a way to reliably reproduce this error,
please add a comment with any additional details you can provide to
`issue 3759`_, and perhaps some additional investigation can be done
(but it's unlikely, as Telegram *is* sending unexpected data).
What does "Could not find a matching Constructor ID for the TLObject" mean?
===========================================================================
Telegram uses "layers", which you can think of as "versions" of the API they
offer. When Telethon reads responses that the Telegram servers send, these
need to be deserialized (into what Telethon calls "TLObjects").
Every Telethon version understands a single Telegram layer. When Telethon
connects to Telegram, both agree on the layer to use. If the layers don't
match, Telegram may send certain objects which Telethon no longer understands.
When this message is reported as a "bug", the most common patterns seem to be
that the Telethon session is being used or has been used from somewhere else.
Make sure that you created the session from Telethon, and are not using the
same session anywhere else. If you need to use the same account from
multiple places, login and use a different session for each place you need.
What does "Task was destroyed but it is pending" mean?
======================================================
Your script likely finished abruptly, the ``asyncio`` event loop got
destroyed, and the library did not get a chance to properly close the
connection and close the session.
Make sure you're either using the context manager for the client or always
call ``await client.disconnect()`` (by e.g. using a ``try/finally``).
What does "The asyncio event loop must not change after connection" mean?
=========================================================================
Telethon uses ``asyncio``, and makes use of things like tasks and queues
internally to manage the connection to the server and match responses to the
requests you make. Most of them are initialized after the client is connected.
For example, if the library expects a result to a request made in loop A, but
you attempt to get that result in loop B, you will very likely find a deadlock.
To avoid a deadlock, the library checks to make sure the loop in use is the
same as the one used to initialize everything, and if not, it throws an error.
The most common cause is ``asyncio.run``, since it creates a new event loop.
If you ``asyncio.run`` a function to create the client and set it up, and then
you ``asyncio.run`` another function to do work, things won't work, so the
library throws an error early to let you know something is wrong.
Instead, it's often a good idea to have a single ``async def main`` and simply
``asyncio.run()`` it and do all the work there. From it, you're also able to
call other ``async def`` without having to touch ``asyncio.run`` again:
.. code-block:: python
# It's fine to create the client outside as long as you don't connect
client = TelegramClient(...)
async def main():
# Now the client will connect, so the loop must not change from now on.
# But as long as you do all the work inside main, including calling
# other async functions, things will work.
async with client:
....
if __name__ == '__main__':
asyncio.run(main())
Be sure to read the ``asyncio`` documentation if you want a better
understanding of event loop, tasks, and what functions you can use.
What does "bases ChatGetter" mean?
==================================
@ -204,6 +354,36 @@ Telegram has a lot to offer, and inheritance helps the library reduce
boilerplate, so it's important to know this concept. For newcomers,
this may be a problem, so we explain what it means here in the FAQ.
Can I send files by ID?
=======================
When people talk about IDs, they often refer to one of two things:
the integer ID inside media, and a random-looking long string.
You cannot use the integer ID to send media. Generally speaking, sending media
requires a combination of ID, ``access_hash`` and ``file_reference``.
The first two are integers, while the last one is a random ``bytes`` sequence.
* The integer ``id`` will always be the same for every account, so every user
or bot looking at a particular media file, will see a consistent ID.
* The ``access_hash`` will always be the same for a given account, but
different accounts will each see their own, different ``access_hash``.
This makes it impossible to get media object from one account and use it in
another. The other account must fetch the media object itself.
* The ``file_reference`` is random for everyone and will only work for a few
hours before it expires. It must be refetched before the media can be used
(to either resend the media or download it).
The second type of "`file ID <https://core.telegram.org/bots/api#inputfile>`_"
people refer to is a concept from the HTTP Bot API. It's a custom format which
encodes enough information to use the media.
Telethon provides an old version of these HTTP Bot API-style file IDs via
``message.file.id``, however, this feature is no longer maintained, so it may
not work. It will be removed in future versions. Nonetheless, it is possible
to find a different Python package (or write your own) to parse these file IDs
and construct the necessary input file objects to send or download the media.
Can I use Flask with the library?
=================================
@ -239,4 +419,5 @@ file and run that, or use the normal ``python`` interpreter.
.. _logging: https://docs.python.org/3/library/logging.html
.. _@SpamBot: https://t.me/SpamBot
.. _issue 297: https://github.com/LonamiWebs/Telethon/issues/297
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples#quart_loginpy
.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy

View File

@ -1 +1,2 @@
telethon
./
sphinx-rtd-theme~=1.3.0

View File

@ -16,6 +16,7 @@ import os
import re
import shutil
import sys
import urllib.request
from pathlib import Path
from subprocess import run
@ -43,6 +44,8 @@ class TempWorkDir:
os.chdir(self.original)
API_REF_URL = 'https://tl.telethon.dev/'
GENERATOR_DIR = Path('telethon_generator')
LIBRARY_DIR = Path('telethon')
@ -155,14 +158,31 @@ def main(argv):
generate(argv[2:], argv[1])
elif len(argv) >= 2 and argv[1] == 'pypi':
# Make sure tl.telethon.dev is up-to-date first
with urllib.request.urlopen(API_REF_URL) as resp:
html = resp.read()
m = re.search(br'layer\s+(\d+)', html)
if not m:
print('Failed to check that the API reference is up to date:', API_REF_URL)
return
from telethon_generator.parsers import find_layer
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
published_layer = int(m[1])
if published_layer != layer:
print('Published layer', published_layer, 'does not match current layer', layer, '.')
print('Make sure to update the API reference site first:', API_REF_URL)
return
# (Re)generate the code to make sure we don't push without it
generate(['tl', 'errors'])
# Try importing the telethon module to assert it has no errors
try:
import telethon
except:
except Exception as e:
print('Packaging for PyPi aborted, importing the module failed.')
print(e)
return
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']

View File

@ -1,9 +1,8 @@
from .client.telegramclient import TelegramClient
from .network import connection
from .tl import types, functions, custom
from .tl.custom import Button
from .tl import patched as _ # import for its side-effects
from . import version, events, utils, errors
from . import version, events, utils, errors, types, functions, custom
__version__ = version.__version__

View File

@ -0,0 +1,3 @@
from .entitycache import EntityCache
from .messagebox import MessageBox, GapError, PrematureEndReason
from .session import SessionState, ChannelState, Entity, EntityType

View File

@ -0,0 +1,59 @@
from .session import EntityType, Entity
_sentinel = object()
class EntityCache:
def __init__(
self,
hash_map: dict = _sentinel,
self_id: int = None,
self_bot: bool = None
):
self.hash_map = {} if hash_map is _sentinel else hash_map
self.self_id = self_id
self.self_bot = self_bot
def set_self_user(self, id, bot, hash):
self.self_id = id
self.self_bot = bot
if hash:
self.hash_map[id] = (hash, EntityType.BOT if bot else EntityType.USER)
def get(self, id):
try:
hash, ty = self.hash_map[id]
return Entity(ty, id, hash)
except KeyError:
return None
def extend(self, users, chats):
# See https://core.telegram.org/api/min for "issues" with "min constructors".
self.hash_map.update(
(u.id, (
u.access_hash,
EntityType.BOT if u.bot else EntityType.USER,
))
for u in users
if getattr(u, 'access_hash', None) and not u.min
)
self.hash_map.update(
(c.id, (
c.access_hash,
EntityType.MEGAGROUP if c.megagroup else (
EntityType.GIGAGROUP if getattr(c, 'gigagroup', None) else EntityType.CHANNEL
),
))
for c in chats
if getattr(c, 'access_hash', None) and not getattr(c, 'min', None)
)
def put(self, entity):
self.hash_map[entity.id] = (entity.hash, entity.ty)
def retain(self, filter):
self.hash_map = {k: v for k, v in self.hash_map.items() if filter(k)}
def __len__(self):
return len(self.hash_map)

View File

@ -0,0 +1,825 @@
"""
This module deals with correct handling of updates, including gaps, and knowing when the code
should "get difference" (the set of updates that the client should know by now minus the set
of updates that it actually knows).
Each chat has its own [`Entry`] in the [`MessageBox`] (this `struct` is the "entry point").
At any given time, the message box may be either getting difference for them (entry is in
[`MessageBox::getting_diff_for`]) or not. If not getting difference, a possible gap may be
found for the updates (entry is in [`MessageBox::possible_gaps`]). Otherwise, the entry is
on its happy path.
Gaps are cleared when they are either resolved on their own (by waiting for a short time)
or because we got the difference for the corresponding entry.
While there are entries for which their difference must be fetched,
[`MessageBox::check_deadlines`] will always return [`Instant::now`], since "now" is the time
to get the difference.
"""
import asyncio
import datetime
import time
import logging
from enum import Enum
from .session import SessionState, ChannelState
from ..tl import types as tl, functions as fn
from ..helpers import get_running_loop
# Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too.
NO_SEQ = 0
# See https://core.telegram.org/method/updates.getChannelDifference.
BOT_CHANNEL_DIFF_LIMIT = 100000
USER_CHANNEL_DIFF_LIMIT = 100
# > It may be useful to wait up to 0.5 seconds
POSSIBLE_GAP_TIMEOUT = 0.5
# After how long without updates the client will "timeout".
#
# When this timeout occurs, the client will attempt to fetch updates by itself, ignoring all the
# updates that arrive in the meantime. After all updates are fetched when this happens, the
# client will resume normal operation, and the timeout will reset.
#
# Documentation recommends 15 minutes without updates (https://core.telegram.org/api/updates).
NO_UPDATES_TIMEOUT = 15 * 60
# object() but with a tag to make it easier to debug
class Sentinel:
__slots__ = ('tag',)
def __init__(self, tag=None):
self.tag = tag or '_'
def __repr__(self):
return self.tag
# Entry "enum".
# Account-wide `pts` includes private conversations (one-to-one) and small group chats.
ENTRY_ACCOUNT = Sentinel('ACCOUNT')
# Account-wide `qts` includes only "secret" one-to-one chats.
ENTRY_SECRET = Sentinel('SECRET')
# Integers will be Channel-specific `pts`, and includes "megagroup", "broadcast" and "supergroup" channels.
# Python's logging doesn't define a TRACE level. Pick halfway between DEBUG and NOTSET.
# We don't define a name for this as libraries shouldn't do that though.
LOG_LEVEL_TRACE = (logging.DEBUG - logging.NOTSET) // 2
_sentinel = Sentinel()
def next_updates_deadline():
return get_running_loop().time() + NO_UPDATES_TIMEOUT
def epoch():
return datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)
class GapError(ValueError):
def __repr__(self):
return 'GapError()'
class PrematureEndReason(Enum):
TEMPORARY_SERVER_ISSUES = 'tmp'
BANNED = 'ban'
# Represents the information needed to correctly handle a specific `tl::enums::Update`.
class PtsInfo:
__slots__ = ('pts', 'pts_count', 'entry')
def __init__(
self,
pts: int,
pts_count: int,
entry: object
):
self.pts = pts
self.pts_count = pts_count
self.entry = entry
@classmethod
def from_update(cls, update):
pts = getattr(update, 'pts', None)
if pts:
pts_count = getattr(update, 'pts_count', None) or 0
try:
entry = update.message.peer_id.channel_id
except AttributeError:
entry = getattr(update, 'channel_id', None) or ENTRY_ACCOUNT
return cls(pts=pts, pts_count=pts_count, entry=entry)
qts = getattr(update, 'qts', None)
if qts:
return cls(pts=qts, pts_count=1, entry=ENTRY_SECRET)
return None
def __repr__(self):
return f'PtsInfo(pts={self.pts}, pts_count={self.pts_count}, entry={self.entry})'
# The state of a particular entry in the message box.
class State:
__slots__ = ('pts', 'deadline')
def __init__(
self,
# Current local persistent timestamp.
pts: int,
# Next instant when we would get the update difference if no updates arrived before then.
deadline: float
):
self.pts = pts
self.deadline = deadline
def __repr__(self):
return f'State(pts={self.pts}, deadline={self.deadline})'
# > ### Recovering gaps
# > […] Manually obtaining updates is also required in the following situations:
# > • Loss of sync: a gap was found in `seq` / `pts` / `qts` (as described above).
# > It may be useful to wait up to 0.5 seconds in this situation and abort the sync in case a new update
# > arrives, that fills the gap.
#
# This is really easy to trigger by spamming messages in a channel (with as little as 3 members works), because
# the updates produced by the RPC request take a while to arrive (whereas the read update comes faster alone).
class PossibleGap:
__slots__ = ('deadline', 'updates')
def __init__(
self,
deadline: float,
# Pending updates (those with a larger PTS, producing the gap which may later be filled).
updates: list # of updates
):
self.deadline = deadline
self.updates = updates
def __repr__(self):
return f'PossibleGap(deadline={self.deadline}, update_count={len(self.updates)})'
# Represents a "message box" (event `pts` for a specific entry).
#
# See https://core.telegram.org/api/updates#message-related-event-sequences.
class MessageBox:
__slots__ = ('_log', 'map', 'date', 'seq', 'next_deadline', 'possible_gaps', 'getting_diff_for')
def __init__(
self,
log,
# Map each entry to their current state.
map: dict = _sentinel, # entry -> state
# Additional fields beyond PTS needed by `ENTRY_ACCOUNT`.
date: datetime.datetime = epoch() + datetime.timedelta(seconds=1),
seq: int = NO_SEQ,
# Holds the entry with the closest deadline (optimization to avoid recalculating the minimum deadline).
next_deadline: object = None, # entry
# Which entries have a gap and may soon trigger a need to get difference.
#
# If a gap is found, stores the required information to resolve it (when should it timeout and what updates
# should be held in case the gap is resolved on its own).
#
# Not stored directly in `map` as an optimization (else we would need another way of knowing which entries have
# a gap in them).
possible_gaps: dict = _sentinel, # entry -> possiblegap
# For which entries are we currently getting difference.
getting_diff_for: set = _sentinel, # entry
):
self._log = log
self.map = {} if map is _sentinel else map
self.date = date
self.seq = seq
self.next_deadline = next_deadline
self.possible_gaps = {} if possible_gaps is _sentinel else possible_gaps
self.getting_diff_for = set() if getting_diff_for is _sentinel else getting_diff_for
if __debug__:
self._trace('MessageBox initialized')
def _trace(self, msg, *args, **kwargs):
# Calls to trace can't really be removed beforehand without some dark magic.
# So every call to trace is prefixed with `if __debug__`` instead, to remove
# it when using `python -O`. Probably unnecessary, but it's nice to avoid
# paying the cost for something that is not used.
self._log.log(LOG_LEVEL_TRACE, 'Current MessageBox state: seq = %r, date = %s, map = %r',
self.seq, self.date.isoformat(), self.map)
self._log.log(LOG_LEVEL_TRACE, msg, *args, **kwargs)
# region Creation, querying, and setting base state.
def load(self, session_state, channel_states):
"""
Create a [`MessageBox`] from a previously known update state.
"""
if __debug__:
self._trace('Loading MessageBox with session_state = %r, channel_states = %r', session_state, channel_states)
deadline = next_updates_deadline()
self.map.clear()
if session_state.pts != NO_SEQ:
self.map[ENTRY_ACCOUNT] = State(pts=session_state.pts, deadline=deadline)
if session_state.qts != NO_SEQ:
self.map[ENTRY_SECRET] = State(pts=session_state.qts, deadline=deadline)
self.map.update((s.channel_id, State(pts=s.pts, deadline=deadline)) for s in channel_states)
self.date = datetime.datetime.fromtimestamp(session_state.date, tz=datetime.timezone.utc)
self.seq = session_state.seq
self.next_deadline = ENTRY_ACCOUNT
def session_state(self):
"""
Return the current state.
This should be used for persisting the state.
"""
return dict(
pts=self.map[ENTRY_ACCOUNT].pts if ENTRY_ACCOUNT in self.map else NO_SEQ,
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
date=self.date,
seq=self.seq,
), {id: state.pts for id, state in self.map.items() if isinstance(id, int)}
def is_empty(self) -> bool:
"""
Return true if the message box is empty and has no state yet.
"""
return ENTRY_ACCOUNT not in self.map
def check_deadlines(self):
"""
Return the next deadline when receiving updates should timeout.
If a deadline expired, the corresponding entries will be marked as needing to get its difference.
While there are entries pending of getting their difference, this method returns the current instant.
"""
now = get_running_loop().time()
if self.getting_diff_for:
return now
deadline = next_updates_deadline()
# Most of the time there will be zero or one gap in flight so finding the minimum is cheap.
if self.possible_gaps:
deadline = min(deadline, *(gap.deadline for gap in self.possible_gaps.values()))
elif self.next_deadline in self.map:
deadline = min(deadline, self.map[self.next_deadline].deadline)
# asyncio's loop time precision only seems to be about 3 decimal places, so it's possible that
# we find the same number again on repeated calls. Without the "or equal" part we would log the
# timeout for updates several times (it also makes sense to get difference if now is the deadline).
if now >= deadline:
# Check all expired entries and add them to the list that needs getting difference.
self.getting_diff_for.update(entry for entry, gap in self.possible_gaps.items() if now >= gap.deadline)
self.getting_diff_for.update(entry for entry, state in self.map.items() if now >= state.deadline)
if __debug__:
self._trace('Deadlines met, now getting diff for %r', self.getting_diff_for)
# When extending `getting_diff_for`, it's important to have the moral equivalent of
# `begin_get_diff` (that is, clear possible gaps if we're now getting difference).
for entry in self.getting_diff_for:
self.possible_gaps.pop(entry, None)
return deadline
# Reset the deadline for the periods without updates for the given entries.
#
# It also updates the next deadline time to reflect the new closest deadline.
def reset_deadlines(self, entries, deadline):
if not entries:
return
for entry in entries:
if entry not in self.map:
raise RuntimeError('Called reset_deadline on an entry for which we do not have state')
self.map[entry].deadline = deadline
if self.next_deadline in entries:
# If the updated deadline was the closest one, recalculate the new minimum.
self.next_deadline = min(self.map.items(), key=lambda entry_state: entry_state[1].deadline)[0]
elif self.next_deadline in self.map and deadline < self.map[self.next_deadline].deadline:
# If the updated deadline is smaller than the next deadline, change the next deadline to be the new one.
# Any entry will do, so the one from the last iteration is fine.
self.next_deadline = entry
# else an unrelated deadline was updated, so the closest one remains unchanged.
# Convenience to reset a channel's deadline, with optional timeout.
def reset_channel_deadline(self, channel_id, timeout):
self.reset_deadlines({channel_id}, get_running_loop().time() + (timeout or NO_UPDATES_TIMEOUT))
# Sets the update state.
#
# Should be called right after login if [`MessageBox::new`] was used, otherwise undesirable
# updates will be fetched.
def set_state(self, state, reset=True):
if __debug__:
self._trace('Setting state %s', state)
deadline = next_updates_deadline()
if state.pts != NO_SEQ or not reset:
self.map[ENTRY_ACCOUNT] = State(pts=state.pts, deadline=deadline)
else:
self.map.pop(ENTRY_ACCOUNT, None)
# Telegram seems to use the `qts` for bot accounts, but while applying difference,
# it might be reset back to 0. See issue #3873 for more details.
#
# During login, a value of zero would mean the `pts` is unknown,
# so the map shouldn't contain that entry.
# But while applying difference, if the value is zero, it (probably)
# truly means that's what should be used (hence the `reset` flag).
if state.qts != NO_SEQ or not reset:
self.map[ENTRY_SECRET] = State(pts=state.qts, deadline=deadline)
else:
self.map.pop(ENTRY_SECRET, None)
self.date = state.date
self.seq = state.seq
# Like [`MessageBox::set_state`], but for channels. Useful when getting dialogs.
#
# The update state will only be updated if no entry was known previously.
def try_set_channel_state(self, id, pts):
if __debug__:
self._trace('Trying to set channel state for %r: %r', id, pts)
if id not in self.map:
self.map[id] = State(pts=pts, deadline=next_updates_deadline())
# Try to begin getting difference for the given entry.
# Fails if the entry does not have a previously-known state that can be used to get its difference.
#
# Clears any previous gaps.
def try_begin_get_diff(self, entry, reason):
if entry not in self.map:
# Won't actually be able to get difference for this entry if we don't have a pts to start off from.
if entry in self.possible_gaps:
raise RuntimeError('Should not have a possible_gap for an entry not in the state map')
if __debug__:
self._trace('Should get difference for %r because %s but cannot due to missing hash', entry, reason)
return
if __debug__:
self._trace('Marking %r as needing difference because %s', entry, reason)
self.getting_diff_for.add(entry)
self.possible_gaps.pop(entry, None)
# Finish getting difference for the given entry.
#
# It also resets the deadline.
def end_get_diff(self, entry):
try:
self.getting_diff_for.remove(entry)
except KeyError:
raise RuntimeError('Called end_get_diff on an entry which was not getting diff for')
self.reset_deadlines({entry}, next_updates_deadline())
assert entry not in self.possible_gaps, "gaps shouldn't be created while getting difference"
# endregion Creation, querying, and setting base state.
# region "Normal" updates flow (processing and detection of gaps).
# Process an update and return what should be done with it.
#
# Updates corresponding to entries for which their difference is currently being fetched
# will be ignored. While according to the [updates' documentation]:
#
# > Implementations [have] to postpone updates received via the socket while
# > filling gaps in the event and `Update` sequences, as well as avoid filling
# > gaps in the same sequence.
#
# In practice, these updates should have also been retrieved through getting difference.
#
# [updates documentation] https://core.telegram.org/api/updates
def process_updates(
self,
updates,
chat_hashes,
result, # out list of updates; returns list of user, chat, or raise if gap
):
# v1 has never sent updates produced by the client itself to the handlers.
# However proper update handling requires those to be processed.
# This is an ugly workaround for that.
self_outgoing = getattr(updates, '_self_outgoing', False)
real_result = result
result = []
date = getattr(updates, 'date', None)
seq = getattr(updates, 'seq', None)
seq_start = getattr(updates, 'seq_start', None)
users = getattr(updates, 'users', None) or []
chats = getattr(updates, 'chats', None) or []
if __debug__:
self._trace('Processing updates with seq = %r, seq_start = %r, date = %s: %s',
seq, seq_start, date.isoformat() if date else None, updates)
if date is None:
# updatesTooLong is the only one with no date (we treat it as a gap)
self.try_begin_get_diff(ENTRY_ACCOUNT, 'received updatesTooLong')
raise GapError
if seq is None:
seq = NO_SEQ
if seq_start is None:
seq_start = seq
# updateShort is the only update which cannot be dispatched directly but doesn't have 'updates' field
updates = getattr(updates, 'updates', None) or [updates.update if isinstance(updates, tl.UpdateShort) else updates]
for u in updates:
u._self_outgoing = self_outgoing
# > For all the other [not `updates` or `updatesCombined`] `Updates` type constructors
# > there is no need to check `seq` or change a local state.
if seq_start != NO_SEQ:
if self.seq + 1 > seq_start:
# Skipping updates that were already handled
if __debug__:
self._trace('Skipping updates as they should have already been handled')
return (users, chats)
elif self.seq + 1 < seq_start:
# Gap detected
self.try_begin_get_diff(ENTRY_ACCOUNT, 'detected gap')
raise GapError
# else apply
def _sort_gaps(update):
pts = PtsInfo.from_update(update)
return pts.pts - pts.pts_count if pts else 0
reset_deadlines = set() # temporary buffer
result.extend(filter(None, (
self.apply_pts_info(u, reset_deadlines=reset_deadlines)
# Telegram can send updates out of order (e.g. ReadChannelInbox first
# and then NewChannelMessage, both with the same pts, but the count is
# 0 and 1 respectively), so we sort them first.
for u in sorted(updates, key=_sort_gaps))))
self.reset_deadlines(reset_deadlines, next_updates_deadline())
if self.possible_gaps:
if __debug__:
self._trace('Trying to re-apply %r possible gaps', len(self.possible_gaps))
# For each update in possible gaps, see if the gap has been resolved already.
for key in list(self.possible_gaps.keys()):
self.possible_gaps[key].updates.sort(key=_sort_gaps)
for _ in range(len(self.possible_gaps[key].updates)):
update = self.possible_gaps[key].updates.pop(0)
# If this fails to apply, it will get re-inserted at the end.
# All should fail, so the order will be preserved (it would've cycled once).
update = self.apply_pts_info(update, reset_deadlines=None)
if update:
result.append(update)
if __debug__:
self._trace('Resolved gap with %r: %s', PtsInfo.from_update(update), update)
# Clear now-empty gaps.
self.possible_gaps = {entry: gap for entry, gap in self.possible_gaps.items() if gap.updates}
real_result.extend(u for u in result if not u._self_outgoing)
if result and not self.possible_gaps:
# > If the updates were applied, local *Updates* state must be updated
# > with `seq` (unless it's 0) and `date` from the constructor.
if __debug__:
self._trace('Updating seq as all updates were applied')
if date != epoch():
self.date = date
if seq != NO_SEQ:
self.seq = seq
return (users, chats)
# Tries to apply the input update if its `PtsInfo` follows the correct order.
#
# If the update can be applied, it is returned; otherwise, the update is stored in a
# possible gap (unless it was already handled or would be handled through getting
# difference) and `None` is returned.
def apply_pts_info(
self,
update,
*,
reset_deadlines,
):
# This update means we need to call getChannelDifference to get the updates from the channel
if isinstance(update, tl.UpdateChannelTooLong):
self.try_begin_get_diff(update.channel_id, 'received updateChannelTooLong')
return None
pts = PtsInfo.from_update(update)
if not pts:
# No pts means that the update can be applied in any order.
if __debug__:
self._trace('No pts in update, so it can be applied in any order: %s', update)
return update
# As soon as we receive an update of any form related to messages (has `PtsInfo`),
# the "no updates" period for that entry is reset.
#
# Build the `HashSet` to avoid calling `reset_deadline` more than once for the same entry.
#
# By the time this method returns, self.map will have an entry for which we can reset its deadline.
if reset_deadlines:
reset_deadlines.add(pts.entry)
if pts.entry in self.getting_diff_for:
# Note: early returning here also prevents gap from being inserted (which they should
# not be while getting difference).
if __debug__:
self._trace('Skipping update with %r as its difference is being fetched', pts)
return None
if pts.entry in self.map:
local_pts = self.map[pts.entry].pts
if local_pts + pts.pts_count > pts.pts:
# Ignore
if __debug__:
self._trace('Skipping update since local pts %r > %r: %s', local_pts, pts, update)
return None
elif local_pts + pts.pts_count < pts.pts:
# Possible gap
# TODO store chats too?
if __debug__:
self._trace('Possible gap since local pts %r < %r: %s', local_pts, pts, update)
if pts.entry not in self.possible_gaps:
self.possible_gaps[pts.entry] = PossibleGap(
deadline=get_running_loop().time() + POSSIBLE_GAP_TIMEOUT,
updates=[]
)
self.possible_gaps[pts.entry].updates.append(update)
return None
else:
# Apply
if __debug__:
self._trace('Applying update pts since local pts %r = %r: %s', local_pts, pts, update)
# In a channel, we may immediately receive:
# * ReadChannelInbox (pts = X, pts_count = 0)
# * NewChannelMessage (pts = X, pts_count = 1)
#
# Notice how both `pts` are the same. If they were to be applied out of order, the first
# one however would've triggered a gap because `local_pts` + `pts_count` of 0 would be
# less than `remote_pts`. So there is no risk by setting the `local_pts` to match the
# `remote_pts` here of missing the new message.
#
# The message would however be lost if we initialized the pts with the first one, since
# the second one would appear "already handled". To prevent this we set the pts to be
# one less when the count is 0 (which might be wrong and trigger a gap later on, but is
# unlikely). This will prevent us from losing updates in the unlikely scenario where these
# two updates arrive in different packets (and therefore couldn't be sorted beforehand).
if pts.entry in self.map:
self.map[pts.entry].pts = pts.pts
else:
# When a chat is migrated to a megagroup, the first update can be a `ReadChannelInbox`
# with `pts = 1, pts_count = 0` followed by a `NewChannelMessage` with `pts = 2, pts_count=1`.
# Note how the `pts` for the message is 2 and not 1 unlike the case described before!
# This is likely because the `pts` cannot be 0 (or it would fail with PERSISTENT_TIMESTAMP_EMPTY),
# which forces the first update to be 1. But if we got difference with 1 and the second update
# also used 1, we would miss it, so Telegram probably uses 2 to work around that.
self.map[pts.entry] = State(
pts=(pts.pts - (0 if pts.pts_count else 1)) or 1,
deadline=next_updates_deadline()
)
return update
# endregion "Normal" updates flow (processing and detection of gaps).
# region Getting and applying account difference.
# Return the request that needs to be made to get the difference, if any.
def get_difference(self):
for entry in (ENTRY_ACCOUNT, ENTRY_SECRET):
if entry in self.getting_diff_for:
if entry not in self.map:
raise RuntimeError('Should not try to get difference for an entry without known state')
gd = fn.updates.GetDifferenceRequest(
pts=self.map[ENTRY_ACCOUNT].pts,
pts_total_limit=None,
date=self.date,
qts=self.map[ENTRY_SECRET].pts if ENTRY_SECRET in self.map else NO_SEQ,
)
if __debug__:
self._trace('Requesting account difference %s', gd)
return gd
return None
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
def apply_difference(
self,
diff,
chat_hashes,
):
if __debug__:
self._trace('Applying account difference %s', diff)
finish = None
result = None
if isinstance(diff, tl.updates.DifferenceEmpty):
finish = True
self.date = diff.date
self.seq = diff.seq
result = [], [], []
elif isinstance(diff, tl.updates.Difference):
finish = True
chat_hashes.extend(diff.users, diff.chats)
result = self.apply_difference_type(diff, chat_hashes)
elif isinstance(diff, tl.updates.DifferenceSlice):
finish = False
chat_hashes.extend(diff.users, diff.chats)
result = self.apply_difference_type(diff, chat_hashes)
elif isinstance(diff, tl.updates.DifferenceTooLong):
finish = True
self.map[ENTRY_ACCOUNT].pts = diff.pts # the deadline will be reset once the diff ends
result = [], [], []
if finish:
account = ENTRY_ACCOUNT in self.getting_diff_for
secret = ENTRY_SECRET in self.getting_diff_for
if not account and not secret:
raise RuntimeError('Should not be applying the difference when neither account or secret was diff was active')
# Both may be active if both expired at the same time.
if account:
self.end_get_diff(ENTRY_ACCOUNT)
if secret:
self.end_get_diff(ENTRY_SECRET)
return result
def apply_difference_type(
self,
diff,
chat_hashes,
):
state = getattr(diff, 'intermediate_state', None) or diff.state
self.set_state(state, reset=False)
# diff.other_updates can contain things like UpdateChannelTooLong and UpdateNewChannelMessage.
# We need to process those as if they were socket updates to discard any we have already handled.
updates = []
self.process_updates(tl.Updates(
updates=diff.other_updates,
users=diff.users,
chats=diff.chats,
date=epoch(),
seq=NO_SEQ, # this way date is not used
), chat_hashes, updates)
updates.extend(tl.UpdateNewMessage(
message=m,
pts=NO_SEQ,
pts_count=NO_SEQ,
) for m in diff.new_messages)
updates.extend(tl.UpdateNewEncryptedMessage(
message=m,
qts=NO_SEQ,
) for m in diff.new_encrypted_messages)
return updates, diff.users, diff.chats
def end_difference(self):
if __debug__:
self._trace('Ending account difference')
account = ENTRY_ACCOUNT in self.getting_diff_for
secret = ENTRY_SECRET in self.getting_diff_for
if not account and not secret:
raise RuntimeError('Should not be ending get difference when neither account or secret was diff was active')
# Both may be active if both expired at the same time.
if account:
self.end_get_diff(ENTRY_ACCOUNT)
if secret:
self.end_get_diff(ENTRY_SECRET)
# endregion Getting and applying account difference.
# region Getting and applying channel difference.
# Return the request that needs to be made to get a channel's difference, if any.
def get_channel_difference(
self,
chat_hashes,
):
entry = next((id for id in self.getting_diff_for if isinstance(id, int)), None)
if not entry:
return None
packed = chat_hashes.get(entry)
if not packed:
# Cannot get channel difference as we're missing its hash
# TODO we should probably log this
self.end_get_diff(entry)
# Remove the outdated `pts` entry from the map so that the next update can correct
# it. Otherwise, it will spam that the access hash is missing.
self.map.pop(entry, None)
return None
state = self.map.get(entry)
if not state:
raise RuntimeError('Should not try to get difference for an entry without known state')
gd = fn.updates.GetChannelDifferenceRequest(
force=False,
channel=tl.InputChannel(packed.id, packed.hash),
filter=tl.ChannelMessagesFilterEmpty(),
pts=state.pts,
limit=BOT_CHANNEL_DIFF_LIMIT if chat_hashes.self_bot else USER_CHANNEL_DIFF_LIMIT
)
if __debug__:
self._trace('Requesting channel difference %s', gd)
return gd
# Similar to [`MessageBox::process_updates`], but using the result from getting difference.
def apply_channel_difference(
self,
request,
diff,
chat_hashes,
):
entry = request.channel.channel_id
if __debug__:
self._trace('Applying channel difference for %r: %s', entry, diff)
self.possible_gaps.pop(entry, None)
if isinstance(diff, tl.updates.ChannelDifferenceEmpty):
assert diff.final
self.end_get_diff(entry)
self.map[entry].pts = diff.pts
return [], [], []
elif isinstance(diff, tl.updates.ChannelDifferenceTooLong):
assert diff.final
self.map[entry].pts = diff.dialog.pts
chat_hashes.extend(diff.users, diff.chats)
self.reset_channel_deadline(entry, diff.timeout)
# This `diff` has the "latest messages and corresponding chats", but it would
# be strange to give the user only partial changes of these when they would
# expect all updates to be fetched. Instead, nothing is returned.
return [], [], []
elif isinstance(diff, tl.updates.ChannelDifference):
if diff.final:
self.end_get_diff(entry)
self.map[entry].pts = diff.pts
chat_hashes.extend(diff.users, diff.chats)
updates = []
self.process_updates(tl.Updates(
updates=diff.other_updates,
users=diff.users,
chats=diff.chats,
date=epoch(),
seq=NO_SEQ, # this way date is not used
), chat_hashes, updates)
updates.extend(tl.UpdateNewChannelMessage(
message=m,
pts=NO_SEQ,
pts_count=NO_SEQ,
) for m in diff.new_messages)
self.reset_channel_deadline(entry, None)
return updates, diff.users, diff.chats
def end_channel_difference(self, request, reason: PrematureEndReason, chat_hashes):
entry = request.channel.channel_id
if __debug__:
self._trace('Ending channel difference for %r because %s', entry, reason)
if reason == PrematureEndReason.TEMPORARY_SERVER_ISSUES:
# Temporary issues. End getting difference without updating the pts so we can retry later.
self.possible_gaps.pop(entry, None)
self.end_get_diff(entry)
elif reason == PrematureEndReason.BANNED:
# Banned in the channel. Forget its state since we can no longer fetch updates from it.
self.possible_gaps.pop(entry, None)
self.end_get_diff(entry)
del self.map[entry]
else:
raise RuntimeError('Unknown reason to end channel difference')
# endregion Getting and applying channel difference.

View File

@ -0,0 +1,195 @@
from typing import Optional, Tuple
from enum import IntEnum
from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
import struct
class SessionState:
"""
Stores the information needed to fetch updates and about the current user.
* user_id: 64-bit number representing the user identifier.
* dc_id: 32-bit number relating to the datacenter identifier where the user is.
* bot: is the logged-in user a bot?
* pts: 64-bit number holding the state needed to fetch updates.
* qts: alternative 64-bit number holding the state needed to fetch updates.
* date: 64-bit number holding the date needed to fetch updates.
* seq: 64-bit-number holding the sequence number needed to fetch updates.
* takeout_id: 64-bit-number holding the identifier of the current takeout session.
Note that some of the numbers will only use 32 out of the 64 available bits.
However, for future-proofing reasons, we recommend you pretend they are 64-bit long.
"""
__slots__ = ('user_id', 'dc_id', 'bot', 'pts', 'qts', 'date', 'seq', 'takeout_id')
def __init__(
self,
user_id: int,
dc_id: int,
bot: bool,
pts: int,
qts: int,
date: int,
seq: int,
takeout_id: Optional[int]
):
self.user_id = user_id
self.dc_id = dc_id
self.bot = bot
self.pts = pts
self.qts = qts
self.date = date
self.seq = seq
self.takeout_id = takeout_id
def __repr__(self):
return repr({k: getattr(self, k) for k in self.__slots__})
class ChannelState:
"""
Stores the information needed to fetch updates from a channel.
* channel_id: 64-bit number representing the channel identifier.
* pts: 64-bit number holding the state needed to fetch updates.
"""
__slots__ = ('channel_id', 'pts')
def __init__(
self,
channel_id: int,
pts: int,
):
self.channel_id = channel_id
self.pts = pts
def __repr__(self):
return repr({k: getattr(self, k) for k in self.__slots__})
class EntityType(IntEnum):
"""
You can rely on the type value to be equal to the ASCII character one of:
* 'U' (85): this entity belongs to a :tl:`User` who is not a ``bot``.
* 'B' (66): this entity belongs to a :tl:`User` who is a ``bot``.
* 'G' (71): this entity belongs to a small group :tl:`Chat`.
* 'C' (67): this entity belongs to a standard broadcast :tl:`Channel`.
* 'M' (77): this entity belongs to a megagroup :tl:`Channel`.
* 'E' (69): this entity belongs to an "enormous" "gigagroup" :tl:`Channel`.
"""
USER = ord('U')
BOT = ord('B')
GROUP = ord('G')
CHANNEL = ord('C')
MEGAGROUP = ord('M')
GIGAGROUP = ord('E')
def canonical(self):
"""
Return the canonical version of this type.
"""
return _canon_entity_types[self]
_canon_entity_types = {
EntityType.USER: EntityType.USER,
EntityType.BOT: EntityType.USER,
EntityType.GROUP: EntityType.GROUP,
EntityType.CHANNEL: EntityType.CHANNEL,
EntityType.MEGAGROUP: EntityType.CHANNEL,
EntityType.GIGAGROUP: EntityType.CHANNEL,
}
class Entity:
"""
Stores the information needed to use a certain user, chat or channel with the API.
* ty: 8-bit number indicating the type of the entity (of type `EntityType`).
* id: 64-bit number uniquely identifying the entity among those of the same type.
* hash: 64-bit signed number needed to use this entity with the API.
The string representation of this class is considered to be stable, for as long as
Telegram doesn't need to add more fields to the entities. It can also be converted
to bytes with ``bytes(entity)``, for a more compact representation.
"""
__slots__ = ('ty', 'id', 'hash')
def __init__(
self,
ty: EntityType,
id: int,
hash: int
):
self.ty = ty
self.id = id
self.hash = hash
@property
def is_user(self):
"""
``True`` if the entity is either a user or a bot.
"""
return self.ty in (EntityType.USER, EntityType.BOT)
@property
def is_group(self):
"""
``True`` if the entity is a small group chat or `megagroup`_.
.. _megagroup: https://telegram.org/blog/supergroups5k
"""
return self.ty in (EntityType.GROUP, EntityType.MEGAGROUP)
@property
def is_broadcast(self):
"""
``True`` if the entity is a broadcast channel or `broadcast group`_.
.. _broadcast group: https://telegram.org/blog/autodelete-inv2#groups-with-unlimited-members
"""
return self.ty in (EntityType.CHANNEL, EntityType.GIGAGROUP)
@classmethod
def from_str(cls, string: str):
"""
Convert the string into an `Entity`.
"""
try:
ty, id, hash = string.split('.')
ty, id, hash = ord(ty), int(id), int(hash)
except AttributeError:
raise TypeError(f'expected str, got {string!r}') from None
except (TypeError, ValueError):
raise ValueError(f'malformed entity str (must be T.id.hash), got {string!r}') from None
return cls(EntityType(ty), id, hash)
@classmethod
def from_bytes(cls, blob):
"""
Convert the bytes into an `Entity`.
"""
try:
ty, id, hash = struct.unpack('<Bqq', blob)
except struct.error:
raise ValueError(f'malformed entity data, got {blob!r}') from None
return cls(EntityType(ty), id, hash)
def __str__(self):
return f'{chr(self.ty)}.{self.id}.{self.hash}'
def __bytes__(self):
return struct.pack('<Bqq', self.ty, self.id, self.hash)
def _as_input_peer(self):
if self.is_user:
return InputPeerUser(self.id, self.hash)
elif self.ty == EntityType.GROUP:
return InputPeerChat(self.id)
else:
return InputPeerChannel(self.id, self.hash)
def __repr__(self):
return repr({k: getattr(self, k) for k in self.__slots__})

View File

@ -7,6 +7,7 @@ import warnings
from .. import utils, helpers, errors, password as pwd_mod
from ..tl import types, functions, custom
from .._updates import SessionState
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
@ -18,8 +19,8 @@ class AuthMethods:
def start(
self: 'TelegramClient',
phone: typing.Callable[[], str] = lambda: input('Please enter your phone (or bot token): '),
password: typing.Callable[[], str] = lambda: getpass.getpass('Please enter your password: '),
phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '),
password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '),
*,
bot_token: str = None,
force_sms: bool = False,
@ -33,12 +34,6 @@ class AuthMethods:
By default, this method will be interactive (asking for
user input if needed), and will handle 2FA if enabled too.
If the phone doesn't belong to an existing account (and will hence
`sign_up` for a new one), **you are agreeing to Telegram's
Terms of Service. This is required and your account
will be banned otherwise.** See https://telegram.org/tos
and https://core.telegram.org/api/terms.
If the event loop is already running, this method returns a
coroutine that you should await on your own code; otherwise
the loop is ran until said coroutine completes.
@ -152,14 +147,16 @@ class AuthMethods:
if bot_token[:bot_token.find(':')] != str(me.id):
warnings.warn(
'the session already had an authorized user so it did '
'not login to the bot account using the provided '
'bot_token (it may not be using the user you expect)'
'not login to the bot account using the provided bot_token; '
'if you were expecting a different user, check whether '
'you are accidentally reusing an existing session'
)
elif phone and not callable(phone) and utils.parse_phone(phone) != me.phone:
warnings.warn(
'the session already had an authorized user so it did '
'not login to the user account using the provided '
'phone (it may not be using the user you expect)'
'not login to the user account using the provided phone; '
'if you were expecting a different user, check whether '
'you are accidentally reusing an existing session'
)
return self
@ -187,7 +184,6 @@ class AuthMethods:
two_step_detected = False
await self.send_code_request(phone, force_sms=force_sms)
sign_up = False # assume login
while attempts < max_attempts:
try:
value = code_callback()
@ -200,19 +196,12 @@ class AuthMethods:
if not value:
raise errors.PhoneCodeEmptyError(request=None)
if sign_up:
me = await self.sign_up(value, first_name, last_name)
else:
# Raises SessionPasswordNeededError if 2FA enabled
me = await self.sign_in(phone, code=value)
# Raises SessionPasswordNeededError if 2FA enabled
me = await self.sign_in(phone, code=value)
break
except errors.SessionPasswordNeededError:
two_step_detected = True
break
except errors.PhoneNumberOccupiedError:
sign_up = False
except errors.PhoneNumberUnoccupiedError:
sign_up = True
except (errors.PhoneCodeEmptyError,
errors.PhoneCodeExpiredError,
errors.PhoneCodeHashEmptyError,
@ -251,13 +240,14 @@ class AuthMethods:
me = await self.sign_in(phone=phone, password=password)
# We won't reach here if any step failed (exit by exception)
signed, name = 'Signed in successfully as', utils.get_display_name(me)
signed, name = 'Signed in successfully as ', utils.get_display_name(me)
tos = '; remember to not break the ToS or you will risk an account ban!'
try:
print(signed, name)
print(signed, name, tos, sep='')
except UnicodeEncodeError:
# Some terminals don't support certain characters
print(signed, name.encode('utf-8', errors='ignore')
.decode('ascii', errors='ignore'))
.decode('ascii', errors='ignore'), tos, sep='')
return self
@ -365,13 +355,18 @@ class AuthMethods:
'and a password only if an RPCError was raised before.'
)
result = await self(request)
try:
result = await self(request)
except errors.PhoneCodeExpiredError:
self._phone_code_hash.pop(phone, None)
raise
if isinstance(result, types.auth.AuthorizationSignUpRequired):
# Emulate pre-layer 104 behaviour
self._tos = result.terms_of_service
raise errors.PhoneNumberUnoccupiedError(request=request)
return self._on_login(result.user)
return await self._on_login(result.user)
async def sign_up(
self: 'TelegramClient',
@ -382,109 +377,41 @@ class AuthMethods:
phone: str = None,
phone_code_hash: str = None) -> 'types.User':
"""
Signs up to Telegram as a new user account.
Use this if you don't have an account yet.
You must call `send_code_request` first.
**By using this method you're agreeing to Telegram's
Terms of Service. This is required and your account
will be banned otherwise.** See https://telegram.org/tos
and https://core.telegram.org/api/terms.
Arguments
code (`str` | `int`):
The code sent by Telegram
first_name (`str`):
The first name to be used by the new account.
last_name (`str`, optional)
Optional last name.
phone (`str` | `int`, optional):
The phone to sign up. This will be the last phone used by
default (you normally don't need to set this).
phone_code_hash (`str`, optional):
The hash returned by `send_code_request`. This can be left as
`None` to use the last hash known for the phone to be used.
Returns
The new created :tl:`User`.
Example
.. code-block:: python
phone = '+34 123 123 123'
await client.send_code_request(phone)
code = input('enter code: ')
await client.sign_up(code, first_name='Anna', last_name='Banana')
This method can no longer be used, and will immediately raise a ``ValueError``.
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
"""
me = await self.get_me()
if me:
return me
raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')
# To prevent abuse, one has to try to sign in before signing up. This
# is the current way in which Telegram validates the code to sign up.
#
# `sign_in` will set `_tos`, so if it's set we don't need to call it
# because the user already tried to sign in.
#
# We're emulating pre-layer 104 behaviour so except the right error:
if not self._tos:
try:
return await self.sign_in(
phone=phone,
code=code,
phone_code_hash=phone_code_hash,
)
except errors.PhoneNumberUnoccupiedError:
pass # code is correct and was used, now need to sign in
if self._tos and self._tos.text:
if self.parse_mode:
t = self.parse_mode.unparse(self._tos.text, self._tos.entities)
else:
t = self._tos.text
sys.stderr.write("{}\n".format(t))
sys.stderr.flush()
phone, phone_code_hash = \
self._parse_phone_and_hash(phone, phone_code_hash)
result = await self(functions.auth.SignUpRequest(
phone_number=phone,
phone_code_hash=phone_code_hash,
first_name=first_name,
last_name=last_name
))
if self._tos:
await self(
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
return self._on_login(result.user)
def _on_login(self, user):
async def _on_login(self, user):
"""
Callback called whenever the login or sign up process completes.
Returns the input user parameter.
"""
self._bot = bool(user.bot)
self._self_input_peer = utils.get_input_peer(user, allow_self=False)
self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
self._authorized = True
state = await self(functions.updates.GetStateRequest())
# the server may send an old qts in getState
difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts))
if isinstance(difference, types.updates.Difference):
state = difference.state
elif isinstance(difference, types.updates.DifferenceSlice):
state = difference.intermediate_state
elif isinstance(difference, types.updates.DifferenceTooLong):
state.pts = difference.pts
self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), [])
return user
async def send_code_request(
self: 'TelegramClient',
phone: str,
*,
force_sms: bool = False) -> 'types.auth.SentCode':
force_sms: bool = False,
_retry_count: int = 0) -> 'types.auth.SentCode':
"""
Sends the Telegram code needed to login to the given phone number.
@ -493,7 +420,8 @@ class AuthMethods:
The phone to which the code will be sent.
force_sms (`bool`, optional):
Whether to force sending as SMS.
Whether to force sending as SMS. This has been deprecated.
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
Returns
An instance of :tl:`SentCode`.
@ -505,6 +433,10 @@ class AuthMethods:
sent = await client.send_code_request(phone)
print(sent)
"""
if force_sms:
warnings.warn('force_sms has been deprecated and no longer works')
force_sms = False
result = None
phone = utils.parse_phone(phone) or self._phone
phone_hash = self._phone_code_hash.get(phone)
@ -514,7 +446,14 @@ class AuthMethods:
result = await self(functions.auth.SendCodeRequest(
phone, self.api_id, self.api_hash, types.CodeSettings()))
except errors.AuthRestartError:
return await self.send_code_request(phone, force_sms=force_sms)
if _retry_count > 2:
raise
return await self.send_code_request(
phone, force_sms=force_sms, _retry_count=_retry_count+1)
# TODO figure out when/if/how this can happen
if isinstance(result, types.auth.SentCodeSuccess):
raise RuntimeError('logged in right after sending the code')
# If we already sent a SMS, do not resend the code (hash may be empty)
if isinstance(result.type, types.auth.SentCodeTypeSms):
@ -529,8 +468,21 @@ class AuthMethods:
self._phone = phone
if force_sms:
result = await self(
functions.auth.ResendCodeRequest(phone, phone_hash))
try:
result = await self(
functions.auth.ResendCodeRequest(phone, phone_hash))
except errors.PhoneCodeExpiredError:
if _retry_count > 2:
raise
self._phone_code_hash.pop(phone, None)
self._log[__name__].info(
"Phone code expired in ResendCodeRequest, requesting a new code"
)
return await self.send_code_request(
phone, force_sms=False, _retry_count=_retry_count+1)
if isinstance(result, types.auth.SentCodeSuccess):
raise RuntimeError('logged in right after resending the code')
self._phone_code_hash[phone] = result.phone_code_hash
@ -568,6 +520,9 @@ class AuthMethods:
# Important! You need to wait for the login to complete!
await qr_login.wait()
# If you have 2FA enabled, `wait` will raise `telethon.errors.SessionPasswordNeededError`.
# You should except that error and call `sign_in` with the password if this happens.
"""
qr_login = custom.QRLogin(self, ignored_ids or [])
await qr_login.recreate()
@ -577,6 +532,8 @@ class AuthMethods:
"""
Logs out Telegram and deletes the current ``*.session`` file.
The client is unusable after logging out and a new instance should be created.
Returns
`True` if the operation was successful.
@ -591,13 +548,12 @@ class AuthMethods:
except errors.RPCError:
return False
self._bot = None
self._self_input_peer = None
self._mb_entity_cache.set_self_user(None, None, None)
self._authorized = False
self._state_cache.reset()
await self.disconnect()
self.session.delete()
await utils.maybe_async(self.session.delete())
self.session = None
return True
async def edit_2fa(

View File

@ -7,8 +7,8 @@ from ..tl import types, custom
class ButtonMethods:
@staticmethod
def build_reply_markup(
buttons: 'typing.Optional[hints.MarkupLike]',
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
buttons: 'typing.Optional[hints.MarkupLike]'
) -> 'typing.Optional[types.TypeReplyMarkup]':
"""
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
the given buttons.
@ -26,9 +26,6 @@ class ButtonMethods:
The button, list of buttons, array of buttons or markup
to convert into a markup.
inline_only (`bool`, optional):
Whether the buttons **must** be inline buttons only or not.
Example
.. code-block:: python
@ -42,8 +39,8 @@ class ButtonMethods:
return None
try:
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2:
return buttons # crc32(b'ReplyMarkup'):
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'):
return buttons
except AttributeError:
pass
@ -57,6 +54,8 @@ class ButtonMethods:
resize = None
single_use = None
selective = None
persistent = None
placeholder = None
rows = []
for row in buttons:
@ -69,6 +68,10 @@ class ButtonMethods:
single_use = button.single_use
if button.selective is not None:
selective = button.selective
if button.persistent is not None:
persistent = button.persistent
if button.placeholder is not None:
placeholder = button.placeholder
button = button.button
elif isinstance(button, custom.MessageButton):
@ -78,19 +81,21 @@ class ButtonMethods:
is_inline |= inline
is_normal |= not inline
if button.SUBCLASS_OF_ID == 0xbad74a3:
# 0xbad74a3 == crc32(b'KeyboardButton')
if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton')
current.append(button)
if current:
rows.append(types.KeyboardButtonRow(current))
if inline_only and is_normal:
raise ValueError('You cannot use non-inline buttons here')
elif is_inline == is_normal and is_normal:
if is_inline and is_normal:
raise ValueError('You cannot mix inline with normal buttons')
elif is_inline:
return types.ReplyInlineMarkup(rows)
# elif is_normal:
return types.ReplyKeyboardMarkup(
rows, resize=resize, single_use=single_use, selective=selective)
rows=rows,
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)

View File

@ -97,7 +97,7 @@ class _ChatAction:
class _ParticipantsIter(RequestIter):
async def _init(self, entity, filter, search, aggressive):
async def _init(self, entity, filter, search):
if isinstance(filter, type):
if filter in (types.ChannelParticipantsBanned,
types.ChannelParticipantsKicked,
@ -122,7 +122,8 @@ class _ParticipantsIter(RequestIter):
self.filter_entity = lambda ent: True
# Only used for channels, but we should always set the attribute
self.requests = []
# Called `requests` even though it's just one for legacy purposes.
self.requests = None
if ty == helpers._EntityType.CHANNEL:
if self.limit <= 0:
@ -133,22 +134,13 @@ class _ParticipantsIter(RequestIter):
raise StopAsyncIteration
self.seen = set()
if aggressive and not filter:
self.requests.extend(functions.channels.GetParticipantsRequest(
channel=entity,
filter=types.ChannelParticipantsSearch(x),
offset=0,
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
hash=0
) for x in (search or string.ascii_lowercase))
else:
self.requests.append(functions.channels.GetParticipantsRequest(
channel=entity,
filter=filter or types.ChannelParticipantsSearch(search),
offset=0,
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
hash=0
))
self.requests = functions.channels.GetParticipantsRequest(
channel=entity,
filter=filter or types.ChannelParticipantsSearch(search),
offset=0,
limit=_MAX_PARTICIPANTS_CHUNK_SIZE,
hash=0
)
elif ty == helpers._EntityType.CHAT:
full = await self.client(
@ -163,7 +155,10 @@ class _ParticipantsIter(RequestIter):
users = {user.id: user for user in full.users}
for participant in full.full_chat.participants.participants:
if isinstance(participant, types.ChannelParticipantBanned):
if isinstance(participant, types.ChannelParticipantLeft):
# See issue #3231 to learn why this is ignored.
continue
elif isinstance(participant, types.ChannelParticipantBanned):
user_id = participant.peer.user_id
else:
user_id = participant.user_id
@ -190,21 +185,14 @@ class _ParticipantsIter(RequestIter):
if not self.requests:
return True
# Only care about the limit for the first request
# (small amount of people, won't be aggressive).
#
# Most people won't care about getting exactly 12,345
# members so it doesn't really matter not to be 100%
# precise with being out of the offset/limit here.
self.requests[0].limit = min(
self.limit - self.requests[0].offset, _MAX_PARTICIPANTS_CHUNK_SIZE)
self.requests.limit = min(self.limit - self.requests.offset, _MAX_PARTICIPANTS_CHUNK_SIZE)
if self.requests[0].offset > self.limit:
if self.requests.offset > self.limit:
return True
if self.total is None:
f = self.requests[0].filter
if len(self.requests) > 1 or (
f = self.requests.filter
if (
not isinstance(f, types.ChannelParticipantsRecent)
and (not isinstance(f, types.ChannelParticipantsSearch) or f.q)
):
@ -212,42 +200,42 @@ class _ParticipantsIter(RequestIter):
# if there's a filter which would reduce the real total number.
# getParticipants is cheaper than getFull.
self.total = (await self.client(functions.channels.GetParticipantsRequest(
channel=self.requests[0].channel,
channel=self.requests.channel,
filter=types.ChannelParticipantsRecent(),
offset=0,
limit=1,
hash=0
))).count
results = await self.client(self.requests)
for i in reversed(range(len(self.requests))):
participants = results[i]
if self.total is None:
# Will only get here if there was one request with a filter that matched all users.
self.total = participants.count
if not participants.users:
self.requests.pop(i)
participants = await self.client(self.requests)
if self.total is None:
# Will only get here if there was one request with a filter that matched all users.
self.total = participants.count
if not participants.users:
self.requests = None
return
self.requests.offset += len(participants.participants)
users = {user.id: user for user in participants.users}
for participant in participants.participants:
if isinstance(participant, types.ChannelParticipantLeft):
# See issue #3231 to learn why this is ignored.
continue
self.requests[i].offset += len(participants.participants)
users = {user.id: user for user in participants.users}
for participant in participants.participants:
if isinstance(participant, types.ChannelParticipantBanned):
if not isinstance(participant.peer, types.PeerUser):
# May have the entire channel banned. See #3105.
continue
user_id = participant.peer.user_id
else:
user_id = participant.user_id
user = users[user_id]
if not self.filter_entity(user) or user.id in self.seen:
elif isinstance(participant, types.ChannelParticipantBanned):
if not isinstance(participant.peer, types.PeerUser):
# May have the entire channel banned. See #3105.
continue
self.seen.add(user_id)
user = users[user_id]
user.participant = participant
self.buffer.append(user)
user_id = participant.peer.user_id
else:
user_id = participant.user_id
user = users[user_id]
if not self.filter_entity(user) or user.id in self.seen:
continue
self.seen.add(user_id)
user = users[user_id]
user.participant = participant
self.buffer.append(user)
class _AdminLogIter(RequestIter):
@ -431,9 +419,6 @@ class ChatMethods:
search (`str`, optional):
Look for participants with this string in name/username.
If ``aggressive is True``, the symbols from this string will
be used.
filter (:tl:`ChannelParticipantsFilter`, optional):
The filter to be used, if you want e.g. only admins
Note that you might not have permissions for some filter.
@ -446,14 +431,11 @@ class ChatMethods:
use :tl:`ChannelParticipantsKicked` instead.
aggressive (`bool`, optional):
Aggressively looks for all participants in the chat.
Does nothing. This is kept for backwards-compatibility.
This is useful for channels since 20 July 2018,
Telegram added a server-side limit where only the
first 200 members can be retrieved. With this flag
set, more than 200 will be often be retrieved.
This has no effect if a ``filter`` is given.
There have been several changes to Telegram's API that limits
the amount of members that can be retrieved, and this was a
hack that no longer works.
Yields
The :tl:`User` objects returned by :tl:`GetParticipantsRequest`
@ -482,8 +464,7 @@ class ChatMethods:
limit,
entity=entity,
filter=filter,
search=search,
aggressive=aggressive
search=search
)
async def get_participants(
@ -947,9 +928,6 @@ class ChatMethods:
"""
entity = await self.get_input_entity(entity)
user = await self.get_input_entity(user)
ty = helpers._entity_type(user)
if ty != helpers._EntityType.USER:
raise ValueError('You must pass a user entity')
perm_names = (
'change_info', 'post_messages', 'edit_messages', 'delete_messages',
@ -987,7 +965,7 @@ class ChatMethods:
is_admin = any(locals()[x] for x in perm_names)
return await self(functions.messages.EditChatAdminRequest(
entity, user, is_admin=is_admin))
entity.chat_id, user, is_admin=is_admin))
else:
raise ValueError(
@ -1136,12 +1114,6 @@ class ChatMethods:
))
user = await self.get_input_entity(user)
ty = helpers._entity_type(user)
if ty != helpers._EntityType.USER:
raise ValueError('You must pass a user entity')
if isinstance(user, types.InputPeerSelf):
raise ValueError('You cannot restrict yourself')
return await self(functions.channels.EditBannedRequest(
channel=entity,
@ -1188,8 +1160,6 @@ class ChatMethods:
"""
entity = await self.get_input_entity(entity)
user = await self.get_input_entity(user)
if helpers._entity_type(user) != helpers._EntityType.USER:
raise ValueError('You must pass a user entity')
ty = helpers._entity_type(entity)
if ty == helpers._EntityType.CHAT:
@ -1259,15 +1229,13 @@ class ChatMethods:
if isinstance(entity, types.Channel):
FullChat = await self(functions.channels.GetFullChannelRequest(entity))
elif isinstance(entity, types.Chat):
FullChat = await self(functions.messages.GetFullChatRequest(entity))
FullChat = await self(functions.messages.GetFullChatRequest(entity.id))
else:
return
return FullChat.chats[0].default_banned_rights
entity = await self.get_input_entity(entity)
user = await self.get_input_entity(user)
if helpers._entity_type(user) != helpers._EntityType.USER:
raise ValueError('You must pass a user entity')
if helpers._entity_type(entity) == helpers._EntityType.CHANNEL:
participant = await self(functions.channels.GetParticipantRequest(
entity,
@ -1276,7 +1244,7 @@ class ChatMethods:
return custom.ParticipantPermissions(participant.participant, False)
elif helpers._entity_type(entity) == helpers._EntityType.CHAT:
chat = await self(functions.messages.GetFullChatRequest(
entity
entity.chat_id
))
if isinstance(user, types.InputPeerSelf):
user = await self.get_me(input_peer=True)

View File

@ -58,6 +58,8 @@ class _DialogsIter(RequestIter):
for x in itertools.chain(r.users, r.chats)
if not isinstance(x, (types.UserEmpty, types.ChatEmpty))}
self.client._mb_entity_cache.extend(r.users, r.chats)
messages = {}
for m in r.messages:
m._finish_init(self.client, entities, None)
@ -82,14 +84,16 @@ class _DialogsIter(RequestIter):
cd = custom.Dialog(self.client, d, entities, message)
if cd.dialog.pts:
self.client._channel_pts[cd.id] = cd.dialog.pts
self.client._message_box.try_set_channel_state(
utils.get_peer_id(d.peer, add_mark=False), cd.dialog.pts)
if not self.ignore_migrated or getattr(
cd.entity, 'migrated_to', None) is None:
self.buffer.append(cd)
if len(r.dialogs) < self.request.limit\
if not self.buffer or len(r.dialogs) < self.request.limit\
or not isinstance(r, types.messages.DialogsSlice):
# Buffer being empty means all returned dialogs were skipped (due to offsets).
# Less than we requested means we reached the end, or
# we didn't get a DialogsSlice which means we got all.
return True

View File

@ -27,21 +27,31 @@ MAX_CHUNK_SIZE = 512 * 1024
# 2021-01-15, users reported that `errors.TimeoutError` can occur while downloading files.
TIMED_OUT_SLEEP = 1
class _CdnRedirect(Exception):
def __init__(self, cdn_redirect=None):
self.cdn_redirect = cdn_redirect
class _DirectDownloadIter(RequestIter):
async def _init(
self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data
):
self, file, dc_id, offset, stride, chunk_size, request_size, file_size, msg_data, cdn_redirect=None):
self.request = functions.upload.GetFileRequest(
file, offset=offset, limit=request_size)
file, offset=offset, limit=request_size)
self._client = self.client
self._cdn_redirect = cdn_redirect
if cdn_redirect is not None:
self.request = functions.upload.GetCdnFileRequest(cdn_redirect.file_token, offset=offset, limit=request_size)
self._client = await self.client._get_cdn_client(cdn_redirect)
self.total = file_size
self._stride = stride
self._chunk_size = chunk_size
self._last_part = None
self._msg_data = msg_data
self._timed_out = False
self._exported = dc_id and self.client.session.dc_id != dc_id
self._exported = dc_id and self._client.session.dc_id != dc_id
if not self._exported:
# The used sender will also change if ``FileMigrateError`` occurs
self._sender = self.client._sender
@ -53,9 +63,12 @@ class _DirectDownloadIter(RequestIter):
config = await self.client(functions.help.GetConfigRequest())
for option in config.dc_options:
if option.ip_address == self.client.session.server_address:
self.client.session.set_dc(
option.id, option.ip_address, option.port)
self.client.session.save()
await utils.maybe_async(
self.client.session.set_dc(
option.id, option.ip_address, option.port
)
)
await utils.maybe_async(self.client.session.save())
break
# TODO Figure out why the session may have the wrong DC ID
@ -73,14 +86,20 @@ class _DirectDownloadIter(RequestIter):
async def _request(self):
try:
result = await self.client._call(self._sender, self.request)
result = await self._client._call(self._sender, self.request)
self._timed_out = False
if isinstance(result, types.upload.FileCdnRedirect):
raise NotImplementedError # TODO Implement
if self.client._mb_entity_cache.self_bot:
raise ValueError('FileCdnRedirect but the GetCdnFileRequest API access for bot users is restricted. Try to change api_id to avoid FileCdnRedirect')
raise _CdnRedirect(result)
if isinstance(result, types.upload.CdnFileReuploadNeeded):
await self.client._call(self.client._sender, functions.upload.ReuploadCdnFileRequest(file_token=self._cdn_redirect.file_token, request_token=result.request_token))
result = await self._client._call(self._sender, self.request)
return result.bytes
else:
return result.bytes
except errors.TimeoutError as e:
except errors.TimedOutError as e:
if self._timed_out:
self.client._log[__name__].warning('Got two timeouts in a row while downloading file')
raise
@ -96,7 +115,7 @@ class _DirectDownloadIter(RequestIter):
self._exported = True
return await self._request()
except errors.FilerefUpgradeNeededError as e:
except (errors.FilerefUpgradeNeededError, errors.FileReferenceExpiredError) as e:
# Only implemented for documents which are the ones that may take that long to download
if not self._msg_data \
or not isinstance(self.request.location, types.InputDocumentFileLocation) \
@ -223,7 +242,8 @@ class DownloadMethods:
The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten.
If file is the type `bytes`, it will be downloaded in-memory
as a bytestring (e.g. ``file=bytes``).
and returned as a bytestring (i.e. ``file=bytes``, without
parentheses or quotes).
download_big (`bool`, optional):
Whether to use the big version of the available photos.
@ -272,7 +292,9 @@ class DownloadMethods:
if isinstance(photo, (types.UserProfilePhoto, types.ChatPhoto)):
dc_id = photo.dc_id
loc = types.InputPeerPhotoFileLocation(
peer=await self.get_input_entity(entity),
# min users can be used to download profile photos
# self.get_input_entity would otherwise not accept those
peer=utils.get_input_peer(entity, check_hash=False),
photo_id=photo.photo_id,
big=download_big
)
@ -331,7 +353,8 @@ class DownloadMethods:
The output file path, directory, or stream-like object.
If the path exists and is a file, it will be overwritten.
If file is the type `bytes`, it will be downloaded in-memory
as a bytestring (e.g. ``file=bytes``).
and returned as a bytestring (i.e. ``file=bytes``, without
parentheses or quotes).
progress_callback (`callable`, optional):
A callback function accepting two parameters:
@ -374,6 +397,9 @@ class DownloadMethods:
path = await message.download_media()
await message.download_media(filename)
# Downloading to memory
blob = await client.download_media(message, bytes)
# Printing download progress
def callback(current, total):
print('Downloaded', current, 'out of', total,
@ -402,7 +428,7 @@ class DownloadMethods:
if isinstance(message.action,
types.MessageActionChatEditPhoto):
media = media.photo
if isinstance(media, types.MessageMediaWebPage):
if isinstance(media.webpage, types.WebPage):
media = media.webpage.document or media.webpage.photo
@ -509,7 +535,9 @@ class DownloadMethods:
dc_id: int = None,
key: bytes = None,
iv: bytes = None,
msg_data: tuple = None) -> typing.Optional[bytes]:
msg_data: tuple = None,
cdn_redirect: types.upload.FileCdnRedirect = None
) -> typing.Optional[bytes]:
if not part_size_kb:
if not file_size:
part_size_kb = 64 # Reasonable default
@ -536,7 +564,7 @@ class DownloadMethods:
try:
async for chunk in self._iter_download(
input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data):
input_location, request_size=part_size, dc_id=dc_id, msg_data=msg_data, cdn_redirect=cdn_redirect):
if iv and key:
chunk = AES.decrypt_ige(chunk, key, iv)
r = f.write(chunk)
@ -554,6 +582,20 @@ class DownloadMethods:
if in_memory:
return f.getvalue()
except _CdnRedirect as e:
self._log[__name__].info('FileCdnRedirect to CDN data center %s', e.cdn_redirect.dc_id)
return await self._download_file(
input_location=input_location,
file=file,
part_size_kb=part_size_kb,
file_size=file_size,
progress_callback=progress_callback,
dc_id=e.cdn_redirect.dc_id,
key=e.cdn_redirect.encryption_key,
iv=e.cdn_redirect.encryption_iv,
msg_data=msg_data,
cdn_redirect=e.cdn_redirect
)
finally:
if isinstance(file, str) or in_memory:
f.close()
@ -675,7 +717,8 @@ class DownloadMethods:
request_size: int = MAX_CHUNK_SIZE,
file_size: int = None,
dc_id: int = None,
msg_data: tuple = None
msg_data: tuple = None,
cdn_redirect: types.upload.FileCdnRedirect = None
):
info = utils._get_file_info(file)
if info.dc_id is not None:
@ -726,6 +769,7 @@ class DownloadMethods:
request_size=request_size,
file_size=file_size,
msg_data=msg_data,
cdn_redirect=cdn_redirect
)
# endregion
@ -734,6 +778,9 @@ class DownloadMethods:
@staticmethod
def _get_thumb(thumbs, thumb):
if not thumbs:
return None
# Seems Telegram has changed the order and put `PhotoStrippedSize`
# last while this is the smallest (layer 116). Ensure we have the
# sizes sorted correctly with a custom function.
@ -876,6 +923,9 @@ class DownloadMethods:
else:
file = self._get_proper_filename(file, 'photo', '.jpg', date=date)
size = self._get_thumb(document.thumbs, thumb)
if not size or isinstance(size, types.PhotoSizeEmpty):
return
if isinstance(size, (types.PhotoCachedSize, types.PhotoStrippedSize)):
return self._download_cached_photo_size(size, file)
@ -916,22 +966,19 @@ class DownloadMethods:
'END:VCARD\n'
).format(f=first_name, l=last_name, p=phone_number).encode('utf-8')
file = cls._get_proper_filename(
file, 'contact', '.vcard',
possible_names=[first_name, phone_number, last_name]
)
if file is bytes:
return result
elif isinstance(file, str):
file = cls._get_proper_filename(
file, 'contact', '.vcard',
possible_names=[first_name, phone_number, last_name]
)
f = open(file, 'wb')
else:
f = file
f = file if hasattr(file, 'write') else open(file, 'wb')
try:
f.write(result)
finally:
# Only close the stream if we opened it
if isinstance(file, str):
if f != file:
f.close()
return file
@ -948,21 +995,20 @@ class DownloadMethods:
)
# TODO Better way to get opened handles of files and auto-close
in_memory = file is bytes
if in_memory:
kind, possible_names = cls._get_kind_and_names(web.attributes)
file = cls._get_proper_filename(
file, kind, utils.get_extension(web),
possible_names=possible_names
)
if file is bytes:
f = io.BytesIO()
elif isinstance(file, str):
kind, possible_names = cls._get_kind_and_names(web.attributes)
file = cls._get_proper_filename(
file, kind, utils.get_extension(web),
possible_names=possible_names
)
f = open(file, 'wb')
else:
elif hasattr(file, 'write'):
f = file
else:
f = open(file, 'wb')
try:
with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession() as session:
# TODO Use progress_callback; get content length from response
# https://github.com/telegramdesktop/tdesktop/blob/c7e773dd9aeba94e2be48c032edc9a78bb50234e/Telegram/SourceFiles/ui/images.cpp#L1318-L1319
async with session.get(web.url) as response:
@ -972,10 +1018,10 @@ class DownloadMethods:
break
f.write(chunk)
finally:
if isinstance(file, str) or file is bytes:
if f != file:
f.close()
return f.getvalue() if in_memory else file
return f.getvalue() if file is bytes else file
@staticmethod
def _get_proper_filename(file, kind, extension,

View File

@ -87,10 +87,15 @@ class MessageParseMethods:
message, msg_entities = parse_mode.parse(message)
if original_message and not message and not msg_entities:
raise ValueError("Failed to parse message")
for i in reversed(range(len(msg_entities))):
e = msg_entities[i]
if isinstance(e, types.MessageEntityTextUrl):
if not e.length:
# 0-length MessageEntity is no longer valid #3884.
# Because the user can provide their own parser (with reasonable 0-length
# entities), strip them here rather than fixing the built-in parsers.
del msg_entities[i]
elif isinstance(e, types.MessageEntityTextUrl):
m = re.match(r'^@|\+|tg://user\?id=(\d+)', e.url)
if m:
user = int(m.group(1)) if m.group(1) else e.url

View File

@ -204,7 +204,24 @@ class _MessagesIter(RequestIter):
message._finish_init(self.client, entities, self.entity)
self.buffer.append(message)
if len(r.messages) < self.request.limit:
# Not a slice (using offset would return the same, with e.g. SearchGlobal).
if isinstance(r, types.messages.Messages):
return True
# Some channels are "buggy" and may return less messages than
# requested (apparently, the messages excluded are, for example,
# "not displayable due to local laws").
#
# This means it's not safe to rely on `len(r.messages) < req.limit` as
# the stop condition. Unfortunately more requests must be made.
#
# However we can still check if the highest ID is equal to or lower
# than the limit, in which case there won't be any more messages
# because the lowest message ID is 1.
#
# We also assume the API will always return, at least, one message if
# there is more to fetch.
if not r.messages or (not self.reverse and r.messages[0].id <= self.request.limit):
return True
# Get the last message that's not empty (in some rare cases
@ -536,7 +553,9 @@ class MessageMethods:
scheduled=scheduled
)
async def get_messages(self: 'TelegramClient', *args, **kwargs) -> 'hints.TotalList':
async def get_messages(
self: 'TelegramClient', *args, **kwargs
) -> typing.Union['hints.TotalList', typing.Optional['types.Message']]:
"""
Same as `iter_messages()`, but returns a
`TotalList <telethon.helpers.TotalList>` instead.
@ -600,7 +619,7 @@ class MessageMethods:
peer=entity,
msg_id=utils.get_message_id(message)
))
m = r.messages[0]
m = min(r.messages, key=lambda msg: msg.id)
chat = next(c for c in r.chats if c.id == m.peer_id.channel_id)
return utils.get_input_peer(chat), m.id
@ -618,12 +637,15 @@ class MessageMethods:
thumb: 'hints.FileLike' = None,
force_document: bool = False,
clear_draft: bool = False,
buttons: 'hints.MarkupLike' = None,
buttons: typing.Optional['hints.MarkupLike'] = None,
silent: bool = None,
background: bool = None,
supports_streaming: bool = False,
schedule: 'hints.DateLike' = None,
comment_to: 'typing.Union[int, types.Message]' = None
comment_to: 'typing.Union[int, types.Message]' = None,
nosound_video: bool = None,
send_as: typing.Optional['hints.EntityLike'] = None,
message_effect_id: typing.Optional[int] = None
) -> 'types.Message':
"""
Sends a message to the specified user, chat or channel.
@ -737,6 +759,25 @@ class MessageMethods:
This parameter takes precedence over ``reply_to``. If there is
no linked chat, `telethon.errors.sgIdInvalidError` is raised.
nosound_video (`bool`, optional):
Only applicable when sending a video file without an audio
track. If set to ``True``, the video will be displayed in
Telegram as a video. If set to ``False``, Telegram will attempt
to display the video as an animated gif. (It may still display
as a video due to other factors.) The value is ignored if set
on non-video files. This is set to ``True`` for albums, as gifs
cannot be sent in albums.
send_as (`entity`):
Unique identifier (int) or username (str) of the chat or channel to send the message as.
You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.
Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,
This setting applies to the current message and will remain effective for future messages unless explicitly changed.
To set this behavior permanently for all messages, use SaveDefaultSendAs.
message_effect_id (`int`, optional):
Unique identifier of the message effect to be added to the message; for private chats only
Returns
The sent `custom.Message <telethon.tl.custom.message.Message>`.
@ -797,6 +838,9 @@ class MessageMethods:
await client.send_message(chat, 'Hi, future!', schedule=timedelta(minutes=5))
"""
if file is not None:
if isinstance(message, types.Message):
formatting_entities = formatting_entities or message.entities
message = message.message
return await self.send_file(
entity, file, caption=message, reply_to=reply_to,
attributes=attributes, parse_mode=parse_mode,
@ -804,12 +848,16 @@ class MessageMethods:
buttons=buttons, clear_draft=clear_draft, silent=silent,
schedule=schedule, supports_streaming=supports_streaming,
formatting_entities=formatting_entities,
comment_to=comment_to, background=background
comment_to=comment_to, background=background,
nosound_video=nosound_video,
send_as=send_as, message_effect_id=message_effect_id
)
entity = await self.get_input_entity(entity)
if comment_to is not None:
entity, reply_to = await self._get_comment_data(entity, comment_to)
else:
reply_to = utils.get_message_id(reply_to)
if isinstance(message, types.Message):
if buttons is None:
@ -831,7 +879,9 @@ class MessageMethods:
reply_to=reply_to,
buttons=markup,
formatting_entities=message.entities,
schedule=schedule
parse_mode=None, # explicitly disable parse_mode to force using even empty formatting_entities
schedule=schedule,
send_as=send_as, message_effect_id=message_effect_id
)
request = functions.messages.SendMessageRequest(
@ -839,13 +889,15 @@ class MessageMethods:
message=message.message or '',
silent=silent,
background=background,
reply_to_msg_id=utils.get_message_id(reply_to),
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
reply_markup=markup,
entities=message.entities,
clear_draft=clear_draft,
no_webpage=not isinstance(
message.media, types.MessageMediaWebPage),
schedule_date=schedule
schedule_date=schedule,
send_as=await self.get_input_entity(send_as) if send_as else None,
effect=message_effect_id
)
message = message.message
else:
@ -861,12 +913,14 @@ class MessageMethods:
message=message,
entities=formatting_entities,
no_webpage=not link_preview,
reply_to_msg_id=utils.get_message_id(reply_to),
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
clear_draft=clear_draft,
silent=silent,
background=background,
reply_markup=self.build_reply_markup(buttons),
schedule_date=schedule
schedule_date=schedule,
send_as=await self.get_input_entity(send_as) if send_as else None,
effect=message_effect_id
)
result = await self(request)
@ -880,7 +934,8 @@ class MessageMethods:
media=result.media,
entities=result.entities,
reply_markup=request.reply_markup,
ttl_period=result.ttl_period
ttl_period=result.ttl_period,
reply_to=request.reply_to
)
message._finish_init(self, {}, entity)
return message
@ -897,7 +952,9 @@ class MessageMethods:
with_my_score: bool = None,
silent: bool = None,
as_album: bool = None,
schedule: 'hints.DateLike' = None
schedule: 'hints.DateLike' = None,
drop_author: bool = None,
drop_media_captions: bool = None,
) -> 'typing.Sequence[types.Message]':
"""
Forwards the given messages to the specified entity.
@ -941,6 +998,12 @@ class MessageMethods:
instead they will be scheduled to be automatically sent
at a later time.
drop_author (`bool`, optional):
Whether to forward messages without quoting the original author.
drop_media_captions (`bool`, optional):
Whether to strip captions from media. Setting this to `True` requires that `drop_author` also be set to `True`.
Returns
The list of forwarded `Message <telethon.tl.custom.message.Message>`,
or a single one if a list wasn't provided as input.
@ -999,7 +1062,7 @@ class MessageMethods:
if isinstance(chunk[0], int):
chat = from_peer
else:
chat = await chunk[0].get_input_chat()
chat = from_peer or await self.get_input_entity(chunk[0].peer_id)
chunk = [m.id for m in chunk]
req = functions.messages.ForwardMessagesRequest(
@ -1009,7 +1072,9 @@ class MessageMethods:
silent=silent,
background=background,
with_my_score=with_my_score,
schedule_date=schedule
schedule_date=schedule,
drop_author=drop_author,
drop_media_captions=drop_media_captions
)
result = await self(req)
sent.extend(self._get_response_message(req, result, entity))
@ -1019,7 +1084,7 @@ class MessageMethods:
async def edit_message(
self: 'TelegramClient',
entity: 'typing.Union[hints.EntityLike, types.Message]',
message: 'hints.MessageLike' = None,
message: 'typing.Union[int, types.Message, types.InputMessageID, str]' = None,
text: str = None,
*,
parse_mode: str = (),
@ -1029,7 +1094,7 @@ class MessageMethods:
file: 'hints.FileLike' = None,
thumb: 'hints.FileLike' = None,
force_document: bool = False,
buttons: 'hints.MarkupLike' = None,
buttons: typing.Optional['hints.MarkupLike'] = None,
supports_streaming: bool = False,
schedule: 'hints.DateLike' = None
) -> 'types.Message':
@ -1045,11 +1110,11 @@ class MessageMethods:
from it, so the next parameter will be assumed to be the
message text.
You may also pass a :tl:`InputBotInlineMessageID`,
You may also pass a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`,
which is the only way to edit messages that were sent
after the user selects an inline query result.
message (`int` | `Message <telethon.tl.custom.message.Message>` | `str`):
message (`int` | `Message <telethon.tl.custom.message.Message>` | :tl:`InputMessageID` | `str`):
The ID of the message (or `Message
<telethon.tl.custom.message.Message>` itself) to be edited.
If the `entity` was a `Message
@ -1117,7 +1182,7 @@ class MessageMethods:
Returns
The edited `Message <telethon.tl.custom.message.Message>`,
unless `entity` was a :tl:`InputBotInlineMessageID` in which
unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which
case this method returns a boolean.
Raises
@ -1143,7 +1208,7 @@ class MessageMethods:
# or
await client.edit_message(message, 'hello!!!')
"""
if isinstance(entity, types.InputBotInlineMessageID):
if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
text = text or message
message = entity
elif isinstance(entity, types.Message):
@ -1159,7 +1224,7 @@ class MessageMethods:
attributes=attributes,
force_document=force_document)
if isinstance(entity, types.InputBotInlineMessageID):
if isinstance(entity, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
request = functions.messages.EditInlineBotMessageRequest(
id=entity,
message=text,
@ -1277,7 +1342,8 @@ class MessageMethods:
message: 'typing.Union[hints.MessageIDLike, typing.Sequence[hints.MessageIDLike]]' = None,
*,
max_id: int = None,
clear_mentions: bool = False) -> bool:
clear_mentions: bool = False,
clear_reactions: bool = False) -> bool:
"""
Marks messages as read and optionally clears mentions.
@ -1311,6 +1377,13 @@ class MessageMethods:
If no message is provided, this will be the only action
taken.
clear_reactions (`bool`):
Whether the reactions badge should be cleared (so that
there are no more reaction notifications) or not for the given entity.
If no message is provided, this will be the only action
taken.
Example
.. code-block:: python
@ -1333,6 +1406,10 @@ class MessageMethods:
entity = await self.get_input_entity(entity)
if clear_mentions:
await self(functions.messages.ReadMentionsRequest(entity))
if max_id is None and not clear_reactions:
return True
if clear_reactions:
await self(functions.messages.ReadReactionsRequest(entity))
if max_id is None:
return True

View File

@ -1,4 +1,5 @@
import abc
import inspect
import re
import asyncio
import collections
@ -6,16 +7,17 @@ import logging
import platform
import time
import typing
import datetime
import pathlib
from .. import version, helpers, __name__ as __base_name__
from .. import utils, version, helpers, __name__ as __base_name__
from ..crypto import rsa
from ..entitycache import EntityCache
from ..extensions import markdown
from ..network import MTProtoSender, Connection, ConnectionTcpFull, TcpMTProxy
from ..sessions import Session, SQLiteSession, MemorySession
from ..statecache import StateCache
from ..tl import functions, types
from ..tl.alltlobjects import LAYER
from .._updates import MessageBox, EntityCache as MbEntityCache, SessionState, ChannelState, Entity, EntityType
DEFAULT_DC_ID = 2
DEFAULT_IPV4_IP = '149.154.167.51'
@ -191,7 +193,7 @@ class TelegramBaseClient(abc.ABC):
Defaults to `lang_code`.
loop (`asyncio.AbstractEventLoop`, optional):
Asyncio event loop to use. Defaults to `asyncio.get_event_loop()`.
Asyncio event loop to use. Defaults to `asyncio.get_running_loop()`.
This argument is ignored.
base_logger (`str` | `logging.Logger`, optional):
@ -208,6 +210,20 @@ class TelegramBaseClient(abc.ABC):
so event handlers, conversations, and QR login will not work.
However, certain scripts don't need updates, so this will reduce
the amount of bandwidth used.
entity_cache_limit (`int`, optional):
How many users, chats and channels to keep in the in-memory cache
at most. This limit is checked against when processing updates.
When this limit is reached or exceeded, all entities that are not
required for update handling will be flushed to the session file.
Note that this implies that there is a lower bound to the amount
of entities that must be kept in memory.
Setting this limit too low will cause the library to attempt to
flush entities to the session file even if no entities can be
removed from the in-memory cache, which will degrade performance.
"""
# Current TelegramClient version
@ -221,7 +237,7 @@ class TelegramBaseClient(abc.ABC):
def __init__(
self: 'TelegramClient',
session: 'typing.Union[str, Session]',
session: 'typing.Union[str, pathlib.Path, Session]',
api_id: int,
api_hash: str,
*,
@ -244,7 +260,9 @@ class TelegramBaseClient(abc.ABC):
system_lang_code: str = 'en',
loop: asyncio.AbstractEventLoop = None,
base_logger: typing.Union[str, logging.Logger] = None,
receive_updates: bool = True
receive_updates: bool = True,
catch_up: bool = False,
entity_cache_limit: int = 5000
):
if not api_id or not api_hash:
raise ValueError(
@ -268,9 +286,9 @@ class TelegramBaseClient(abc.ABC):
self._log = _Loggers()
# Determine what session object we have
if isinstance(session, str) or session is None:
if isinstance(session, (str, pathlib.Path)):
try:
session = SQLiteSession(session)
session = SQLiteSession(str(session))
except ImportError:
import warnings
warnings.warn(
@ -281,24 +299,17 @@ class TelegramBaseClient(abc.ABC):
'you use another session storage'
)
session = MemorySession()
elif session is None:
session = MemorySession()
elif not isinstance(session, Session):
raise TypeError(
'The given session must be a str or a Session instance.'
)
# ':' in session.server_address is True if it's an IPv6 address
if (not session.server_address or
(':' in session.server_address) != use_ipv6):
session.set_dc(
DEFAULT_DC_ID,
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
DEFAULT_PORT
)
self.flood_sleep_threshold = flood_sleep_threshold
# TODO Use AsyncClassWrapper(session)
# ChatGetter and SenderGetter can use the in-memory _entity_cache
# ChatGetter and SenderGetter can use the in-memory _mb_entity_cache
# to avoid network access and the need for await in session files.
#
# The session files only wants the entities to persist
@ -306,7 +317,6 @@ class TelegramBaseClient(abc.ABC):
# TODO Session should probably return all cached
# info of entities, not just the input versions
self.session = session
self._entity_cache = EntityCache()
self.api_id = int(api_id)
self.api_hash = api_hash
@ -377,46 +387,27 @@ class TelegramBaseClient(abc.ABC):
proxy=init_proxy
)
self._sender = MTProtoSender(
self.session.auth_key,
loggers=self._log,
retries=self._connection_retries,
delay=self._retry_delay,
auto_reconnect=self._auto_reconnect,
connect_timeout=self._timeout,
auth_key_callback=self._auth_key_callback,
update_callback=self._handle_update,
auto_reconnect_callback=self._handle_auto_reconnect
)
# Remember flood-waited requests to avoid making them again
self._flood_waited_requests = {}
# Cache ``{dc_id: (_ExportState, MTProtoSender)}`` for all borrowed senders
self._borrowed_senders = {}
self._borrow_sender_lock = asyncio.Lock()
self._exported_sessions = {}
self._loop = None # only used as a sanity check
self._updates_error = None
self._updates_handle = None
self._keepalive_handle = None
self._last_request = time.time()
self._channel_pts = {}
self._no_updates = not receive_updates
if sequential_updates:
self._updates_queue = asyncio.Queue()
self._dispatching_updates_queue = asyncio.Event()
else:
# Use a set of pending instead of a queue so we can properly
# terminate all pending updates on disconnect.
self._updates_queue = set()
self._dispatching_updates_queue = None
# Used for non-sequential updates, in order to terminate all pending tasks on disconnect.
self._sequential_updates = sequential_updates
self._event_handler_tasks = set()
self._authorized = None # None = unknown, False = no, True = yes
# Update state (for catching up after a disconnection)
# TODO Get state from channels too
self._state_cache = StateCache(
self.session.get_update_state(0), self._log)
# Some further state for subclasses
self._event_builders = []
@ -441,13 +432,29 @@ class TelegramBaseClient(abc.ABC):
self._phone = None
self._tos = None
# Sometimes we need to know who we are, cache the self peer
self._self_input_peer = None
self._bot = None
# A place to store if channels are a megagroup or not (see `edit_admin`)
self._megagroup_cache = {}
# This is backported from v2 in a very ad-hoc way just to get proper update handling
self._catch_up = catch_up
self._updates_queue = asyncio.Queue()
self._message_box = MessageBox(self._log['messagebox'])
self._mb_entity_cache = MbEntityCache() # required for proper update handling (to know when to getDifference)
self._entity_cache_limit = entity_cache_limit
self._sender = MTProtoSender(
self.session.auth_key,
loggers=self._log,
retries=self._connection_retries,
delay=self._retry_delay,
auto_reconnect=self._auto_reconnect,
connect_timeout=self._timeout,
auth_key_callback=self._auth_key_callback,
updates_queue=self._updates_queue,
auto_reconnect_callback=self._handle_auto_reconnect
)
# endregion
# region Properties
@ -469,7 +476,7 @@ class TelegramBaseClient(abc.ABC):
# Join the task (wait for it to complete)
await task
"""
return asyncio.get_event_loop()
return helpers.get_running_loop()
@property
def disconnected(self: 'TelegramClient') -> asyncio.Future:
@ -522,6 +529,26 @@ class TelegramBaseClient(abc.ABC):
except OSError:
print('Failed to connect')
"""
if self.session is None:
raise ValueError('TelegramClient instance cannot be reused after logging out')
if self._loop is None:
self._loop = helpers.get_running_loop()
elif self._loop != helpers.get_running_loop():
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
# ':' in session.server_address is True if it's an IPv6 address
if (not self.session.server_address or
(':' in self.session.server_address) != self._use_ipv6):
await utils.maybe_async(
self.session.set_dc(
DEFAULT_DC_ID,
DEFAULT_IPV6_IP if self._use_ipv6 else DEFAULT_IPV4_IP,
DEFAULT_PORT
)
)
await utils.maybe_async(self.session.save())
if not await self._sender.connect(self._connection(
self.session.server_address,
self.session.port,
@ -534,15 +561,54 @@ class TelegramBaseClient(abc.ABC):
return
self.session.auth_key = self._sender.auth_key
self.session.save()
await utils.maybe_async(self.session.save())
try:
# See comment when saving entities to understand this hack
self_entity = await utils.maybe_async(self.session.get_input_entity(0))
self_id = self_entity.access_hash
self_user = await utils.maybe_async(self.session.get_input_entity(self_id))
self._mb_entity_cache.set_self_user(self_id, None, self_user.access_hash)
except ValueError:
pass
if self._catch_up:
ss = SessionState(0, 0, False, 0, 0, 0, 0, None)
cs = []
update_states = await utils.maybe_async(self.session.get_update_states())
for entity_id, state in update_states:
if entity_id == 0:
# TODO current session doesn't store self-user info but adding that is breaking on downstream session impls
ss = SessionState(0, 0, False, state.pts, state.qts, int(state.date.timestamp()), state.seq, None)
else:
cs.append(ChannelState(entity_id, state.pts))
self._message_box.load(ss, cs)
for state in cs:
try:
entity = await utils.maybe_async(self.session.get_input_entity(state.channel_id))
except ValueError:
self._log[__name__].warning(
'No access_hash in cache for channel %s, will not catch up', state.channel_id)
else:
self._mb_entity_cache.put(Entity(EntityType.CHANNEL, entity.channel_id, entity.access_hash))
self._init_request.query = functions.help.GetConfigRequest()
await self._sender.send(functions.InvokeWithLayerRequest(
LAYER, self._init_request
))
req = self._init_request
if self._no_updates:
req = functions.InvokeWithoutUpdatesRequest(req)
await self._sender.send(functions.InvokeWithLayerRequest(LAYER, req))
if self._message_box.is_empty():
me = await self.get_me()
if me:
await self._on_login(me) # also calls GetState to initialize the MessageBox
self._updates_handle = self.loop.create_task(self._update_loop())
self._keepalive_handle = self.loop.create_task(self._keepalive_loop())
def is_connected(self: 'TelegramClient') -> bool:
"""
@ -567,6 +633,12 @@ class TelegramBaseClient(abc.ABC):
coroutine that you should await on your own code; otherwise
the loop is ran until said coroutine completes.
Event handlers which are currently running will be cancelled before
this function returns (in order to properly clean-up their tasks).
In particular, this means that using ``disconnect`` in a handler
will cause code after the ``disconnect`` to never run. If this is
needed, consider spawning a separate task to do the remaining work.
Example
.. code-block:: python
@ -574,7 +646,11 @@ class TelegramBaseClient(abc.ABC):
await client.disconnect()
"""
if self.loop.is_running():
return self._disconnect_coro()
# Disconnect may be called from an event handler, which would
# cancel itself during itself and never actually complete the
# disconnection. Shield the task to prevent disconnect itself
# from being cancelled. See issue #3942 for more details.
return asyncio.shield(self.loop.create_task(self._disconnect_coro()))
else:
try:
self.loop.run_until_complete(self._disconnect_coro())
@ -615,7 +691,32 @@ class TelegramBaseClient(abc.ABC):
else:
connection._proxy = proxy
async def _save_states_and_entities(self: 'TelegramClient'):
# As a hack to not need to change the session files, save ourselves with ``id=0`` and ``access_hash`` of our ``id``.
# This way it is possible to determine our own ID by querying for 0. However, whether we're a bot is not saved.
# Piggy-back on an arbitrary TL type with users and chats so the session can understand to read the entities.
# It doesn't matter if we put users in the list of chats.
if self._mb_entity_cache.self_id:
await utils.maybe_async(
self.session.process_entities(
types.contacts.ResolvedPeer(None, [types.InputPeerUser(0, self._mb_entity_cache.self_id)], [])
)
)
ss, cs = self._message_box.session_state()
await utils.maybe_async(self.session.set_update_state(0, types.updates.State(**ss, unread_count=0)))
now = datetime.datetime.now() # any datetime works; channels don't need it
for channel_id, pts in cs.items():
await utils.maybe_async(
self.session.set_update_state(
channel_id, types.updates.State(pts, 0, now, 0, unread_count=0)
)
)
async def _disconnect_coro(self: 'TelegramClient'):
if self.session is None:
return # already logged out and disconnected
await self._disconnect()
# Also clean-up all exported senders because we're done with them
@ -635,24 +736,16 @@ class TelegramBaseClient(abc.ABC):
# trio's nurseries would handle this for us, but this is asyncio.
# All tasks spawned in the background should properly be terminated.
if self._dispatching_updates_queue is None and self._updates_queue:
for task in self._updates_queue:
if self._event_handler_tasks:
for task in self._event_handler_tasks:
task.cancel()
await asyncio.wait(self._updates_queue)
self._updates_queue.clear()
await asyncio.wait(self._event_handler_tasks)
self._event_handler_tasks.clear()
pts, date = self._state_cache[None]
if pts and date:
self.session.set_update_state(0, types.updates.State(
pts=pts,
qts=0,
date=date,
seq=0,
unread_count=0
))
await self._save_states_and_entities()
self.session.close()
await utils.maybe_async(self.session.close())
async def _disconnect(self: 'TelegramClient'):
"""
@ -663,7 +756,8 @@ class TelegramBaseClient(abc.ABC):
"""
await self._sender.disconnect()
await helpers._cancel(self._log[__name__],
updates_handle=self._updates_handle)
updates_handle=self._updates_handle,
keepalive_handle=self._keepalive_handle)
async def _switch_dc(self: 'TelegramClient', new_dc):
"""
@ -672,22 +766,22 @@ class TelegramBaseClient(abc.ABC):
self._log[__name__].info('Reconnecting to new data center %s', new_dc)
dc = await self._get_dc(new_dc)
self.session.set_dc(dc.id, dc.ip_address, dc.port)
await utils.maybe_async(self.session.set_dc(dc.id, dc.ip_address, dc.port))
# auth_key's are associated with a server, which has now changed
# so it's not valid anymore. Set to None to force recreating it.
self._sender.auth_key.key = None
self.session.auth_key = None
self.session.save()
await utils.maybe_async(self.session.save())
await self._disconnect()
return await self.connect()
def _auth_key_callback(self: 'TelegramClient', auth_key):
async def _auth_key_callback(self: 'TelegramClient', auth_key):
"""
Callback from the sender whenever it needed to generate a
new authorization key. This means we are not authorized.
"""
self.session.auth_key = auth_key
self.session.save()
await utils.maybe_async(self.session.save())
# endregion
@ -702,7 +796,8 @@ class TelegramBaseClient(abc.ABC):
if cdn and not self._cdn_config:
cls._cdn_config = await self(functions.help.GetCdnConfigRequest())
for pk in cls._cdn_config.public_keys:
rsa.add_key(pk.public_key)
if pk.dc_id == dc_id:
rsa.add_key(pk.public_key, old=False)
try:
return next(
@ -715,10 +810,13 @@ class TelegramBaseClient(abc.ABC):
'Failed to get DC %s (cdn = %s) with use_ipv6 = %s; retrying ignoring IPv6 check',
dc_id, cdn, self._use_ipv6
)
return next(
dc for dc in cls._config.dc_options
if dc.id == dc_id and bool(dc.cdn) == cdn
)
try:
return next(
dc for dc in cls._config.dc_options
if dc.id == dc_id and bool(dc.cdn) == cdn
)
except StopIteration:
raise ValueError(f'Failed to get DC {dc_id} (cdn = {cdn})')
async def _create_exported_sender(self: 'TelegramClient', dc_id):
"""
@ -806,28 +904,30 @@ class TelegramBaseClient(abc.ABC):
async def _get_cdn_client(self: 'TelegramClient', cdn_redirect):
"""Similar to ._borrow_exported_client, but for CDNs"""
# TODO Implement
raise NotImplementedError
session = self._exported_sessions.get(cdn_redirect.dc_id)
if not session:
dc = await self._get_dc(cdn_redirect.dc_id, cdn=True)
session = self.session.clone()
await session.set_dc(dc.id, dc.ip_address, dc.port)
session = await utils.maybe_async(self.session.clone())
await utils.maybe_async(session.set_dc(dc.id, dc.ip_address, dc.port))
self._exported_sessions[cdn_redirect.dc_id] = session
self._log[__name__].info('Creating new CDN client')
client = TelegramBaseClient(
client = self.__class__(
session, self.api_id, self.api_hash,
proxy=self._sender.connection.conn.proxy,
timeout=self._sender.connection.get_timeout()
proxy=self._proxy,
timeout=self._timeout,
loop=self.loop
)
# This will make use of the new RSA keys for this specific CDN.
#
# We won't be calling GetConfigRequest because it's only called
# when needed by ._get_dc, and also it's static so it's likely
# set already. Avoid invoking non-CDN methods by not syncing updates.
client.connect(_sync_updates=False)
session.auth_key = self._sender.auth_key
await client._sender.connect(self._connection(
session.server_address,
session.port,
session.dc_id,
loggers=self._log,
proxy=self._proxy,
local_addr=self._local_addr
))
return client
# endregion
@ -860,10 +960,6 @@ class TelegramBaseClient(abc.ABC):
"""
raise NotImplementedError
@abc.abstractmethod
def _handle_update(self: 'TelegramClient', update):
raise NotImplementedError
@abc.abstractmethod
def _update_loop(self: 'TelegramClient'):
raise NotImplementedError

View File

@ -7,10 +7,17 @@ import time
import traceback
import typing
import logging
import warnings
from collections import deque
import sqlite3
from .. import events, utils, errors
from ..events.common import EventBuilder, EventCommon
from ..tl import types, functions
from .._updates import GapError, PrematureEndReason
from ..helpers import get_running_loop
from ..version import __version__
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
@ -26,7 +33,10 @@ class UpdateMethods:
try:
# Make a high-level request to notify that we want updates
await self(functions.updates.GetStateRequest())
return await self.disconnected
result = await self.disconnected
if self._updates_error is not None:
raise self._updates_error
return result
except KeyboardInterrupt:
pass
finally:
@ -49,6 +59,8 @@ class UpdateMethods:
It also notifies Telegram that we want to receive updates
as described in https://core.telegram.org/api/updates.
If an unexpected error occurs during update handling,
the client will disconnect and said error will be raised.
Manual disconnections can be made by calling `disconnect()
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
@ -237,106 +249,240 @@ class UpdateMethods:
await client.catch_up()
"""
pts, date = self._state_cache[None]
if not pts:
return
self.session.catching_up = True
try:
while True:
d = await self(functions.updates.GetDifferenceRequest(
pts, date, 0
))
if isinstance(d, (types.updates.DifferenceSlice,
types.updates.Difference)):
if isinstance(d, types.updates.Difference):
state = d.state
else:
state = d.intermediate_state
pts, date = state.pts, state.date
self._handle_update(types.Updates(
users=d.users,
chats=d.chats,
date=state.date,
seq=state.seq,
updates=d.other_updates + [
types.UpdateNewMessage(m, 0, 0)
for m in d.new_messages
]
))
# TODO Implement upper limit (max_pts)
# We don't want to fetch updates we already know about.
#
# We may still get duplicates because the Difference
# contains a lot of updates and presumably only has
# the state for the last one, but at least we don't
# unnecessarily fetch too many.
#
# updates.getDifference's pts_total_limit seems to mean
# "how many pts is the request allowed to return", and
# if there is more than that, it returns "too long" (so
# there would be duplicate updates since we know about
# some). This can be used to detect collisions (i.e.
# it would return an update we have already seen).
else:
if isinstance(d, types.updates.DifferenceEmpty):
date = d.date
elif isinstance(d, types.updates.DifferenceTooLong):
pts = d.pts
break
except (ConnectionError, asyncio.CancelledError):
pass
finally:
# TODO Save new pts to session
self._state_cache._pts_date = (pts, date)
self.session.catching_up = False
await self._updates_queue.put(types.UpdatesTooLong())
# endregion
# region Private methods
# It is important to not make _handle_update async because we rely on
# the order that the updates arrive in to update the pts and date to
# be always-increasing. There is also no need to make this async.
def _handle_update(self: 'TelegramClient', update):
self.session.process_entities(update)
self._entity_cache.add(update)
if isinstance(update, (types.Updates, types.UpdatesCombined)):
entities = {utils.get_peer_id(x): x for x in
itertools.chain(update.users, update.chats)}
for u in update.updates:
self._process_update(u, update.updates, entities=entities)
elif isinstance(update, types.UpdateShort):
self._process_update(update.update, None)
else:
self._process_update(update, None)
self._state_cache.update(update)
def _process_update(self: 'TelegramClient', update, others, entities=None):
update._entities = entities or {}
# This part is somewhat hot so we don't bother patching
# update with channel ID/its state. Instead we just pass
# arguments which is faster.
channel_id = self._state_cache.get_channel_id(update)
args = (update, others, channel_id, self._state_cache[channel_id])
if self._dispatching_updates_queue is None:
task = self.loop.create_task(self._dispatch_update(*args))
self._updates_queue.add(task)
task.add_done_callback(lambda _: self._updates_queue.discard(task))
else:
self._updates_queue.put_nowait(args)
if not self._dispatching_updates_queue.is_set():
self._dispatching_updates_queue.set()
self.loop.create_task(self._dispatch_queue_updates())
self._state_cache.update(update)
async def _update_loop(self: 'TelegramClient'):
# If the MessageBox is not empty, the account had to be logged-in to fill in its state.
# This flag is used to propagate the "you got logged-out" error up (but getting logged-out
# can only happen if it was once logged-in).
was_once_logged_in = self._authorized is True or not self._message_box.is_empty()
self._updates_error = None
try:
if self._catch_up:
# User wants to catch up as soon as the client is up and running,
# so this is the best place to do it.
await self.catch_up()
updates_to_dispatch = deque()
while self.is_connected():
if updates_to_dispatch:
if self._sequential_updates:
await self._dispatch_update(updates_to_dispatch.popleft())
else:
while updates_to_dispatch:
# TODO if _dispatch_update fails for whatever reason, it's not logged! this should be fixed
task = self.loop.create_task(self._dispatch_update(updates_to_dispatch.popleft()))
self._event_handler_tasks.add(task)
task.add_done_callback(self._event_handler_tasks.discard)
continue
if len(self._mb_entity_cache) >= self._entity_cache_limit:
self._log[__name__].info(
'In-memory entity cache limit reached (%s/%s), flushing to session',
len(self._mb_entity_cache),
self._entity_cache_limit
)
await self._save_states_and_entities()
self._mb_entity_cache.retain(lambda id: id == self._mb_entity_cache.self_id or id in self._message_box.map)
if len(self._mb_entity_cache) >= self._entity_cache_limit:
warnings.warn('in-memory entities exceed entity_cache_limit after flushing; consider setting a larger limit')
self._log[__name__].info(
'In-memory entity cache at %s/%s after flushing to session',
len(self._mb_entity_cache),
self._entity_cache_limit
)
get_diff = self._message_box.get_difference()
if get_diff:
self._log[__name__].debug('Getting difference for account updates')
try:
diff = await self(get_diff)
except (
errors.ServerError,
errors.TimedOutError,
errors.FloodWaitError,
ValueError
) as e:
# Telegram is having issues
self._log[__name__].info('Cannot get difference since Telegram is having issues: %s', type(e).__name__)
self._message_box.end_difference()
continue
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
# Not logged in or broken authorization key, can't get difference
self._log[__name__].info('Cannot get difference since the account is not logged in: %s', type(e).__name__)
self._message_box.end_difference()
if was_once_logged_in:
self._updates_error = e
await self.disconnect()
break
continue
except (errors.TypeNotFoundError, sqlite3.OperationalError) as e:
# User is likely doing weird things with their account or session and Telegram gets confused as to what layer they use
self._log[__name__].warning('Cannot get difference since the account is likely misusing the session: %s', e)
self._message_box.end_difference()
self._updates_error = e
await self.disconnect()
break
except OSError as e:
# Network is likely down, but it's unclear for how long.
# If disconnect is called this task will be cancelled along with the sleep.
# If disconnect is not called, getting difference should be retried after a few seconds.
self._log[__name__].info('Cannot get difference since the network is down: %s: %s', type(e).__name__, e)
await asyncio.sleep(5)
continue
updates, users, chats = self._message_box.apply_difference(diff, self._mb_entity_cache)
if updates:
self._log[__name__].info('Got difference for account updates')
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
continue
get_diff = self._message_box.get_channel_difference(self._mb_entity_cache)
if get_diff:
self._log[__name__].debug('Getting difference for channel %s updates', get_diff.channel.channel_id)
try:
diff = await self(get_diff)
except (errors.UnauthorizedError, errors.AuthKeyError) as e:
# Not logged in or broken authorization key, can't get difference
self._log[__name__].warning(
'Cannot get difference for channel %s since the account is not logged in: %s',
get_diff.channel.channel_id, type(e).__name__
)
self._message_box.end_channel_difference(
get_diff,
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
self._mb_entity_cache
)
if was_once_logged_in:
self._updates_error = e
await self.disconnect()
break
continue
except (errors.TypeNotFoundError, sqlite3.OperationalError) as e:
self._log[__name__].warning(
'Cannot get difference for channel %s since the account is likely misusing the session: %s',
get_diff.channel.channel_id, e
)
self._message_box.end_channel_difference(
get_diff,
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
self._mb_entity_cache
)
self._updates_error = e
await self.disconnect()
break
except (
errors.PersistentTimestampOutdatedError,
errors.PersistentTimestampInvalidError,
errors.ServerError,
errors.TimedOutError,
errors.FloodWaitError,
ValueError
) as e:
# According to Telegram's docs:
# "Channel internal replication issues, try again later (treat this like an RPC_CALL_FAIL)."
# We can treat this as "empty difference" and not update the local pts.
# Then this same call will be retried when another gap is detected or timeout expires.
#
# Another option would be to literally treat this like an RPC_CALL_FAIL and retry after a few
# seconds, but if Telegram is having issues it's probably best to wait for it to send another
# update (hinting it may be okay now) and retry then.
#
# This is a bit hacky because MessageBox doesn't really have a way to "not update" the pts.
# Instead we manually extract the previously-known pts and use that.
#
# For PersistentTimestampInvalidError:
# Somehow our pts is either too new or the server does not know about this.
# We treat this as PersistentTimestampOutdatedError for now.
# TODO investigate why/when this happens and if this is the proper solution
self._log[__name__].warning(
'Getting difference for channel updates %s caused %s;'
' ending getting difference prematurely until server issues are resolved',
get_diff.channel.channel_id, type(e).__name__
)
self._message_box.end_channel_difference(
get_diff,
PrematureEndReason.TEMPORARY_SERVER_ISSUES,
self._mb_entity_cache
)
continue
except (errors.ChannelPrivateError, errors.ChannelInvalidError):
# Timeout triggered a get difference, but we have been banned in the channel since then.
# Because we can no longer fetch updates from this channel, we should stop keeping track
# of it entirely.
self._log[__name__].info(
'Account is now banned in %d so we can no longer fetch updates from it',
get_diff.channel.channel_id
)
self._message_box.end_channel_difference(
get_diff,
PrematureEndReason.BANNED,
self._mb_entity_cache
)
continue
except OSError as e:
self._log[__name__].info(
'Cannot get difference for channel %d since the network is down: %s: %s',
get_diff.channel.channel_id, type(e).__name__, e
)
await asyncio.sleep(5)
continue
updates, users, chats = self._message_box.apply_channel_difference(get_diff, diff, self._mb_entity_cache)
if updates:
self._log[__name__].info('Got difference for channel %d updates', get_diff.channel.channel_id)
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(updates, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
continue
deadline = self._message_box.check_deadlines()
deadline_delay = deadline - get_running_loop().time()
if deadline_delay > 0:
# Don't bother sleeping and timing out if the delay is already 0 (pollutes the logs).
try:
updates = await asyncio.wait_for(self._updates_queue.get(), deadline_delay)
except asyncio.TimeoutError:
self._log[__name__].debug('Timeout waiting for updates expired')
continue
else:
continue
processed = []
try:
users, chats = self._message_box.process_updates(updates, self._mb_entity_cache, processed)
except GapError:
continue # get(_channel)_difference will start returning requests
_preprocess_updates = await utils.maybe_async(self._preprocess_updates(processed, users, chats))
updates_to_dispatch.extend(_preprocess_updates)
except asyncio.CancelledError:
pass
except Exception as e:
self._log[__name__].exception(f'Fatal error handling updates (this is a bug in Telethon v{__version__}, please report it)')
self._updates_error = e
await self.disconnect()
async def _preprocess_updates(self, updates, users, chats):
self._mb_entity_cache.extend(users, chats)
await utils.maybe_async(self.session.process_entities(types.contacts.ResolvedPeer(None, users, chats)))
entities = {utils.get_peer_id(x): x
for x in itertools.chain(users, chats)}
for u in updates:
u._entities = entities
return updates
async def _keepalive_loop(self: 'TelegramClient'):
# Pings' ID don't really need to be secure, just "random"
rnd = lambda: random.randrange(-2**63, 2**63)
while self.is_connected():
@ -372,57 +518,18 @@ class UpdateMethods:
# inserted because this is a rather expensive operation
# (default's sqlite3 takes ~0.1s to commit changes). Do
# it every minute instead. No-op if there's nothing new.
self.session.save()
await self._save_states_and_entities()
# We need to send some content-related request at least hourly
# for Telegram to keep delivering updates, otherwise they will
# just stop even if we're connected. Do so every 30 minutes.
#
# TODO Call getDifference instead since it's more relevant
if time.time() - self._last_request > 30 * 60:
if not await self.is_user_authorized():
# What can be the user doing for so
# long without being logged in...?
continue
await utils.maybe_async(self.session.save())
try:
await self(functions.updates.GetStateRequest())
except (ConnectionError, asyncio.CancelledError):
return
async def _dispatch_update(self: 'TelegramClient', update):
# TODO only used for AlbumHack, and MessageBox is not really designed for this
others = None
async def _dispatch_queue_updates(self: 'TelegramClient'):
while not self._updates_queue.empty():
await self._dispatch_update(*self._updates_queue.get_nowait())
self._dispatching_updates_queue.clear()
async def _dispatch_update(self: 'TelegramClient', update, others, channel_id, pts_date):
if not self._entity_cache.ensure_cached(update):
# We could add a lock to not fetch the same pts twice if we are
# already fetching it. However this does not happen in practice,
# which makes sense, because different updates have different pts.
if self._state_cache.update(update, check_only=True):
# If the update doesn't have pts, fetching won't do anything.
# For example, UpdateUserStatus or UpdateChatUserTyping.
try:
await self._get_difference(update, channel_id, pts_date)
except OSError:
pass # We were disconnected, that's okay
except errors.RPCError:
# There's a high chance the request fails because we lack
# the channel. Because these "happen sporadically" (#1428)
# we should be okay (no flood waits) even if more occur.
pass
except ValueError:
# There is a chance that GetFullChannelRequest and GetDifferenceRequest
# inside the _get_difference() function will end up with
# ValueError("Request was unsuccessful N time(s)") for whatever reasons.
pass
if not self._self_input_peer:
if not self._mb_entity_cache.self_id:
# Some updates require our own ID, so we must make sure
# that the event builder has offline access to it. Calling
# `get_me()` will cache it under `self._self_input_peer`.
# `get_me()` will cache it under `self._mb_entity_cache`.
#
# It will return `None` if we haven't logged in yet which is
# fine, we will just retry next time anyway.
@ -523,67 +630,6 @@ class UpdateMethods:
name = getattr(callback, '__name__', repr(callback))
self._log[__name__].exception('Unhandled exception on %s', name)
async def _get_difference(self: 'TelegramClient', update, channel_id, pts_date):
"""
Get the difference for this `channel_id` if any, then load entities.
Calls :tl:`updates.getDifference`, which fills the entities cache
(always done by `__call__`) and lets us know about the full entities.
"""
# Fetch since the last known pts/date before this update arrived,
# in order to fetch this update at full, including its entities.
self._log[__name__].debug('Getting difference for entities '
'for %r', update.__class__)
if channel_id:
# There are reports where we somehow call get channel difference
# with `InputPeerEmpty`. Check our assumptions to better debug
# this when it happens.
assert isinstance(channel_id, int), 'channel_id was {}, not int in {}'.format(type(channel_id), update)
try:
# Wrap the ID inside a peer to ensure we get a channel back.
where = await self.get_input_entity(types.PeerChannel(channel_id))
except ValueError:
# There's a high chance that this fails, since
# we are getting the difference to fetch entities.
return
if not pts_date:
# First-time, can't get difference. Get pts instead.
result = await self(functions.channels.GetFullChannelRequest(
utils.get_input_channel(where)
))
self._state_cache[channel_id] = result.full_chat.pts
return
result = await self(functions.updates.GetChannelDifferenceRequest(
channel=where,
filter=types.ChannelMessagesFilterEmpty(),
pts=pts_date, # just pts
limit=100,
force=True
))
else:
if not pts_date[0]:
# First-time, can't get difference. Get pts instead.
result = await self(functions.updates.GetStateRequest())
self._state_cache[None] = result.pts, result.date
return
result = await self(functions.updates.GetDifferenceRequest(
pts=pts_date[0],
date=pts_date[1],
qts=0
))
if isinstance(result, (types.updates.Difference,
types.updates.DifferenceSlice,
types.updates.ChannelDifference,
types.updates.ChannelDifferenceTooLong)):
update._entities.update({
utils.get_peer_id(x): x for x in
itertools.chain(result.users, result.chats)
})
async def _handle_auto_reconnect(self: 'TelegramClient'):
# TODO Catch-up
# For now we make a high-level request to let Telegram

View File

@ -18,7 +18,6 @@ try:
except ImportError:
PIL = None
if typing.TYPE_CHECKING:
from .telegramclient import TelegramClient
@ -36,7 +35,7 @@ class _CacheType:
def _resize_photo_if_needed(
file, is_image, width=1280, height=1280, background=(255, 255, 255)):
file, is_image, width=2560, height=2560, background=(255, 255, 255)):
# https://github.com/telegramdesktop/tdesktop/blob/12905f0dcb9d513378e7db11989455a1b764ef75/Telegram/SourceFiles/boxes/photo_crop_box.cpp#L254
if (not is_image
@ -47,7 +46,17 @@ def _resize_photo_if_needed(
if isinstance(file, bytes):
file = io.BytesIO(file)
before = file.tell() if isinstance(file, io.IOBase) else None
if isinstance(file, io.IOBase):
# Pillow seeks to 0 unconditionally later anyway
old_pos = file.tell()
file.seek(0, io.SEEK_END)
before = file.tell()
elif isinstance(file, str) and os.path.exists(file):
# Check if file exists as a path and if so, get its size on disk
before = os.path.getsize(file)
else:
# Would be weird...
before = None
try:
# Don't use a `with` block for `image`, or `file` would be closed.
@ -58,34 +67,43 @@ def _resize_photo_if_needed(
except KeyError:
kwargs = {}
if image.width <= width and image.height <= height:
return file
if image.mode == 'RGB':
# Check if image is within acceptable bounds, if so, check if the image is at or below 10 MB, or assume it isn't if size is None or 0
if image.width <= width and image.height <= height and (before <= 10000000 if before else False):
return file
image.thumbnail((width, height), PIL.Image.ANTIALIAS)
alpha_index = image.mode.find('A')
if alpha_index == -1:
# If the image mode doesn't have alpha
# channel then don't bother masking it away.
# If the image is already RGB, don't convert it
# certain modes such as 'P' have no alpha index but can't be saved as JPEG directly
image.thumbnail((width, height), PIL.Image.LANCZOS)
result = image
else:
# We could save the resized image with the original format, but
# JPEG often compresses better -> smaller size -> faster upload
# We need to mask away the alpha channel ([3]), since otherwise
# IOError is raised when trying to save alpha channels in JPEG.
image.thumbnail((width, height), PIL.Image.LANCZOS)
result = PIL.Image.new('RGB', image.size, background)
result.paste(image, mask=image.split()[alpha_index])
mask = None
if image.has_transparency_data:
if image.mode == 'RGBA':
mask = image.getchannel('A')
else:
mask = image.convert('RGBA').getchannel('A')
result.paste(image, mask=mask)
buffer = io.BytesIO()
result.save(buffer, 'JPEG', **kwargs)
result.save(buffer, 'JPEG', progressive=True, **kwargs)
buffer.seek(0)
buffer.name = 'a.jpg'
return buffer
except IOError:
return file
finally:
if before is not None:
file.seek(before, io.SEEK_SET)
# The original position might matter
if isinstance(file, io.IOBase):
file.seek(old_pos)
class UploadMethods:
@ -99,6 +117,7 @@ class UploadMethods:
*,
caption: typing.Union[str, typing.Sequence[str]] = None,
force_document: bool = False,
mime_type: str = None,
file_size: int = None,
clear_draft: bool = False,
progress_callback: 'hints.ProgressCallback' = None,
@ -107,17 +126,24 @@ class UploadMethods:
thumb: 'hints.FileLike' = None,
allow_cache: bool = True,
parse_mode: str = (),
formatting_entities: typing.Optional[typing.List[types.TypeMessageEntity]] = None,
formatting_entities: typing.Optional[
typing.Union[
typing.List[types.TypeMessageEntity], typing.List[typing.List[types.TypeMessageEntity]]
]
] = None,
voice_note: bool = False,
video_note: bool = False,
buttons: 'hints.MarkupLike' = None,
buttons: typing.Optional['hints.MarkupLike'] = None,
silent: bool = None,
background: bool = None,
supports_streaming: bool = False,
schedule: 'hints.DateLike' = None,
comment_to: 'typing.Union[int, types.Message]' = None,
ttl: int = None,
**kwargs) -> 'types.Message':
nosound_video: bool = None,
send_as: typing.Optional['hints.EntityLike'] = None,
message_effect_id: typing.Optional[int] = None,
**kwargs) -> typing.Union[typing.List[typing.Any], typing.Any]:
"""
Sends message with the given file to the specified entity.
@ -184,6 +210,13 @@ class UploadMethods:
the extension of an image file or a video file, it will be
sent as such. Otherwise always as a document.
mime_type (`str`, optional):
Custom mime type to use for the file to be sent (for example,
``audio/mpeg``, ``audio/x-vorbis+ogg``, etc.).
It can change the type of files displayed.
If not set to any value, the mime type will be determined
automatically based on the file's extension.
file_size (`int`, optional):
The size of the file to be uploaded if it needs to be uploaded,
which will be determined automatically if not specified.
@ -230,7 +263,11 @@ class UploadMethods:
default.
formatting_entities (`list`, optional):
A list of message formatting entities. When provided, the ``parse_mode`` is ignored.
Optional formatting entities for the sent media message. When sending an album,
`formatting_entities` can be a list of lists, where each inner list contains
`types.TypeMessageEntity`. Each inner list will be assigned to the corresponding
file in a pairwise manner with the caption. If provided, the ``parse_mode``
parameter will be ignored.
voice_note (`bool`, optional):
If `True` the audio will be sent as a voice note.
@ -286,6 +323,25 @@ class UploadMethods:
Not all types of media can be used with this parameter, such
as text documents, which will fail with ``TtlMediaInvalidError``.
nosound_video (`bool`, optional):
Only applicable when sending a video file without an audio
track. If set to ``True``, the video will be displayed in
Telegram as a video. If set to ``False``, Telegram will attempt
to display the video as an animated gif. (It may still display
as a video due to other factors.) The value is ignored if set
on non-video files. This is set to ``True`` for albums, as gifs
cannot be sent in albums.
send_as (`entity`):
Unique identifier (int) or username (str) of the chat or channel to send the message as.
You can use this to send the message on behalf of a chat or channel where you have appropriate permissions.
Use the GetSendAs to return the list of message sender identifiers, which can be used to send messages in the chat,
This setting applies to the current message and will remain effective for future messages unless explicitly changed.
To set this behavior permanently for all messages, use SaveDefaultSendAs.
message_effect_id (`int`, optional):
Unique identifier of the message effect to be added to the message; for private chats only
Returns
The `Message <telethon.tl.custom.message.Message>` (or messages)
containing the sent file, or messages if a list of them was passed.
@ -343,6 +399,9 @@ class UploadMethods:
if not caption:
caption = ''
if not formatting_entities:
formatting_entities = []
entity = await self.get_input_entity(entity)
if comment_to is not None:
entity, reply_to = await self._get_comment_data(entity, comment_to)
@ -352,38 +411,46 @@ class UploadMethods:
# First check if the user passed an iterable, in which case
# we may want to send grouped.
if utils.is_list_like(file):
sent_count = 0
used_callback = None if not progress_callback else (
lambda s, t: progress_callback(sent_count + s, len(file))
)
if utils.is_list_like(caption):
captions = caption
else:
captions = [caption]
# Check that formatting_entities list is valid
if all(utils.is_list_like(obj) for obj in formatting_entities):
formatting_entities = formatting_entities
elif utils.is_list_like(formatting_entities):
formatting_entities = [formatting_entities]
else:
raise TypeError('The formatting_entities argument must be a list or a sequence of lists')
# Check that all entities in all lists are of the correct type
if not all(isinstance(ent, types.TypeMessageEntity) for sublist in formatting_entities for ent in sublist):
raise TypeError('All entities must be instances of <types.TypeMessageEntity>')
result = []
while file:
result += await self._send_album(
entity, file[:10], caption=captions[:10],
progress_callback=progress_callback, reply_to=reply_to,
entity, file[:10], caption=captions[:10], formatting_entities=formatting_entities[:10],
progress_callback=used_callback, reply_to=reply_to,
parse_mode=parse_mode, silent=silent, schedule=schedule,
supports_streaming=supports_streaming, clear_draft=clear_draft,
force_document=force_document, background=background,
send_as=send_as, message_effect_id=message_effect_id
)
file = file[10:]
captions = captions[10:]
for doc, cap in zip(file, captions):
result.append(await self.send_file(
entity, doc, allow_cache=allow_cache,
caption=cap, force_document=force_document,
progress_callback=progress_callback, reply_to=reply_to,
attributes=attributes, thumb=thumb, voice_note=voice_note,
video_note=video_note, buttons=buttons, silent=silent,
supports_streaming=supports_streaming, schedule=schedule,
clear_draft=clear_draft, background=background,
**kwargs
))
formatting_entities = formatting_entities[10:]
sent_count += 10
return result
if formatting_entities is not None:
if formatting_entities:
msg_entities = formatting_entities
else:
caption, msg_entities =\
@ -391,11 +458,13 @@ class UploadMethods:
file_handle, media, image = await self._file_to_media(
file, force_document=force_document,
mime_type=mime_type,
file_size=file_size,
progress_callback=progress_callback,
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
attributes=attributes, allow_cache=allow_cache, thumb=thumb,
voice_note=voice_note, video_note=video_note,
supports_streaming=supports_streaming, ttl=ttl
supports_streaming=supports_streaming, ttl=ttl,
nosound_video=nosound_video,
)
# e.g. invalid cast from :tl:`MessageMediaWebPage`
@ -403,19 +472,25 @@ class UploadMethods:
raise TypeError('Cannot use {!r} as file'.format(file))
markup = self.build_reply_markup(buttons)
reply_to = None if reply_to is None else types.InputReplyToMessage(reply_to)
request = functions.messages.SendMediaRequest(
entity, media, reply_to_msg_id=reply_to, message=caption,
entity, media, reply_to=reply_to, message=caption,
entities=msg_entities, reply_markup=markup, silent=silent,
schedule_date=schedule, clear_draft=clear_draft,
background=background
background=background,
send_as=await self.get_input_entity(send_as) if send_as else None,
effect=message_effect_id
)
return self._get_response_message(request, await self(request), entity)
async def _send_album(self: 'TelegramClient', entity, files, caption='',
formatting_entities=None,
progress_callback=None, reply_to=None,
parse_mode=(), silent=None, schedule=None,
supports_streaming=None, clear_draft=None,
force_document=False, background=None, ttl=None):
force_document=False, background=None, ttl=None,
send_as: typing.Optional['hints.EntityLike'] = None,
message_effect_id: typing.Optional[int] = None):
"""Specialized version of .send_file for albums"""
# We don't care if the user wants to avoid cache, we will use it
# anyway. Why? The cached version will be exactly the same thing
@ -423,36 +498,51 @@ class UploadMethods:
# cache only makes a difference for documents where the user may
# want the attributes used on them to change.
#
# In theory documents can be sent inside the albums but they appear
# In theory documents can be sent inside the albums, but they appear
# as different messages (not inside the album), and the logic to set
# the attributes/avoid cache is already written in .send_file().
entity = await self.get_input_entity(entity)
if not utils.is_list_like(caption):
caption = (caption,)
if not all(isinstance(obj, list) for obj in formatting_entities):
formatting_entities = (formatting_entities,)
captions = []
for c in reversed(caption): # Pop from the end (so reverse)
captions.append(await self._parse_message_text(c or '', parse_mode))
# If the formatting_entities argument is provided, we don't use parse_mode
if formatting_entities:
# Pop from the end (so reverse)
capt_with_ent = itertools.zip_longest(reversed(caption), reversed(formatting_entities), fillvalue=None)
for msg_caption, msg_entities in capt_with_ent:
captions.append((msg_caption, msg_entities))
else:
for c in reversed(caption): # Pop from the end (so reverse)
captions.append(await self._parse_message_text(c or '', parse_mode))
reply_to = utils.get_message_id(reply_to)
used_callback = None if not progress_callback else (
# use an integer when sent matches total, to easily determine a file has been fully sent
lambda s, t: progress_callback(sent_count + 1 if s == t else sent_count + s / t, len(files))
)
# Need to upload the media first, but only if they're not cached yet
media = []
for file in files:
for sent_count, file in enumerate(files):
# Albums want :tl:`InputMedia` which, in theory, includes
# :tl:`InputMediaUploadedPhoto`. However using that will
# :tl:`InputMediaUploadedPhoto`. However, using that will
# make it `raise MediaInvalidError`, so we need to upload
# it as media and then convert that to :tl:`InputMediaPhoto`.
fh, fm, _ = await self._file_to_media(
file, supports_streaming=supports_streaming,
force_document=force_document, ttl=ttl)
force_document=force_document, ttl=ttl,
progress_callback=used_callback, nosound_video=True)
if isinstance(fm, (types.InputMediaUploadedPhoto, types.InputMediaPhotoExternal)):
r = await self(functions.messages.UploadMediaRequest(
entity, media=fm
))
fm = utils.get_input_media(r.photo)
elif isinstance(fm, types.InputMediaUploadedDocument):
elif isinstance(fm, (types.InputMediaUploadedDocument, types.InputMediaDocumentExternal)):
r = await self(functions.messages.UploadMediaRequest(
entity, media=fm
))
@ -473,9 +563,11 @@ class UploadMethods:
# Now we can construct the multi-media request
request = functions.messages.SendMultiMediaRequest(
entity, reply_to_msg_id=reply_to, multi_media=media,
entity, reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to), multi_media=media,
silent=silent, schedule_date=schedule, clear_draft=clear_draft,
background=background
background=background,
send_as=await self.get_input_entity(send_as) if send_as else None,
effect=message_effect_id
)
result = await self(request)
@ -546,6 +638,13 @@ class UploadMethods:
A callback function accepting two parameters:
``(sent bytes, total)``.
When sending an album, the callback will receive a number
between 0 and the amount of files as the "sent" parameter,
and the amount of files as the "total". Note that the first
parameter will be a floating point number to indicate progress
within a file (e.g. ``2.5`` means it has sent 50% of the third
file, because it's between 2 and 3).
Returns
:tl:`InputFileBig` if the file size is larger than 10MB,
`InputSizedFile <telethon.tl.custom.inputsizedfile.InputSizedFile>`
@ -604,7 +703,7 @@ class UploadMethods:
part_count = (file_size + part_size - 1) // part_size
self._log[__name__].info('Uploading file of %d bytes in %d chunks of %d',
file_size, part_count, part_size)
file_size, part_count, part_size)
pos = 0
for part_index in range(part_count):
@ -668,7 +767,7 @@ class UploadMethods:
progress_callback=None, attributes=None, thumb=None,
allow_cache=True, voice_note=False, video_note=False,
supports_streaming=False, mime_type=None, as_image=None,
ttl=None):
ttl=None, nosound_video=None):
if not file:
return None, None, None
@ -681,7 +780,7 @@ class UploadMethods:
# `aiofiles` do not base `io.IOBase` but do have `read`, so we
# just check for the read attribute to see if it's file-like.
if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig))\
if not isinstance(file, (str, bytes, types.InputFile, types.InputFileBig)) \
and not hasattr(file, 'read'):
# The user may pass a Message containing media (or the media,
# or anything similar) that should be treated as a file. Try
@ -753,13 +852,18 @@ class UploadMethods:
thumb = str(thumb.absolute())
thumb = await self.upload_file(thumb, file_size=file_size)
# setting `nosound_video` to `True` doesn't affect videos with sound
# instead it prevents sending silent videos as GIFs
nosound_video = nosound_video if mime_type.split("/")[0] == 'video' else None
media = types.InputMediaUploadedDocument(
file=file_handle,
mime_type=mime_type,
attributes=attributes,
thumb=thumb,
force_file=force_document and not is_image,
ttl_seconds=ttl
ttl_seconds=ttl,
nosound_video=nosound_video
)
return file_handle, media, as_image

View File

@ -30,10 +30,15 @@ class UserMethods:
return await self._call(self._sender, request, ordered=ordered)
async def _call(self: 'TelegramClient', sender, request, ordered=False, flood_sleep_threshold=None):
if self._loop is not None and self._loop != helpers.get_running_loop():
raise RuntimeError('The asyncio event loop must not change after connection (see the FAQ for details)')
# if the loop is None it will fail with a connection error later on
if flood_sleep_threshold is None:
flood_sleep_threshold = self.flood_sleep_threshold
requests = (request if utils.is_list_like(request) else (request,))
for r in requests:
requests = list(request) if utils.is_list_like(request) else [request]
request = list(request) if utils.is_list_like(request) else request
for i, r in enumerate(requests):
if not isinstance(r, TLRequest):
raise _NOT_A_REQUEST()
await r.resolve(self, utils)
@ -52,7 +57,11 @@ class UserMethods:
raise errors.FloodWaitError(request=r, capture=diff)
if self._no_updates:
r = functions.InvokeWithoutUpdatesRequest(r)
if utils.is_list_like(request):
request[i] = functions.InvokeWithoutUpdatesRequest(r)
else:
# This should only run once as requests should be a list of 1 item
request = functions.InvokeWithoutUpdatesRequest(r)
request_index = 0
last_error = None
@ -71,8 +80,7 @@ class UserMethods:
exceptions.append(e)
results.append(None)
continue
self.session.process_entities(result)
self._entity_cache.add(result)
await utils.maybe_async(self.session.process_entities(result))
exceptions.append(None)
results.append(result)
request_index += 1
@ -82,11 +90,11 @@ class UserMethods:
return results
else:
result = await future
self.session.process_entities(result)
self._entity_cache.add(result)
await utils.maybe_async(self.session.process_entities(result))
return result
except (errors.ServerError, errors.RpcCallFailError,
errors.RpcMcgetFailError, errors.InterdcCallErrorError,
errors.TimedOutError,
errors.InterdcCallRichErrorError) as e:
last_error = e
self._log[__name__].warning(
@ -94,7 +102,8 @@ class UserMethods:
e.__class__.__name__, e)
await asyncio.sleep(2)
except (errors.FloodWaitError, errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
except (errors.FloodWaitError, errors.FloodPremiumWaitError,
errors.SlowModeWaitError, errors.FloodTestPhoneWaitError) as e:
last_error = e
if utils.is_list_like(request):
request = request[request_index]
@ -154,20 +163,17 @@ class UserMethods:
me = await client.get_me()
print(me.username)
"""
if input_peer and self._self_input_peer:
return self._self_input_peer
if input_peer and self._mb_entity_cache.self_id:
return self._mb_entity_cache.get(self._mb_entity_cache.self_id)._as_input_peer()
try:
me = (await self(
functions.users.GetUsersRequest([types.InputUserSelf()])))[0]
self._bot = me.bot
if not self._self_input_peer:
self._self_input_peer = utils.get_input_peer(
me, allow_self=False
)
if not self._mb_entity_cache.self_id:
self._mb_entity_cache.set_self_user(me.id, me.bot, me.access_hash)
return self._self_input_peer if input_peer else me
return utils.get_input_peer(me, allow_self=False) if input_peer else me
except errors.UnauthorizedError:
return None
@ -179,7 +185,7 @@ class UserMethods:
This property is used in every update, and some like `updateLoginToken`
occur prior to login, so it gracefully handles when no ID is known yet.
"""
return self._self_input_peer.user_id if self._self_input_peer else None
return self._mb_entity_cache.self_id
async def is_bot(self: 'TelegramClient') -> bool:
"""
@ -193,10 +199,10 @@ class UserMethods:
else:
print('Hello')
"""
if self._bot is None:
self._bot = (await self.get_me()).bot
if self._mb_entity_cache.self_bot is None:
await self.get_me(input_peer=True)
return self._bot
return self._mb_entity_cache.self_bot
async def is_user_authorized(self: 'TelegramClient') -> bool:
"""
@ -222,7 +228,7 @@ class UserMethods:
async def get_entity(
self: 'TelegramClient',
entity: 'hints.EntitiesLike') -> 'hints.Entity':
entity: 'hints.EntitiesLike') -> typing.Union['hints.Entity', typing.List['hints.Entity']]:
"""
Turns the given entity into a valid Telegram :tl:`User`, :tl:`Chat`
or :tl:`Channel`. You can also pass a list or iterable of entities,
@ -321,7 +327,9 @@ class UserMethods:
# Merge users, chats and channels into a single dictionary
id_entity = {
utils.get_peer_id(x): x
# `get_input_entity` might've guessed the type from a non-marked ID,
# so the only way to match that with the input is by not using marks here.
utils.get_peer_id(x, add_mark=False): x
for x in itertools.chain(users, chats, channels)
}
@ -334,7 +342,7 @@ class UserMethods:
if isinstance(x, str):
result.append(await self._get_entity_from_string(x))
elif not isinstance(x, types.InputPeerSelf):
result.append(id_entity[utils.get_peer_id(x)])
result.append(id_entity[utils.get_peer_id(x, add_mark=False)])
else:
result.append(next(
u for u in id_entity.values()
@ -417,8 +425,8 @@ class UserMethods:
try:
# 0x2d45687 == crc32(b'Peer')
if isinstance(peer, int) or peer.SUBCLASS_OF_ID == 0x2d45687:
return self._entity_cache[peer]
except (AttributeError, KeyError):
return self._mb_entity_cache.get(utils.get_peer_id(peer, add_mark=False))._as_input_peer()
except AttributeError:
pass
# Then come known strings that take precedence
@ -427,7 +435,8 @@ class UserMethods:
# No InputPeer, cached peer, or known string. Fetch from disk cache
try:
return self.session.get_input_entity(peer)
input_entity = await utils.maybe_async(self.session.get_input_entity(peer))
return input_entity
except ValueError:
pass
@ -465,7 +474,7 @@ class UserMethods:
raise ValueError(
'Could not find the input entity for {} ({}). Please read https://'
'docs.telethon.dev/en/latest/concepts/entities.html to'
'docs.telethon.dev/en/stable/concepts/entities.html to'
' find out more details.'
.format(peer, type(peer).__name__)
)
@ -566,8 +575,8 @@ class UserMethods:
pass
try:
# Nobody with this username, maybe it's an exact name/title
return await self.get_entity(
self.session.get_input_entity(string))
input_entity = await utils.maybe_async(self.session.get_input_entity(string))
return await self.get_entity(input_entity)
except ValueError:
pass

1
telethon/custom.py Normal file
View File

@ -0,0 +1 @@
from .tl.custom import *

View File

@ -1,147 +0,0 @@
import inspect
import itertools
from . import utils
from .tl import types
# Which updates have the following fields?
_has_field = {
('user_id', int): [],
('chat_id', int): [],
('channel_id', int): [],
('peer', 'TypePeer'): [],
('peer', 'TypeDialogPeer'): [],
('message', 'TypeMessage'): [],
}
# Note: We don't bother checking for some rare:
# * `UpdateChatParticipantAdd.inviter_id` integer.
# * `UpdateNotifySettings.peer` dialog peer.
# * `UpdatePinnedDialogs.order` list of dialog peers.
# * `UpdateReadMessagesContents.messages` list of messages.
# * `UpdateChatParticipants.participants` list of participants.
#
# There are also some uninteresting `update.message` of type string.
def _fill():
for name in dir(types):
update = getattr(types, name)
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
cid = update.CONSTRUCTOR_ID
sig = inspect.signature(update.__init__)
for param in sig.parameters.values():
vec = _has_field.get((param.name, param.annotation))
if vec is not None:
vec.append(cid)
# Future-proof check: if the documentation format ever changes
# then we won't be able to pick the update types we are interested
# in, so we must make sure we have at least an update for each field
# which likely means we are doing it right.
if not all(_has_field.values()):
raise RuntimeError('FIXME: Did the init signature or updates change?')
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
_fill()
class EntityCache:
"""
In-memory input entity cache, defaultdict-like behaviour.
"""
def add(self, entities):
"""
Adds the given entities to the cache, if they weren't saved before.
"""
if not utils.is_list_like(entities):
# Invariant: all "chats" and "users" are always iterables,
# and "user" never is (so we wrap it inside a list).
entities = itertools.chain(
getattr(entities, 'chats', []),
getattr(entities, 'users', []),
(hasattr(entities, 'user') and [entities.user]) or []
)
for entity in entities:
try:
pid = utils.get_peer_id(entity)
if pid not in self.__dict__:
# Note: `get_input_peer` already checks for `access_hash`
self.__dict__[pid] = utils.get_input_peer(entity)
except TypeError:
pass
def __getitem__(self, item):
"""
Gets the corresponding :tl:`InputPeer` for the given ID or peer,
or raises ``KeyError`` on any error (i.e. cannot be found).
"""
if not isinstance(item, int) or item < 0:
try:
return self.__dict__[utils.get_peer_id(item)]
except TypeError:
raise KeyError('Invalid key will not have entity') from None
for cls in (types.PeerUser, types.PeerChat, types.PeerChannel):
result = self.__dict__.get(utils.get_peer_id(cls(item)))
if result:
return result
raise KeyError('No cached entity for the given key')
def clear(self):
"""
Clear the entity cache.
"""
self.__dict__.clear()
def ensure_cached(
self,
update,
has_user_id=frozenset(_has_field[('user_id', int)]),
has_chat_id=frozenset(_has_field[('chat_id', int)]),
has_channel_id=frozenset(_has_field[('channel_id', int)]),
has_peer=frozenset(_has_field[('peer', 'TypePeer')] + _has_field[('peer', 'TypeDialogPeer')]),
has_message=frozenset(_has_field[('message', 'TypeMessage')])
):
"""
Ensures that all the relevant entities in the given update are cached.
"""
# This method is called pretty often and we want it to have the lowest
# overhead possible. For that, we avoid `isinstance` and constantly
# getting attributes out of `types.` by "caching" the constructor IDs
# in sets inside the arguments, and using local variables.
dct = self.__dict__
cid = update.CONSTRUCTOR_ID
if cid in has_user_id and \
update.user_id not in dct:
return False
if cid in has_chat_id and \
utils.get_peer_id(types.PeerChat(update.chat_id)) not in dct:
return False
if cid in has_channel_id and \
utils.get_peer_id(types.PeerChannel(update.channel_id)) not in dct:
return False
if cid in has_peer and \
utils.get_peer_id(update.peer) not in dct:
return False
if cid in has_message:
x = update.message
y = getattr(x, 'peer_id', None) # handle MessageEmpty
if y and utils.get_peer_id(y) not in dct:
return False
y = getattr(x, 'from_id', None)
if y and utils.get_peer_id(y) not in dct:
return False
# We don't quite worry about entities anywhere else.
# This is enough.
return True

View File

@ -6,7 +6,7 @@ import re
from .common import (
ReadCancelledError, TypeNotFoundError, InvalidChecksumError,
InvalidBufferError, SecurityError, CdnFileTamperedError,
InvalidBufferError, AuthKeyNotFound, SecurityError, CdnFileTamperedError,
AlreadyInConversationError, BadMessageError, MultiError
)

View File

@ -1,5 +1,6 @@
"""Errors not related to the Telegram API itself"""
import struct
import textwrap
from ..tl import TLRequest
@ -18,8 +19,8 @@ class TypeNotFoundError(Exception):
def __init__(self, invalid_constructor_id, remaining):
super().__init__(
'Could not find a matching Constructor ID for the TLObject '
'that was supposed to be read with ID {:08x}. Most likely, '
'a TLObject was trying to be read when it should not be read. '
'that was supposed to be read with ID {:08x}. See the FAQ '
'for more details. '
'Remaining bytes: {!r}'.format(invalid_constructor_id, remaining))
self.invalid_constructor_id = invalid_constructor_id
@ -58,6 +59,22 @@ class InvalidBufferError(BufferError):
'Invalid response buffer (too short {})'.format(self.payload))
class AuthKeyNotFound(Exception):
"""
The server claims it doesn't know about the authorization key (session
file) currently being used. This might be because it either has never
seen this authorization key, or it used to know about the authorization
key but has forgotten it, either temporarily or permanently (possibly
due to server errors).
If the issue persists, you may need to recreate the session file and login
again. This is not done automatically because it is not possible to know
if the issue is temporary or permanent.
"""
def __init__(self):
super().__init__(textwrap.dedent(self.__class__.__doc__))
class SecurityError(Exception):
"""
Generic security error, mostly used when generating a new AuthKey.

View File

@ -97,8 +97,10 @@ class Album(EventBuilder):
@classmethod
def build(cls, update, others=None, self_id=None):
if not others:
return # We only care about albums which come inside the same Updates
# TODO normally we'd only check updates if they come with other updates
# but MessageBox is not designed for this so others will always be None.
# In essence we always rely on AlbumHack rather than returning early if not others.
others = [update]
if isinstance(update,
(types.UpdateNewMessage, types.UpdateNewChannelMessage)):
@ -150,23 +152,15 @@ class Album(EventBuilder):
"""
def __init__(self, messages):
message = messages[0]
if not message.out and isinstance(message.peer_id, types.PeerUser):
# Incoming message (e.g. from a bot) has peer_id=us, and
# from_id=bot (the actual "chat" from a user's perspective).
chat_peer = message.from_id
else:
chat_peer = message.peer_id
super().__init__(chat_peer=chat_peer,
super().__init__(chat_peer=message.peer_id,
msg_id=message.id, broadcast=bool(message.post))
SenderGetter.__init__(self, message.sender_id)
self.messages = messages
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
self.sender_id, self._entities, client._mb_entity_cache)
for msg in self.messages:
msg._finish_init(client, self._entities, None)

View File

@ -151,7 +151,7 @@ class CallbackQuery(EventBuilder):
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
self.sender_id, self._entities, client._mb_entity_cache)
@property
def id(self):
@ -208,8 +208,9 @@ class CallbackQuery(EventBuilder):
if not getattr(self._input_sender, 'access_hash', True):
# getattr with True to handle the InputPeerSelf() case
try:
self._input_sender = self._client._entity_cache[self._sender_id]
except KeyError:
self._input_sender = self._client._mb_entity_cache.get(
utils.resolve_id(self._sender_id)[0])._as_input_peer()
except AttributeError:
m = await self.get_message()
if m:
self._sender = m._sender
@ -299,7 +300,7 @@ class CallbackQuery(EventBuilder):
"""
Edits the message. Shorthand for
`telethon.client.messages.MessageMethods.edit_message` with
the ``entity`` set to the correct :tl:`InputBotInlineMessageID`.
the ``entity`` set to the correct :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64`.
Returns `True` if the edit was successful.
@ -312,7 +313,7 @@ class CallbackQuery(EventBuilder):
since the message object is normally not present.
"""
self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, types.InputBotInlineMessageID):
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
return await self._client.edit_message(
self.query.msg_id, *args, **kwargs
)
@ -337,6 +338,8 @@ class CallbackQuery(EventBuilder):
This method will likely fail if `via_inline` is `True`.
"""
self._client.loop.create_task(self.answer())
if isinstance(self.query.msg_id, (types.InputBotInlineMessageID, types.InputBotInlineMessageID64)):
raise TypeError('Inline messages cannot be deleted as there is no API request available to do so')
return await self._client.delete_messages(
await self.get_input_chat(), [self.query.msg_id],
*args, **kwargs

View File

@ -108,6 +108,17 @@ class ChatAction(EventBuilder):
return cls.Event(msg,
new_score=action.score)
elif isinstance(update, types.UpdateChannelParticipant) \
and bool(update.new_participant) != bool(update.prev_participant):
# If members are hidden, bots will receive this update instead,
# as there won't be a service message. Promotions and demotions
# seem to have both new and prev participant, which are ignored
# by this event.
return cls.Event(types.PeerChannel(update.channel_id),
users=update.user_id,
added_by=update.actor_id if update.new_participant else None,
kicked_by=update.actor_id if update.prev_participant else None)
class Event(EventCommon):
"""
Represents the event of a new chat action.
@ -142,7 +153,7 @@ class ChatAction(EventBuilder):
new_title (`str`, optional):
The new title string for the chat, if applicable.
new_score (`str`, optional):
The new score string for the game, if applicable.
@ -414,9 +425,10 @@ class ChatAction(EventBuilder):
# If missing, try from the entity cache
try:
self._input_users.append(self._client._entity_cache[user_id])
self._input_users.append(self._client._mb_entity_cache.get(
utils.resolve_id(user_id)[0])._as_input_peer())
continue
except KeyError:
except AttributeError:
pass
return self._input_users or []

View File

@ -154,7 +154,7 @@ class EventCommon(ChatGetter, abc.ABC):
self._client = client
if self._chat_peer:
self._chat, self._input_chat = utils._get_entity_pair(
self.chat_id, self._entities, client._entity_cache)
self.chat_id, self._entities, client._mb_entity_cache)
else:
self._chat = self._input_chat = None

View File

@ -4,7 +4,7 @@ import re
import asyncio
from .common import EventBuilder, EventCommon, name_inner_event
from .. import utils
from .. import utils, helpers
from ..tl import types, functions, custom
from ..tl.custom.sendergetter import SenderGetter
@ -99,7 +99,7 @@ class InlineQuery(EventBuilder):
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
self.sender_id, self._entities, client._mb_entity_cache)
@property
def id(self):
@ -242,6 +242,6 @@ class InlineQuery(EventBuilder):
if inspect.isawaitable(obj):
return asyncio.ensure_future(obj)
f = asyncio.get_event_loop().create_future()
f = helpers.get_running_loop().create_future()
f.set_result(obj)
return f

View File

@ -14,7 +14,7 @@ from ..tl.custom.sendergetter import SenderGetter
# in a single place will make it annoying to use (since
# the user needs to check for the existence of `None`).
#
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUserPhoto
# TODO Handle UpdateUserBlocked, UpdateUserName, UpdateUserPhone, UpdateUser
def _requires_action(function):
@functools.wraps(function)
@ -95,7 +95,7 @@ class UserUpdate(EventBuilder):
def _set_client(self, client):
super()._set_client(client)
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, self._entities, client._entity_cache)
self.sender_id, self._entities, client._mb_entity_cache)
@property
def user(self):
@ -246,7 +246,7 @@ class UserUpdate(EventBuilder):
return isinstance(self.action, types.SendMessageUploadPhotoAction)
@property
@_requires_action
@_requires_status
def last_seen(self):
"""
Exact `datetime.datetime` when the user was last seen if known.

View File

@ -1,11 +1,9 @@
"""
This module contains the BinaryReader utility class.
"""
import os
import struct
import time
from datetime import datetime, timezone, timedelta
from io import BytesIO
from struct import unpack
from datetime import datetime, timedelta, timezone
from ..errors import TypeNotFoundError
from ..tl.alltlobjects import tlobjects
@ -21,7 +19,8 @@ class BinaryReader:
"""
def __init__(self, data):
self.stream = BytesIO(data)
self.stream = data or b''
self.position = 0
self._last = None # Should come in handy to spot -404 errors
# region Reading
@ -30,23 +29,35 @@ class BinaryReader:
# https://core.telegram.org/mtproto
def read_byte(self):
"""Reads a single byte value."""
return self.read(1)[0]
value, = struct.unpack_from("<B", self.stream, self.position)
self.position += 1
return value
def read_int(self, signed=True):
"""Reads an integer (4 bytes) value."""
return int.from_bytes(self.read(4), byteorder='little', signed=signed)
fmt = '<i' if signed else '<I'
value, = struct.unpack_from(fmt, self.stream, self.position)
self.position += 4
return value
def read_long(self, signed=True):
"""Reads a long integer (8 bytes) value."""
return int.from_bytes(self.read(8), byteorder='little', signed=signed)
fmt = '<q' if signed else '<Q'
value, = struct.unpack_from(fmt, self.stream, self.position)
self.position += 8
return value
def read_float(self):
"""Reads a real floating point (4 bytes) value."""
return unpack('<f', self.read(4))[0]
value, = struct.unpack_from("<f", self.stream, self.position)
self.position += 4
return value
def read_double(self):
"""Reads a real floating point (8 bytes) value."""
return unpack('<d', self.read(8))[0]
value, = struct.unpack_from("<d", self.stream, self.position)
self.position += 8
return value
def read_large_int(self, bits, signed=True):
"""Reads a n-bits long integer value."""
@ -55,7 +66,12 @@ class BinaryReader:
def read(self, length=-1):
"""Read the given amount of bytes, or -1 to read all remaining."""
result = self.stream.read(length)
if length >= 0:
result = self.stream[self.position:self.position + length]
self.position += length
else:
result = self.stream[self.position:]
self.position += len(result)
if (length >= 0) and (len(result) != length):
raise BufferError(
'No more data left to read (need {}, got {}: {}); last read {}'
@ -67,7 +83,7 @@ class BinaryReader:
def get_bytes(self):
"""Gets the byte array representing the current buffer as a whole."""
return self.stream.getvalue()
return self.stream
# endregion
@ -153,24 +169,24 @@ class BinaryReader:
def close(self):
"""Closes the reader, freeing the BytesIO stream."""
self.stream.close()
self.stream = b''
# region Position related
def tell_position(self):
"""Tells the current position on the stream."""
return self.stream.tell()
return self.position
def set_position(self, position):
"""Sets the current position on the stream."""
self.stream.seek(position)
self.position = position
def seek(self, offset):
"""
Seeks the stream position given an offset from the current position.
The offset may be negative.
"""
self.stream.seek(offset, os.SEEK_CUR)
self.position += offset
# endregion

View File

@ -1,34 +1,22 @@
"""
Simple HTML -> Telegram entity parser.
"""
import struct
from collections import deque
from html import escape
from html.parser import HTMLParser
from typing import Iterable, Optional, Tuple, List
from typing import Iterable, Tuple, List
from .. import helpers
from ..helpers import add_surrogate, del_surrogate, within_surrogate, strip_text
from ..tl import TLObject
from ..tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityEmail, MessageEntityUrl,
MessageEntityTextUrl, MessageEntityMentionName,
MessageEntityUnderline, MessageEntityStrike, MessageEntityBlockquote,
TypeMessageEntity
MessageEntityCustomEmoji, TypeMessageEntity
)
# Helpers from markdown.py
def _add_surrogate(text):
return ''.join(
''.join(chr(y) for y in struct.unpack('<HH', x.encode('utf-16le')))
if (0x10000 <= ord(x) <= 0x10FFFF) else x for x in text
)
def _del_surrogate(text):
return text.encode('utf-16', 'surrogatepass').decode('utf-16')
class HTMLToTelegramParser(HTMLParser):
def __init__(self):
super().__init__()
@ -86,11 +74,19 @@ class HTMLToTelegramParser(HTMLParser):
EntityType = MessageEntityUrl
else:
EntityType = MessageEntityTextUrl
args['url'] = url
args['url'] = del_surrogate(url)
url = None
self._open_tags_meta.popleft()
self._open_tags_meta.appendleft(url)
elif tag == 'tg-emoji':
try:
emoji_id = int(attrs['emoji-id'])
except (KeyError, ValueError):
return
EntityType = MessageEntityCustomEmoji
args['document_id'] = emoji_id
if EntityType and tag not in self._building_entities:
self._building_entities[tag] = EntityType(
offset=len(self.text),
@ -133,13 +129,36 @@ def parse(html: str) -> Tuple[str, List[TypeMessageEntity]]:
return html, []
parser = HTMLToTelegramParser()
parser.feed(_add_surrogate(html))
text = helpers.strip_text(parser.text, parser.entities)
return _del_surrogate(text), parser.entities
parser.feed(add_surrogate(html))
text = strip_text(parser.text, parser.entities)
parser.entities.reverse()
parser.entities.sort(key=lambda entity: entity.offset)
return del_surrogate(text), parser.entities
def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
_length: Optional[int] = None) -> str:
ENTITY_TO_FORMATTER = {
MessageEntityBold: ('<strong>', '</strong>'),
MessageEntityItalic: ('<em>', '</em>'),
MessageEntityCode: ('<code>', '</code>'),
MessageEntityUnderline: ('<u>', '</u>'),
MessageEntityStrike: ('<del>', '</del>'),
MessageEntityBlockquote: ('<blockquote>', '</blockquote>'),
MessageEntityPre: lambda e, _: (
"<pre>\n"
" <code class='language-{}'>\n"
" ".format(e.language), "{}\n"
" </code>\n"
"</pre>"
),
MessageEntityEmail: lambda _, t: ('<a href="mailto:{}">'.format(t), '</a>'),
MessageEntityUrl: lambda _, t: ('<a href="{}">'.format(t), '</a>'),
MessageEntityTextUrl: lambda e, _: ('<a href="{}">'.format(escape(e.url)), '</a>'),
MessageEntityMentionName: lambda e, _: ('<a href="tg://user?id={}">'.format(e.user_id), '</a>'),
MessageEntityCustomEmoji: lambda e, _: ('<tg-emoji emoji-id="{}">'.format(e.document_id), '</tg-emoji>'),
}
def unparse(text: str, entities: Iterable[TypeMessageEntity]) -> str:
"""
Performs the reverse operation to .parse(), effectively returning HTML
given a normal text and its MessageEntity's.
@ -153,77 +172,32 @@ def unparse(text: str, entities: Iterable[TypeMessageEntity], _offset: int = 0,
elif not entities:
return escape(text)
text = _add_surrogate(text)
if _length is None:
_length = len(text)
html = []
last_offset = 0
if isinstance(entities, TLObject):
entities = (entities,)
text = add_surrogate(text)
insert_at = []
for i, entity in enumerate(entities):
if entity.offset >= _offset + _length:
break
relative_offset = entity.offset - _offset
if relative_offset > last_offset:
html.append(escape(text[last_offset:relative_offset]))
elif relative_offset < last_offset:
continue
s = entity.offset
e = entity.offset + entity.length
delimiter = ENTITY_TO_FORMATTER.get(type(entity), None)
if delimiter:
if callable(delimiter):
delimiter = delimiter(entity, text[s:e])
insert_at.append((s, i, delimiter[0]))
insert_at.append((e, -i, delimiter[1]))
skip_entity = False
length = entity.length
insert_at.sort(key=lambda t: (t[0], t[1]))
next_escape_bound = len(text)
while insert_at:
# Same logic as markdown.py
at, _, what = insert_at.pop()
while within_surrogate(text, at):
at += 1
# If we are in the middle of a surrogate nudge the position by +1.
# Otherwise we would end up with malformed text and fail to encode.
# For example of bad input: "Hi \ud83d\ude1c"
# https://en.wikipedia.org/wiki/UTF-16#U+010000_to_U+10FFFF
while helpers.within_surrogate(text, relative_offset, length=_length):
relative_offset += 1
text = text[:at] + what + escape(text[at:next_escape_bound]) + text[next_escape_bound:]
next_escape_bound = at
while helpers.within_surrogate(text, relative_offset + length, length=_length):
length += 1
text = escape(text[:next_escape_bound]) + text[next_escape_bound:]
entity_text = unparse(text=text[relative_offset:relative_offset + length],
entities=entities[i + 1:],
_offset=entity.offset, _length=length)
entity_type = type(entity)
if entity_type == MessageEntityBold:
html.append('<strong>{}</strong>'.format(entity_text))
elif entity_type == MessageEntityItalic:
html.append('<em>{}</em>'.format(entity_text))
elif entity_type == MessageEntityCode:
html.append('<code>{}</code>'.format(entity_text))
elif entity_type == MessageEntityUnderline:
html.append('<u>{}</u>'.format(entity_text))
elif entity_type == MessageEntityStrike:
html.append('<del>{}</del>'.format(entity_text))
elif entity_type == MessageEntityBlockquote:
html.append('<blockquote>{}</blockquote>'.format(entity_text))
elif entity_type == MessageEntityPre:
if entity.language:
html.append(
"<pre>\n"
" <code class='language-{}'>\n"
" {}\n"
" </code>\n"
"</pre>".format(entity.language, entity_text))
else:
html.append('<pre><code>{}</code></pre>'
.format(entity_text))
elif entity_type == MessageEntityEmail:
html.append('<a href="mailto:{0}">{0}</a>'.format(entity_text))
elif entity_type == MessageEntityUrl:
html.append('<a href="{0}">{0}</a>'.format(entity_text))
elif entity_type == MessageEntityTextUrl:
html.append('<a href="{}">{}</a>'
.format(escape(entity.url), entity_text))
elif entity_type == MessageEntityMentionName:
html.append('<a href="tg://user?id={}">{}</a>'
.format(entity.user_id, entity_text))
else:
skip_entity = True
last_offset = relative_offset + (0 if skip_entity else length)
while helpers.within_surrogate(text, last_offset, length=_length):
last_offset += 1
html.append(escape(text[last_offset:]))
return _del_surrogate(''.join(html))
return del_surrogate(text)

View File

@ -22,14 +22,10 @@ DEFAULT_DELIMITERS = {
'```': MessageEntityPre
}
DEFAULT_URL_RE = re.compile(r'\[([\S\s]+?)\]\((.+?)\)')
DEFAULT_URL_RE = re.compile(r'\[([^]]*?)\]\(([\s\S]*?)\)')
DEFAULT_URL_FORMAT = '[{0}]({1})'
def overlap(a, b, x, y):
return max(a, x) < min(b, y)
def parse(message, delimiters=None, url_re=None):
"""
Parses the given markdown message and returns its stripped representation
@ -90,8 +86,8 @@ def parse(message, delimiters=None, url_re=None):
for ent in result:
# If the end is after our start, it is affected
if ent.offset + ent.length > i:
# If the old start is also before ours, it is fully enclosed
if ent.offset <= i:
# If the old start is before ours and the old end is after ours, we are fully enclosed
if ent.offset <= i and ent.offset + ent.length >= end + len(delim):
ent.length -= len(delim) * 2
else:
ent.length -= len(delim)
@ -119,7 +115,7 @@ def parse(message, delimiters=None, url_re=None):
message[m.end():]
))
delim_size = m.end() - m.start() - len(m.group())
delim_size = m.end() - m.start() - len(m.group(1))
for ent in result:
# If the end is after our start, it is affected
if ent.offset + ent.length > m.start():
@ -164,13 +160,13 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
text = add_surrogate(text)
delimiters = {v: k for k, v in delimiters.items()}
insert_at = []
for entity in entities:
for i, entity in enumerate(entities):
s = entity.offset
e = entity.offset + entity.length
delimiter = delimiters.get(type(entity), None)
if delimiter:
insert_at.append((s, delimiter))
insert_at.append((e, delimiter))
insert_at.append((s, i, delimiter))
insert_at.append((e, -i, delimiter))
else:
url = None
if isinstance(entity, MessageEntityTextUrl):
@ -178,12 +174,12 @@ def unparse(text, entities, delimiters=None, url_fmt=None):
elif isinstance(entity, MessageEntityMentionName):
url = 'tg://user?id={}'.format(entity.user_id)
if url:
insert_at.append((s, '['))
insert_at.append((e, ']({})'.format(url)))
insert_at.append((s, i, '['))
insert_at.append((e, -i, ']({})'.format(url)))
insert_at.sort(key=lambda t: t[0])
insert_at.sort(key=lambda t: (t[0], t[1]))
while insert_at:
at, what = insert_at.pop()
at, _, what = insert_at.pop()
# If we are in the middle of a surrogate nudge the position by -1.
# Otherwise we would end up with malformed text and fail to encode.

1
telethon/functions.py Normal file
View File

@ -0,0 +1 @@
from .tl.functions import *

View File

@ -7,6 +7,7 @@ import struct
import inspect
import logging
import functools
import sys
from pathlib import Path
from hashlib import sha1
@ -57,47 +58,81 @@ def within_surrogate(text, index, *, length=None):
return (
1 < index < len(text) and # in bounds
'\ud800' <= text[index - 1] <= '\udfff' and # previous is
'\ud800' <= text[index - 1] <= '\udbff' and # previous is
'\ud800' <= text[index] <= '\udfff' # current is
)
def strip_text(text, entities):
"""
Strips whitespace from the given text modifying the provided entities.
Strips whitespace from the given surrogated text modifying the provided
entities, also removing any empty (0-length) entities.
This assumes that there are no overlapping entities, that their length
is greater or equal to one, and that their length is not out of bounds.
This assumes that the length of entities is greater or equal to 0, and
that no entity is out of bounds.
"""
if not entities:
return text.strip()
while text and text[-1].isspace():
e = entities[-1]
if e.offset + e.length == len(text):
if e.length == 1:
del entities[-1]
if not entities:
return text.strip()
len_ori = len(text)
text = text.lstrip()
left_offset = len_ori - len(text)
text = text.rstrip()
len_final = len(text)
for i in reversed(range(len(entities))):
e = entities[i]
if e.length == 0:
del entities[i]
continue
if e.offset + e.length > left_offset:
if e.offset >= left_offset:
# 0 1|2 3 4 5 | 0 1|2 3 4 5
# ^ ^ | ^
# lo(2) o(5) | o(2)/lo(2)
e.offset -= left_offset
# |0 1 2 3 | |0 1 2 3
# ^ | ^
# o=o-lo(3=5-2) | o=o-lo(0=2-2)
else:
e.length -= 1
text = text[:-1]
# e.offset < left_offset and e.offset + e.length > left_offset
# 0 1 2 3|4 5 6 7 8 9 10
# ^ ^ ^
# o(1) lo(4) o+l(1+9)
e.length = e.offset + e.length - left_offset
e.offset = 0
# |0 1 2 3 4 5 6
# ^ ^
# o(0) o+l=0+o+l-lo(6=0+6=0+1+9-4)
else:
# e.offset + e.length <= left_offset
# 0 1 2 3|4 5
# ^ ^
# o(0) o+l(4)
# lo(4)
del entities[i]
continue
while text and text[0].isspace():
for i in reversed(range(len(entities))):
e = entities[i]
if e.offset != 0:
e.offset -= 1
continue
if e.length == 1:
del entities[0]
if not entities:
return text.lstrip()
else:
e.length -= 1
text = text[1:]
if e.offset + e.length <= len_final:
# |0 1 2 3 4 5 6 7 8 9
# ^ ^
# o(1) o+l(1+9)/lf(10)
continue
if e.offset >= len_final:
# |0 1 2 3 4
# ^
# o(5)/lf(5)
del entities[i]
else:
# e.offset < len_final and e.offset + e.length > len_final
# |0 1 2 3 4 5 (6) (7) (8) (9)
# ^ ^ ^
# o(1) lf(6) o+l(1+8)
e.length = len_final - e.offset
# |0 1 2 3 4 5
# ^ ^
# o(1) o+l=o+lf-o=lf(6=1+5=1+6-1)
return text
@ -118,7 +153,7 @@ def retry_range(retries, force_retry=True):
while attempt != retries:
attempt += 1
yield attempt
async def _maybe_await(value):
@ -313,21 +348,22 @@ class _FileStream(io.IOBase):
self._size = os.path.getsize(self._file)
self._stream = open(self._file, 'rb')
self._close_stream = True
return self
elif isinstance(self._file, bytes):
if isinstance(self._file, bytes):
self._size = len(self._file)
self._stream = io.BytesIO(self._file)
self._close_stream = True
return self
elif not callable(getattr(self._file, 'read', None)):
if not callable(getattr(self._file, 'read', None)):
raise TypeError('file description should have a `read` method')
elif self._size is not None:
self._name = getattr(self._file, 'name', None)
self._stream = self._file
self._close_stream = False
self._name = getattr(self._file, 'name', None)
self._stream = self._file
self._close_stream = False
else:
if self._size is None:
if callable(getattr(self._file, 'seekable', None)):
seekable = await _maybe_await(self._file.seekable())
else:
@ -338,8 +374,6 @@ class _FileStream(io.IOBase):
await _maybe_await(self._file.seek(0, os.SEEK_END))
self._size = await _maybe_await(self._file.tell())
await _maybe_await(self._file.seek(pos, os.SEEK_SET))
self._stream = self._file
self._close_stream = False
else:
_log.warning(
'Could not determine file size beforehand so the entire '
@ -389,3 +423,12 @@ class _FileStream(io.IOBase):
pass
# endregion
def get_running_loop():
if sys.version_info >= (3, 7):
try:
return asyncio.get_running_loop()
except RuntimeError:
return asyncio.get_event_loop_policy().get_event_loop()
else:
return asyncio.get_event_loop()

View File

@ -44,7 +44,12 @@ FileLike = typing.Union[
typing.BinaryIO,
types.TypeMessageMedia,
types.TypeInputFile,
types.TypeInputFileLocation
types.TypeInputFileLocation,
types.TypeInputMedia,
types.TypePhoto,
types.TypeInputPhoto,
types.TypeDocument,
types.TypeInputDocument
]
# Can't use `typing.Type` in Python 3.5.2

View File

@ -13,7 +13,7 @@ try:
except ImportError:
python_socks = None
from ...errors import InvalidChecksumError
from ...errors import InvalidChecksumError, InvalidBufferError
from ... import helpers
@ -116,9 +116,15 @@ class Connection(abc.ABC):
# python_socks internal errors are not inherited from
# builtin IOError (just from Exception). Instead of adding those
# in exceptions clauses everywhere through the code, we
# rather monkey-patch them in place.
# rather monkey-patch them in place. Keep in mind that
# ProxyError takes error_code as keyword argument.
python_socks._errors.ProxyError = ConnectionError
class ConnectionErrorExtra(ConnectionError):
def __init__(self, message, error_code=None):
super().__init__(message)
self.error_code = error_code
python_socks._errors.ProxyError = ConnectionErrorExtra
python_socks._errors.ProxyConnectionError = ConnectionError
python_socks._errors.ProxyTimeoutError = ConnectionError
@ -155,7 +161,7 @@ class Connection(abc.ABC):
# Actual TCP connection is performed here.
await asyncio.wait_for(
asyncio.get_event_loop().sock_connect(sock=sock, address=address),
helpers.get_running_loop().sock_connect(sock=sock, address=address),
timeout=timeout
)
@ -190,7 +196,7 @@ class Connection(abc.ABC):
# Actual TCP connection and negotiation performed here.
await asyncio.wait_for(
asyncio.get_event_loop().sock_connect(sock=sock, address=address),
helpers.get_running_loop().sock_connect(sock=sock, address=address),
timeout=timeout
)
@ -244,7 +250,7 @@ class Connection(abc.ABC):
await self._connect(timeout=timeout, ssl=ssl)
self._connected = True
loop = asyncio.get_event_loop()
loop = helpers.get_running_loop()
self._send_task = loop.create_task(self._send_loop())
self._recv_task = loop.create_task(self._recv_loop())
@ -253,6 +259,9 @@ class Connection(abc.ABC):
Disconnects from the server, and clears
pending outgoing and incoming messages.
"""
if not self._connected:
return
self._connected = False
await helpers._cancel(
@ -265,7 +274,12 @@ class Connection(abc.ABC):
self._writer.close()
if sys.version_info >= (3, 7):
try:
await self._writer.wait_closed()
await asyncio.wait_for(self._writer.wait_closed(), timeout=10)
except asyncio.TimeoutError:
# See issue #3917. For some users, this line was hanging indefinitely.
# The hard timeout is not ideal (connection won't be properly closed),
# but the code will at least be able to procceed.
self._log.warning('Graceful disconnection timed out, forcibly ignoring cleanup')
except Exception as e:
# Disconnecting should never raise. Seen:
# * OSError: No route to host and
@ -291,8 +305,10 @@ class Connection(abc.ABC):
This method returns a coroutine.
"""
while self._connected:
result = await self._recv_queue.get()
if result: # None = sentinel value = keep trying
result, err = await self._recv_queue.get()
if err:
raise err
if result:
return result
raise ConnectionError('Not connected')
@ -319,34 +335,31 @@ class Connection(abc.ABC):
"""
This loop is constantly putting items on the queue as they're read.
"""
while self._connected:
try:
data = await self._recv()
except asyncio.CancelledError:
break
except Exception as e:
if isinstance(e, (IOError, asyncio.IncompleteReadError)):
msg = 'The server closed the connection'
self._log.info(msg)
elif isinstance(e, InvalidChecksumError):
msg = 'The server response had an invalid checksum'
self._log.info(msg)
try:
while self._connected:
try:
data = await self._recv()
except asyncio.CancelledError:
break
except (IOError, asyncio.IncompleteReadError) as e:
self._log.warning('Server closed the connection: %s', e)
await self._recv_queue.put((None, e))
await self.disconnect()
except InvalidChecksumError as e:
self._log.warning('Server response had invalid checksum: %s', e)
await self._recv_queue.put((None, e))
except InvalidBufferError as e:
self._log.warning('Server response had invalid buffer: %s', e)
await self._recv_queue.put((None, e))
except Exception as e:
self._log.exception('Unexpected exception in the receive loop')
await self._recv_queue.put((None, e))
await self.disconnect()
else:
msg = 'Unexpected exception in the receive loop'
self._log.exception(msg)
await self._recv_queue.put((data, None))
finally:
await self.disconnect()
await self.disconnect()
# Add a sentinel value to unstuck recv
if self._recv_queue.empty():
self._recv_queue.put_nowait(None)
break
try:
await self._recv_queue.put(data)
except asyncio.CancelledError:
break
def _init_conn(self):
"""

View File

@ -2,7 +2,7 @@ import struct
from zlib import crc32
from .connection import Connection, PacketCodec
from ...errors import InvalidChecksumError
from ...errors import InvalidChecksumError, InvalidBufferError
class FullPacketCodec(PacketCodec):
@ -24,6 +24,18 @@ class FullPacketCodec(PacketCodec):
async def read_packet(self, reader):
packet_len_seq = await reader.readexactly(8) # 4 and 4
packet_len, seq = struct.unpack('<ii', packet_len_seq)
if packet_len < 0 and seq < 0:
# It has been observed that the length and seq can be -429,
# followed by the body of 4 bytes also being -429.
# See https://github.com/LonamiWebs/Telethon/issues/4042.
body = await reader.readexactly(4)
raise InvalidBufferError(body)
elif packet_len < 8:
# Currently unknown why packet_len may be less than 8 but not negative.
# Attempting to `readexactly` with less than 0 fails without saying what
# the number was which is less helpful.
raise InvalidBufferError(packet_len_seq)
body = await reader.readexactly(packet_len - 8)
checksum = struct.unpack('<I', body[-4:])[0]
body = body[:-4]

View File

@ -1,5 +1,6 @@
import asyncio
import hashlib
import base64
import os
from .connection import ObfuscatedConnection
@ -98,7 +99,7 @@ class TcpMTProxy(ObfuscatedConnection):
def __init__(self, ip, port, dc_id, *, loggers, proxy=None, local_addr=None):
# connect to proxy's host and port instead of telegram's ones
proxy_host, proxy_port = self.address_info(proxy)
self._secret = bytes.fromhex(proxy[2])
self._secret = self.normalize_secret(proxy[2])
super().__init__(
proxy_host, proxy_port, dc_id, loggers=loggers)
@ -130,6 +131,18 @@ class TcpMTProxy(ObfuscatedConnection):
raise ValueError("No proxy info specified for MTProxy connection")
return proxy_info[:2]
@staticmethod
def normalize_secret(secret):
if secret[:2] in ("ee", "dd"): # Remove extra bytes
secret = secret[2:]
try:
secret_bytes = bytes.fromhex(secret)
except ValueError:
secret = secret + '=' * (-len(secret) % 4)
secret_bytes = base64.b64decode(secret.encode())
return secret_bytes[:16] # Remove the domain from the secret (until domain support is added)
class ConnectionTcpMTProxyAbridged(TcpMTProxy):
"""

View File

@ -1,6 +1,8 @@
import asyncio
import collections
import struct
import datetime
import time
from . import authenticator
from ..extensions.messagepacker import MessagePacker
@ -10,18 +12,20 @@ from .mtprotostate import MTProtoState
from ..tl.tlobject import TLRequest
from .. import helpers, utils
from ..errors import (
BadMessageError, InvalidBufferError, SecurityError,
BadMessageError, InvalidBufferError, AuthKeyNotFound, SecurityError,
TypeNotFoundError, rpc_message_to_error
)
from ..extensions import BinaryReader
from ..tl.core import RpcResult, MessageContainer, GzipPacked
from ..tl.functions.auth import LogOutRequest
from ..tl.functions import PingRequest, DestroySessionRequest
from ..tl.functions import PingRequest, DestroySessionRequest, DestroyAuthKeyRequest
from ..tl.types import (
MsgsAck, Pong, BadServerSalt, BadMsgNotification, FutureSalts,
MsgNewDetailedInfo, NewSessionCreated, MsgDetailedInfo, MsgsStateReq,
MsgsStateInfo, MsgsAllInfo, MsgResendReq, upload, DestroySessionOk, DestroySessionNone,
DestroyAuthKeyOk, DestroyAuthKeyNone, DestroyAuthKeyFail
)
from ..tl import types as _tl
from ..crypto import AuthKey
from ..helpers import retry_range
@ -44,7 +48,7 @@ class MTProtoSender:
def __init__(self, auth_key, *, loggers,
retries=5, delay=1, auto_reconnect=True, connect_timeout=None,
auth_key_callback=None,
update_callback=None, auto_reconnect_callback=None):
updates_queue=None, auto_reconnect_callback=None):
self._connection = None
self._loggers = loggers
self._log = loggers[__name__]
@ -53,7 +57,7 @@ class MTProtoSender:
self._auto_reconnect = auto_reconnect
self._connect_timeout = connect_timeout
self._auth_key_callback = auth_key_callback
self._update_callback = update_callback
self._updates_queue = updates_queue
self._auto_reconnect_callback = auto_reconnect_callback
self._connect_lock = asyncio.Lock()
self._ping = None
@ -66,7 +70,7 @@ class MTProtoSender:
# pending futures should be cancelled.
self._user_connected = False
self._reconnecting = False
self._disconnected = asyncio.get_event_loop().create_future()
self._disconnected = helpers.get_running_loop().create_future()
self._disconnected.set_result(None)
# We need to join the loops upon disconnection
@ -108,8 +112,11 @@ class MTProtoSender:
MsgsStateReq.CONSTRUCTOR_ID: self._handle_state_forgotten,
MsgResendReq.CONSTRUCTOR_ID: self._handle_state_forgotten,
MsgsAllInfo.CONSTRUCTOR_ID: self._handle_msg_all,
DestroySessionOk: self._handle_destroy_session,
DestroySessionNone: self._handle_destroy_session,
DestroySessionOk.CONSTRUCTOR_ID: self._handle_destroy_session,
DestroySessionNone.CONSTRUCTOR_ID: self._handle_destroy_session,
DestroyAuthKeyOk.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
DestroyAuthKeyNone.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
DestroyAuthKeyFail.CONSTRUCTOR_ID: self._handle_destroy_auth_key,
}
# Public API
@ -256,7 +263,7 @@ class MTProtoSender:
await self._disconnect(error=e)
raise e
loop = asyncio.get_event_loop()
loop = helpers.get_running_loop()
self._log.debug('Starting send loop')
self._send_loop_handle = loop.create_task(self._send_loop())
@ -295,7 +302,7 @@ class MTProtoSender:
# notify whenever we change it. This is crucial when we
# switch to different data centers.
if self._auth_key_callback:
self._auth_key_callback(self.auth_key)
await self._auth_key_callback(self.auth_key)
self._log.debug('auth_key generation success!')
return True
@ -377,11 +384,8 @@ class MTProtoSender:
except BufferError as e:
# TODO there should probably only be one place to except all these errors
if isinstance(e, InvalidBufferError) and e.code == 404:
self._log.info('Broken authorization key; resetting')
self.auth_key.key = None
if self._auth_key_callback:
self._auth_key_callback(None)
self._log.info('Server does not know about the current auth key; the session may need to be recreated')
last_error = AuthKeyNotFound()
ok = False
break
else:
@ -398,7 +402,7 @@ class MTProtoSender:
self._pending_state.clear()
if self._auto_reconnect_callback:
asyncio.get_event_loop().create_task(self._auto_reconnect_callback())
helpers.get_running_loop().create_task(self._auto_reconnect_callback())
break
else:
@ -423,7 +427,7 @@ class MTProtoSender:
# gets stuck.
# TODO It still gets stuck? Investigate where and why.
self._reconnecting = True
asyncio.get_event_loop().create_task(self._reconnect(error))
helpers.get_running_loop().create_task(self._reconnect(error))
def _keepalive_ping(self, rnd_id):
"""
@ -501,13 +505,29 @@ class MTProtoSender:
self._log.debug('Receiving items from the network...')
try:
body = await self._connection.recv()
except IOError as e:
self._log.info('Connection closed while receiving data')
except asyncio.CancelledError:
raise # bypass except Exception
except (IOError, asyncio.IncompleteReadError) as e:
self._log.info('Connection closed while receiving data: %s', e)
self._start_reconnect(e)
return
except InvalidBufferError as e:
if e.code == 429:
self._log.warning('Server indicated flood error at transport level: %s', e)
await self._disconnect(error=e)
else:
self._log.exception('Server sent invalid buffer')
self._start_reconnect(e)
return
except Exception as e:
self._log.exception('Unhandled error while receiving data')
self._start_reconnect(e)
return
try:
message = self._state.decrypt_message_data(body)
if message is None:
continue # this message is to be ignored
except TypeNotFoundError as e:
# Received object which we don't know how to deserialize
self._log.info('Type %08x not found, remaining data %r',
@ -521,18 +541,14 @@ class MTProtoSender:
continue
except BufferError as e:
if isinstance(e, InvalidBufferError) and e.code == 404:
self._log.info('Broken authorization key; resetting')
self.auth_key.key = None
if self._auth_key_callback:
self._auth_key_callback(None)
await self._disconnect(error=e)
self._log.info('Server does not know about the current auth key; the session may need to be recreated')
await self._disconnect(error=AuthKeyNotFound())
else:
self._log.warning('Invalid buffer %s', e)
self._start_reconnect(e)
return
except Exception as e:
self._log.exception('Unhandled error while receiving data')
self._log.exception('Unhandled error while decrypting data')
self._start_reconnect(e)
return
@ -596,12 +612,20 @@ class MTProtoSender:
# However receiving a File() with empty bytes is "common".
# See #658, #759 and #958. They seem to happen in a container
# which contain the real response right after.
try:
with BinaryReader(rpc_result.body) as reader:
if not isinstance(reader.tgread_object(), upload.File):
raise ValueError('Not an upload.File')
except (TypeNotFoundError, ValueError):
self._log.info('Received response without parent request: %s', rpc_result.body)
#
# But, it might also happen that we get an *error* for no parent request.
# If that's the case attempting to read from body which is None would fail with:
# "BufferError: No more data left to read (need 4, got 0: b''); last read None".
# This seems to be particularly common for "RpcError(error_code=-500, error_message='No workers running')".
if rpc_result.error:
self._log.info('Received error without parent request: %s', rpc_result.error)
else:
try:
with BinaryReader(rpc_result.body) as reader:
if not isinstance(reader.tgread_object(), upload.File):
raise ValueError('Not an upload.File')
except (TypeNotFoundError, ValueError):
self._log.info('Received response without parent request: %s', rpc_result.body)
return
if rpc_result.error:
@ -620,6 +644,7 @@ class MTProtoSender:
if not state.future.cancelled():
state.future.set_exception(e)
else:
self._store_own_updates(result)
if not state.future.cancelled():
state.future.set_result(result)
@ -648,12 +673,54 @@ class MTProtoSender:
try:
assert message.obj.SUBCLASS_OF_ID == 0x8af52aac # crc32(b'Updates')
except AssertionError:
self._log.warning('Note: %s is not an update, not dispatching it %s', message.obj)
self._log.warning(
'Note: %s is not an update, not dispatching it %s',
message.obj.__class__.__name__,
message.obj
)
return
self._log.debug('Handling update %s', message.obj.__class__.__name__)
if self._update_callback:
self._update_callback(message.obj)
self._updates_queue.put_nowait(message.obj)
def _store_own_updates(self, obj, *, _update_ids=frozenset((
_tl.UpdateShortMessage.CONSTRUCTOR_ID,
_tl.UpdateShortChatMessage.CONSTRUCTOR_ID,
_tl.UpdateShort.CONSTRUCTOR_ID,
_tl.UpdatesCombined.CONSTRUCTOR_ID,
_tl.Updates.CONSTRUCTOR_ID,
_tl.UpdateShortSentMessage.CONSTRUCTOR_ID,
)), _update_like_ids=frozenset((
_tl.messages.AffectedHistory.CONSTRUCTOR_ID,
_tl.messages.AffectedMessages.CONSTRUCTOR_ID,
_tl.messages.AffectedFoundMessages.CONSTRUCTOR_ID,
))):
try:
if obj.CONSTRUCTOR_ID in _update_ids:
obj._self_outgoing = True # flag to only process, but not dispatch these
self._updates_queue.put_nowait(obj)
elif obj.CONSTRUCTOR_ID in _update_like_ids:
# Ugly "hack" (?) - otherwise bots reliably detect gaps when deleting messages.
#
# Note: the `date` being `None` is used to check for `updatesTooLong`, so epoch
# is used instead. It is still not read, because `updateShort` has no `seq`.
#
# Some requests, such as `readHistory`, also return these types. But the `pts_count`
# seems to be zero, so while this will produce some bogus `updateDeleteMessages`,
# it's still one of the "cleaner" approaches to handling the new `pts`.
# `updateDeleteMessages` is probably the "least-invasive" update that can be used.
upd = _tl.UpdateShort(
_tl.UpdateDeleteMessages([], obj.pts, obj.pts_count),
datetime.datetime(*time.gmtime(0)[:6]).replace(tzinfo=datetime.timezone.utc)
)
upd._self_outgoing = True
self._updates_queue.put_nowait(upd)
elif obj.CONSTRUCTOR_ID == _tl.messages.InvitedUsers.CONSTRUCTOR_ID:
obj.updates._self_outgoing = True
self._updates_queue.put_nowait(obj.updates)
except AttributeError:
pass
async def _handle_pong(self, message):
"""
@ -826,3 +893,26 @@ class MTProtoSender:
del self._pending_state[msg_id]
if not state.future.cancelled():
state.future.set_result(message.obj)
async def _handle_destroy_auth_key(self, message):
"""
Handles :tl:`DestroyAuthKeyFail`, :tl:`DestroyAuthKeyNone`, and :tl:`DestroyAuthKeyOk`.
:tl:`DestroyAuthKey` is not intended for users to use, but they still
might, and the response won't come in `rpc_result`, so thhat's worked
around here.
"""
self._log.debug('Handling destroy auth key %s', message.obj)
for msg_id, state in list(self._pending_state.items()):
if isinstance(state.request, DestroyAuthKeyRequest):
del self._pending_state[msg_id]
if not state.future.cancelled():
state.future.set_result(message.obj)
# If the auth key has been destroyed, that pretty much means the
# library can't continue as our auth key will no longer be found
# on the server.
# Even if the library didn't disconnect, the server would (and then
# the library would reconnect and learn about auth key being invalid).
if isinstance(message.obj, DestroyAuthKeyOk):
await self._disconnect(error=AuthKeyNotFound())

View File

@ -2,6 +2,7 @@ import os
import struct
import time
from hashlib import sha256
from collections import deque
from ..crypto import AES
from ..errors import SecurityError, InvalidBufferError
@ -10,6 +11,17 @@ from ..tl.core import TLMessage
from ..tl.tlobject import TLRequest
from ..tl.functions import InvokeAfterMsgRequest
from ..tl.core.gzippacked import GzipPacked
from ..tl.types import BadServerSalt, BadMsgNotification
# N is not specified in https://core.telegram.org/mtproto/security_guidelines#checking-msg-id, but 500 is reasonable
MAX_RECENT_MSG_IDS = 500
MSG_TOO_NEW_DELTA = 30
MSG_TOO_OLD_DELTA = 300
# Something must be wrong if we ignore too many messages at the same time
MAX_CONSECUTIVE_IGNORED = 10
class _OpaqueRequest(TLRequest):
@ -54,6 +66,9 @@ class MTProtoState:
self.salt = 0
self.id = self._sequence = self._last_msg_id = None
self._recent_remote_ids = deque(maxlen=MAX_RECENT_MSG_IDS)
self._highest_remote_id = 0
self._ignore_count = 0
self.reset()
def reset(self):
@ -64,6 +79,9 @@ class MTProtoState:
self.id = struct.unpack('q', os.urandom(8))[0]
self._sequence = 0
self._last_msg_id = 0
self._recent_remote_ids.clear()
self._highest_remote_id = 0
self._ignore_count = 0
def update_message_id(self, message):
"""
@ -134,6 +152,8 @@ class MTProtoState:
"""
Inverse of `encrypt_message_data` for incoming server messages.
"""
now = time.time() # get the time as early as possible, even if other checks make it go unused
if len(body) < 8:
raise InvalidBufferError(body)
@ -156,9 +176,19 @@ class MTProtoState:
reader = BinaryReader(body)
reader.read_long() # remote_salt
if reader.read_long() != self.id:
raise SecurityError('Server replied with a wrong session ID')
raise SecurityError('Server replied with a wrong session ID (see FAQ for details)')
remote_msg_id = reader.read_long()
if remote_msg_id % 2 != 1:
raise SecurityError('Server sent an even msg_id')
# Only perform the (somewhat expensive) check of duplicate if we did receive a lower ID
if remote_msg_id <= self._highest_remote_id and remote_msg_id in self._recent_remote_ids:
self._log.warning('Server resent the older message %d, ignoring', remote_msg_id)
self._count_ignored()
return None
remote_sequence = reader.read_int()
reader.read_int() # msg_len for the inner object, padding ignored
@ -167,8 +197,45 @@ class MTProtoState:
# reader isn't used for anything else after this, it's unnecessary.
obj = reader.tgread_object()
# "Certain client-to-server service messages containing data sent by the client to the
# server (for example, msg_id of a recent client query) may, nonetheless, be processed
# on the client even if the time appears to be "incorrect". This is especially true of
# messages to change server_salt and notifications about invalid time on the client."
#
# This means we skip the time check for certain types of messages.
if obj.CONSTRUCTOR_ID in (BadServerSalt.CONSTRUCTOR_ID, BadMsgNotification.CONSTRUCTOR_ID):
if not self._highest_remote_id and not self.time_offset:
# If the first message we receive is a bad notification, take this opportunity
# to adjust the time offset. Assume it will remain stable afterwards. Updating
# the offset unconditionally would make the next checks pointless.
self.update_time_offset(remote_msg_id)
else:
remote_msg_time = remote_msg_id >> 32
time_delta = (now + self.time_offset) - remote_msg_time
if time_delta > MSG_TOO_OLD_DELTA:
self._log.warning('Server sent a very old message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
self._count_ignored()
return None
if -time_delta > MSG_TOO_NEW_DELTA:
self._log.warning('Server sent a very new message with ID %d, ignoring (see FAQ for details)', remote_msg_id)
self._count_ignored()
return None
self._recent_remote_ids.append(remote_msg_id)
self._highest_remote_id = remote_msg_id
self._ignore_count = 0
return TLMessage(remote_msg_id, remote_sequence, obj)
def _count_ignored(self):
# It's possible that ignoring a message "bricks" the connection,
# but this should not happen unless there's something else wrong.
self._ignore_count += 1
if self._ignore_count >= MAX_CONSECUTIVE_IGNORED:
raise SecurityError('Too many messages had to be ignored consecutively')
def _get_new_msg_id(self):
"""
Generates a new unique message ID based on the current

View File

@ -98,6 +98,11 @@ class Session(ABC):
raise NotImplementedError
@abstractmethod
def get_update_states(self):
"""
Returns an iterable over all known pairs of ``(entity ID, update state)``.
"""
def close(self):
"""
Called on client disconnection. Should be used to

View File

@ -77,6 +77,9 @@ class MemorySession(Session):
def set_update_state(self, entity_id, state):
self._update_states[entity_id] = state
def get_update_states(self):
return self._update_states.items()
def close(self):
pass
@ -171,7 +174,7 @@ class MemorySession(Session):
def get_entity_rows_by_id(self, id, exact=True):
try:
if exact:
return next((id, hash) for found_id, hash, _, _, _
return next((found_id, hash) for found_id, hash, _, _, _
in self._entities if found_id == id)
else:
ids = (
@ -179,7 +182,7 @@ class MemorySession(Session):
utils.get_peer_id(PeerChat(id)),
utils.get_peer_id(PeerChannel(id))
)
return next((id, hash) for found_id, hash, _, _, _
return next((found_id, hash) for found_id, hash, _, _, _
in self._entities if found_id in ids)
except StopIteration:
pass

View File

@ -2,7 +2,7 @@ import datetime
import os
import time
from telethon.tl import types
from ..tl import types
from .memory import MemorySession, _SentFileType
from .. import utils
from ..crypto import AuthKey
@ -215,6 +215,20 @@ class SQLiteSession(MemorySession):
entity_id, state.pts, state.qts,
state.date.timestamp(), state.seq)
def get_update_states(self):
c = self._cursor()
try:
rows = c.execute('select id, pts, qts, date, seq from update_state').fetchall()
return ((row[0], types.updates.State(
pts=row[1],
qts=row[2],
date=datetime.datetime.fromtimestamp(row[3], tz=datetime.timezone.utc),
seq=row[4],
unread_count=0)
) for row in rows)
finally:
c.close()
def save(self):
"""Saves the current session object as session_user_id.session"""
# This is a no-op if there are no changes to commit, so there's

View File

@ -1,164 +0,0 @@
import inspect
from .tl import types
# Which updates have the following fields?
_has_channel_id = []
# TODO EntityCache does the same. Reuse?
def _fill():
for name in dir(types):
update = getattr(types, name)
if getattr(update, 'SUBCLASS_OF_ID', None) == 0x9f89304e:
cid = update.CONSTRUCTOR_ID
sig = inspect.signature(update.__init__)
for param in sig.parameters.values():
if param.name == 'channel_id' and param.annotation == int:
_has_channel_id.append(cid)
if not _has_channel_id:
raise RuntimeError('FIXME: Did the init signature or updates change?')
# We use a function to avoid cluttering the globals (with name/update/cid/doc)
_fill()
class StateCache:
"""
In-memory update state cache, defaultdict-like behaviour.
"""
def __init__(self, initial, loggers):
# We only care about the pts and the date. By using a tuple which
# is lightweight and immutable we can easily copy them around to
# each update in case they need to fetch missing entities.
self._logger = loggers[__name__]
if initial:
self._pts_date = initial.pts, initial.date
else:
self._pts_date = None, None
def reset(self):
self.__dict__.clear()
self._pts_date = None, None
# TODO Call this when receiving responses too...?
def update(
self,
update,
*,
channel_id=None,
has_pts=frozenset(x.CONSTRUCTOR_ID for x in (
types.UpdateNewMessage,
types.UpdateDeleteMessages,
types.UpdateReadHistoryInbox,
types.UpdateReadHistoryOutbox,
types.UpdateWebPage,
types.UpdateReadMessagesContents,
types.UpdateEditMessage,
types.updates.State,
types.updates.DifferenceTooLong,
types.UpdateShortMessage,
types.UpdateShortChatMessage,
types.UpdateShortSentMessage
)),
has_date=frozenset(x.CONSTRUCTOR_ID for x in (
types.UpdateUserPhoto,
types.UpdateEncryption,
types.UpdateEncryptedMessagesRead,
types.UpdateChatParticipantAdd,
types.updates.DifferenceEmpty,
types.UpdateShortMessage,
types.UpdateShortChatMessage,
types.UpdateShort,
types.UpdatesCombined,
types.Updates,
types.UpdateShortSentMessage,
)),
has_channel_pts=frozenset(x.CONSTRUCTOR_ID for x in (
types.UpdateChannelTooLong,
types.UpdateNewChannelMessage,
types.UpdateDeleteChannelMessages,
types.UpdateEditChannelMessage,
types.UpdateChannelWebPage,
types.updates.ChannelDifferenceEmpty,
types.updates.ChannelDifferenceTooLong,
types.updates.ChannelDifference
)),
check_only=False
):
"""
Update the state with the given update.
"""
cid = update.CONSTRUCTOR_ID
if check_only:
return cid in has_pts or cid in has_date or cid in has_channel_pts
if cid in has_pts:
if cid in has_date:
self._pts_date = update.pts, update.date
else:
self._pts_date = update.pts, self._pts_date[1]
elif cid in has_date:
self._pts_date = self._pts_date[0], update.date
if cid in has_channel_pts:
if channel_id is None:
channel_id = self.get_channel_id(update)
if channel_id is None:
self._logger.info(
'Failed to retrieve channel_id from %s', update)
else:
self.__dict__[channel_id] = update.pts
def get_channel_id(
self,
update,
has_channel_id=frozenset(_has_channel_id),
# Hardcoded because only some with message are for channels
has_message=frozenset(x.CONSTRUCTOR_ID for x in (
types.UpdateNewChannelMessage,
types.UpdateEditChannelMessage
))
):
"""
Gets the **unmarked** channel ID from this update, if it has any.
Fails for ``*difference`` updates, where ``channel_id``
is supposedly already known from the outside.
"""
cid = update.CONSTRUCTOR_ID
if cid in has_channel_id:
return update.channel_id
elif cid in has_message:
if update.message.peer_id is None:
# Telegram sometimes sends empty messages to give a newer pts:
# UpdateNewChannelMessage(message=MessageEmpty(id), pts=pts, pts_count=1)
# Not sure why, but it's safe to ignore them.
self._logger.debug('Update has None peer_id %s', update)
else:
return update.message.peer_id.channel_id
return None
def __getitem__(self, item):
"""
If `item` is `None`, returns the default ``(pts, date)``.
If it's an **unmarked** channel ID, returns its ``pts``.
If no information is known, ``pts`` will be `None`.
"""
if item is None:
return self._pts_date
else:
return self.__dict__.get(item)
def __setitem__(self, where, value):
if where is None:
self._pts_date = value
else:
self.__dict__[where] = value

View File

@ -14,7 +14,7 @@ import asyncio
import functools
import inspect
from . import events, errors, utils, connection
from . import events, errors, utils, connection, helpers
from .client.account import _TakeoutClient
from .client.telegramclient import TelegramClient
from .tl import types, functions, custom
@ -32,7 +32,7 @@ def _syncify_wrap(t, method_name):
@functools.wraps(method)
def syncified(*args, **kwargs):
coro = method(*args, **kwargs)
loop = asyncio.get_event_loop()
loop = helpers.get_running_loop()
if loop.is_running():
return coro
else:

View File

@ -1,4 +1,7 @@
import gzip
try:
from isal import igzip as gzip
except ImportError:
import gzip
import struct
from .. import TLObject

View File

@ -37,11 +37,14 @@ class Button:
to 128 characters and add the ellipsis () character as
the 129.
"""
def __init__(self, button, *, resize, single_use, selective):
def __init__(self, button, *, resize, single_use, selective,
persistent, placeholder):
self.button = button
self.resize = resize
self.single_use = single_use
self.selective = selective
self.persistent = persistent
self.placeholder = placeholder
@staticmethod
def _is_inline(button):
@ -49,12 +52,14 @@ class Button:
Returns `True` if the button belongs to an inline keyboard.
"""
return isinstance(button, (
types.KeyboardButtonCopy,
types.KeyboardButtonBuy,
types.KeyboardButtonCallback,
types.KeyboardButtonGame,
types.KeyboardButtonSwitchInline,
types.KeyboardButtonUrl,
types.InputKeyboardButtonUrlAuth
types.InputKeyboardButtonUrlAuth,
types.KeyboardButtonWebView,
))
@staticmethod
@ -166,11 +171,15 @@ class Button:
)
@classmethod
def text(cls, text, *, resize=None, single_use=None, selective=None):
def text(cls, text, *, resize=None, single_use=None, selective=None,
persistent=None, placeholder=None):
"""
Creates a new keyboard button with the given text.
Args:
text (`str`):
The title of the button.
resize (`bool`):
If present, the entire keyboard will be reconfigured to
be resized and be smaller if there are not many buttons.
@ -185,48 +194,77 @@ class Button:
users. It will target users that are @mentioned in the text
of the message or to the sender of the message you reply to.
persistent (`bool`):
If present, always show the keyboard when the regular keyboard
is hidden. Defaults to false, in which case the custom keyboard
can be hidden and revealed via the keyboard icon.
placeholder (`str`):
The placeholder to be shown in the input field when the keyboard is active;
1-64 characters
When the user clicks this button, a text message with the same text
as the button will be sent, and can be handled with `events.NewMessage
<telethon.events.newmessage.NewMessage>`. You cannot distinguish
between a button press and the user typing and sending exactly the
same text on their own.
"""
return cls(types.KeyboardButton(text),
resize=resize, single_use=single_use, selective=selective)
return cls(
types.KeyboardButton(text),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@classmethod
def request_location(cls, text, *,
resize=None, single_use=None, selective=None):
def request_location(cls, text, *, resize=None, single_use=None, selective=None,
persistent=None, placeholder=None):
"""
Creates a new keyboard button to request the user's location on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their location with the
bot, and if confirmed a message with geo media will be sent.
"""
return cls(types.KeyboardButtonRequestGeoLocation(text),
resize=resize, single_use=single_use, selective=selective)
return cls(
types.KeyboardButtonRequestGeoLocation(text),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@classmethod
def request_phone(cls, text, *,
resize=None, single_use=None, selective=None):
def request_phone(cls, text, *, resize=None, single_use=None,
selective=None, persistent=None, placeholder=None):
"""
Creates a new keyboard button to request the user's phone on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their phone with the
bot, and if confirmed a message with contact media will be sent.
"""
return cls(types.KeyboardButtonRequestPhone(text),
resize=resize, single_use=single_use, selective=selective)
return cls(
types.KeyboardButtonRequestPhone(text),
resize=resize,
single_use=single_use,
selective=selective,
placeholder=placeholder,
persistent=persistent
)
@classmethod
def request_poll(cls, text, *, force_quiz=False,
resize=None, single_use=None, selective=None):
def request_poll(cls, text, *, force_quiz=False, resize=None, single_use=None,
selective=None, persistent=None, placeholder=None):
"""
Creates a new keyboard button to request the user to create a poll.
@ -238,13 +276,20 @@ class Button:
the votes cannot be retracted. Otherwise, users can vote and retract
the vote, and the pol might be multiple choice.
``resize``, ``single_use`` and ``selective`` are documented in `text`.
``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a screen letting the user create a
poll will be shown, and if they do create one, the poll will be sent.
"""
return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
resize=resize, single_use=single_use, selective=selective)
return cls(
types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@staticmethod
def clear(selective=None):
@ -263,15 +308,8 @@ class Button:
Forces a reply to the message with this markup. If used,
no other button should be present or it will be ignored.
``single_use`` and ``selective`` are as documented in `text`.
``single_use``, ``selective`` and ``placeholder`` are as documented in `text`.
Args:
placeholder (str):
text to show the user at typing place of message.
If the placeholder is too long, Telegram applications will
crop the text (for example, to 64 characters and adding an
ellipsis () character as the 65th).
"""
return types.ReplyKeyboardForceReply(
single_use=single_use,

View File

@ -66,8 +66,9 @@ class ChatGetter(abc.ABC):
"""
if self._input_chat is None and self._chat_peer and self._client:
try:
self._input_chat = self._client._entity_cache[self._chat_peer]
except KeyError:
self._input_chat = self._client._mb_entity_cache.get(
utils.get_peer_id(self._chat_peer, add_mark=False))._as_input_peer()
except AttributeError:
pass
return self._input_chat

View File

@ -1,11 +1,11 @@
import datetime
from .. import TLObject
from .. import TLObject, types
from ..functions.messages import SaveDraftRequest
from ..types import DraftMessage
from ...errors import RPCError
from ...extensions import markdown
from ...utils import get_input_peer, get_peer
from ...utils import get_input_peer, get_peer, get_peer_id
class Draft:
@ -37,7 +37,7 @@ class Draft:
self._raw_text = draft.message
self.date = draft.date
self.link_preview = not draft.no_webpage
self.reply_to_msg_id = draft.reply_to_msg_id
self.reply_to_msg_id = draft.reply_to.reply_to_msg_id if isinstance(draft.reply_to, types.InputReplyToMessage) else None
@property
def entity(self):
@ -53,8 +53,9 @@ class Draft:
"""
if not self._input_entity:
try:
self._input_entity = self._client._entity_cache[self._peer]
except KeyError:
self._input_entity = self._client._mb_entity_cache.get(
get_peer_id(self._peer, add_mark=False))._as_input_peer()
except AttributeError:
pass
return self._input_entity
@ -138,7 +139,7 @@ class Draft:
peer=self._peer,
message=raw_text,
no_webpage=not link_preview,
reply_to_msg_id=reply_to,
reply_to=None if reply_to is None else types.InputReplyToMessage(reply_to),
entities=entities
))

View File

@ -21,7 +21,12 @@ class File:
@property
def id(self):
"""
The bot-API style ``file_id`` representing this file.
The old bot-API style ``file_id`` representing this file.
.. warning::
This feature has not been maintained for a long time and
may not work. It will be removed in future versions.
.. note::

View File

@ -36,12 +36,12 @@ class Forward(ChatGetter, SenderGetter):
if ty == helpers._EntityType.USER:
sender_id = utils.get_peer_id(original.from_id)
sender, input_sender = utils._get_entity_pair(
sender_id, entities, client._entity_cache)
sender_id, entities, client._mb_entity_cache)
elif ty in (helpers._EntityType.CHAT, helpers._EntityType.CHANNEL):
peer = original.from_id
chat, input_chat = utils._get_entity_pair(
utils.get_peer_id(peer), entities, client._entity_cache)
utils.get_peer_id(peer), entities, client._mb_entity_cache)
# This call resets the client
ChatGetter.__init__(self, peer, chat=chat, input_chat=input_chat)

View File

@ -391,7 +391,7 @@ class InlineBuilder:
'text geo contact game'.split(), args) if x[1]) or 'none')
)
markup = self._client.build_reply_markup(buttons, inline_only=True)
markup = self._client.build_reply_markup(buttons)
if text is not None:
text, msg_entities = await self._client._parse_message_text(
text, parse_mode

View File

@ -158,7 +158,7 @@ class InlineResult:
background=background,
clear_draft=clear_draft,
hide_via=hide_via,
reply_to_msg_id=reply_id
reply_to=None if reply_id is None else types.InputReplyToMessage(reply_id)
)
return self._client._get_response_message(
req, await self._client(req), entity)

View File

@ -65,6 +65,15 @@ class Message(ChatGetter, SenderGetter, TLObject):
pinned (`bool`):
Whether this message is currently pinned or not.
noforwards (`bool`):
Whether this message can be forwarded or not.
invert_media (`bool`):
Whether the media in this message should be inverted.
offline (`bool`):
Whether the message was sent by an implicit action, for example, as an away or a greeting business message, or as a scheduled message.
id (`int`):
The ID of this message. This field is *always* present.
Any other member is optional and may be `None`.
@ -87,7 +96,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
The ID of the bot used to send this message
through its inline mode (e.g. "via @like").
reply_to (:tl:`MessageReplyHeader`):
reply_to (:tl:`MessageReplyHeader` | :tl:`MessageReplyStoryHeader`):
The original reply header if this message is replying to another.
date (`datetime`):
@ -141,6 +150,9 @@ class Message(ChatGetter, SenderGetter, TLObject):
(photo albums or video albums), all of them will
have the same value here.
reactions (:tl:`MessageReactions`)
Reactions to this message.
restriction_reason (List[:tl:`RestrictionReason`])
An optional list of reasons why this message was restricted.
If the list is `None`, this message has not been restricted.
@ -154,53 +166,68 @@ class Message(ChatGetter, SenderGetter, TLObject):
action (:tl:`MessageAction`):
The message action object of the message for :tl:`MessageService`
instances, which will be `None` for other types of messages.
saved_peer_id (:tl:`Peer`)
"""
# region Initialization
def __init__(
# Common to all
self, id: int,
# Common to Message and MessageService (mandatory)
peer_id: types.TypePeer = None,
date: Optional[datetime] = None,
# Common to Message and MessageService (flags)
out: Optional[bool] = None,
mentioned: Optional[bool] = None,
media_unread: Optional[bool] = None,
silent: Optional[bool] = None,
post: Optional[bool] = None,
from_id: Optional[types.TypePeer] = None,
reply_to: Optional[types.TypeMessageReplyHeader] = None,
ttl_period: Optional[int] = None,
# For Message (mandatory)
message: Optional[str] = None,
# For Message (flags)
fwd_from: Optional[types.TypeMessageFwdHeader] = None,
via_bot_id: Optional[int] = None,
media: Optional[types.TypeMessageMedia] = None,
reply_markup: Optional[types.TypeReplyMarkup] = None,
entities: Optional[List[types.TypeMessageEntity]] = None,
views: Optional[int] = None,
edit_date: Optional[datetime] = None,
post_author: Optional[str] = None,
grouped_id: Optional[int] = None,
from_scheduled: Optional[bool] = None,
legacy: Optional[bool] = None,
edit_hide: Optional[bool] = None,
pinned: Optional[bool] = None,
restriction_reason: Optional[types.TypeRestrictionReason] = None,
forwards: Optional[int] = None,
replies: Optional[types.TypeMessageReplies] = None,
# For MessageAction (mandatory)
action: Optional[types.TypeMessageAction] = None
self,
id: int,
peer_id: types.TypePeer,
date: Optional[datetime] = None,
message: Optional[str] = None,
# Copied from Message.__init__ signature
out: Optional[bool] = None,
mentioned: Optional[bool] = None,
media_unread: Optional[bool] = None,
silent: Optional[bool] = None,
post: Optional[bool] = None,
from_scheduled: Optional[bool] = None,
legacy: Optional[bool] = None,
edit_hide: Optional[bool] = None,
pinned: Optional[bool] = None,
noforwards: Optional[bool] = None,
invert_media: Optional[bool] = None,
offline: Optional[bool] = None,
video_processing_pending: Optional[bool] = None,
paid_suggested_post_stars: Optional[bool] = None,
paid_suggested_post_ton: Optional[bool] = None,
from_id: Optional[types.TypePeer] = None,
from_boosts_applied: Optional[int] = None,
saved_peer_id: Optional[types.TypePeer] = None,
fwd_from: Optional[types.TypeMessageFwdHeader] = None,
via_bot_id: Optional[int] = None,
via_business_bot_id: Optional[int] = None,
reply_to: Optional[types.TypeMessageReplyHeader] = None,
media: Optional[types.TypeMessageMedia] = None,
reply_markup: Optional[types.TypeReplyMarkup] = None,
entities: Optional[List[types.TypeMessageEntity]] = None,
views: Optional[int] = None,
forwards: Optional[int] = None,
replies: Optional[types.TypeMessageReplies] = None,
edit_date: Optional[datetime] = None,
post_author: Optional[str] = None,
grouped_id: Optional[int] = None,
reactions: Optional[types.TypeMessageReactions] = None,
restriction_reason: Optional[List[types.TypeRestrictionReason]] = None,
ttl_period: Optional[int] = None,
quick_reply_shortcut_id: Optional[int] = None,
effect: Optional[int] = None,
factcheck: Optional[types.TypeFactCheck] = None,
report_delivery_until_date: Optional[datetime] = None,
paid_message_stars: Optional[int] = None,
suggested_post: Optional[types.TypeSuggestedPost] = None,
# Copied from MessageService.__init__ signature
action: Optional[types.TypeMessageAction] = None,
reactions_are_possible: Optional[bool] = None,
):
# Common properties to messages, then to service (in the order they're defined in the `.tl`)
# Copied from Message.__init__ body
self.id = id
self.peer_id = peer_id
self.date = date
self.message = message
self.out = bool(out)
self.mentioned = mentioned
self.media_unread = media_unread
@ -209,27 +236,41 @@ class Message(ChatGetter, SenderGetter, TLObject):
self.from_scheduled = from_scheduled
self.legacy = legacy
self.edit_hide = edit_hide
self.id = id
self.pinned = pinned
self.noforwards = noforwards
self.invert_media = invert_media
self.offline = offline
self.video_processing_pending = video_processing_pending
self.paid_suggested_post_stars = paid_suggested_post_stars
self.paid_suggested_post_ton = paid_suggested_post_ton
self.from_id = from_id
self.peer_id = peer_id
self.from_boosts_applied = from_boosts_applied
self.saved_peer_id = saved_peer_id
self.fwd_from = fwd_from
self.via_bot_id = via_bot_id
self.via_business_bot_id = via_business_bot_id
self.reply_to = reply_to
self.date = date
self.message = message
self.media = None if isinstance(media, types.MessageMediaEmpty) else media
self.media = None if isinstance(media, types.MessageMediaEmpty) else media
self.reply_markup = reply_markup
self.entities = entities
self.views = views
self.forwards = forwards
self.replies = replies
self.edit_date = edit_date
self.pinned = pinned
self.post_author = post_author
self.grouped_id = grouped_id
self.reactions = reactions
self.restriction_reason = restriction_reason
self.ttl_period = ttl_period
self.quick_reply_shortcut_id = quick_reply_shortcut_id
self.effect = effect
self.factcheck = factcheck
self.report_delivery_until_date = report_delivery_until_date
self.paid_message_stars = paid_message_stars
self.suggested_post = suggested_post
# Copied from MessageService.__init__ body
self.action = action
self.reactions_are_possible = reactions_are_possible
# Convenient storage for custom functions
# TODO This is becoming a bit of bloat
@ -261,6 +302,8 @@ class Message(ChatGetter, SenderGetter, TLObject):
SenderGetter.__init__(self, sender_id)
self._forward = None
self._reply_to_chat = None
self._reply_to_sender = None
def _finish_init(self, client, entities, input_chat):
"""
@ -275,7 +318,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
if self.peer_id == types.PeerUser(client._self_id) and not self.fwd_from:
self.out = True
cache = client._entity_cache
cache = client._mb_entity_cache
self._sender, self._input_sender = utils._get_entity_pair(
self.sender_id, entities, cache)
@ -313,6 +356,14 @@ class Message(ChatGetter, SenderGetter, TLObject):
if self.replies and self.replies.channel_id:
self._linked_chat = entities.get(utils.get_peer_id(
types.PeerChannel(self.replies.channel_id)))
if isinstance(self.reply_to, types.MessageReplyHeader):
if self.reply_to.reply_to_peer_id:
self._reply_to_chat = entities.get(utils.get_peer_id(self.reply_to.reply_to_peer_id))
if self.reply_to.reply_from:
if self.reply_to.reply_from.from_id:
self._reply_to_sender = entities.get(utils.get_peer_id(self.reply_to.reply_from.from_id))
# endregion Initialization
@ -373,10 +424,11 @@ class Message(ChatGetter, SenderGetter, TLObject):
@property
def is_reply(self):
"""
`True` if the message is a reply to some other message.
`True` if the message is a reply to some other message or story.
Remember that you can access the ID of the message
this one is replying to through `reply_to.reply_to_msg_id`,
Remember that if the replied-to is a message,
you can access the ID of the message this one is
replying to through `reply_to.reply_to_msg_id`,
and the `Message` object with `get_reply_message()`.
"""
return self.reply_to is not None
@ -389,6 +441,22 @@ class Message(ChatGetter, SenderGetter, TLObject):
"""
return self._forward
@property
def reply_to_chat(self):
"""
The :tl:`Channel` in which the replied-to message was sent,
if this message is a reply in another chat
"""
return self._reply_to_chat
@property
def reply_to_sender(self):
"""
The :tl:`User`, :tl:`Channel`, or whatever other entity that
sent the replied-to message, if this message is a reply in another chat.
"""
return self._reply_to_sender
@property
def buttons(self):
"""
@ -649,7 +717,11 @@ class Message(ChatGetter, SenderGetter, TLObject):
Returns the message ID this message is replying to, if any.
This is equivalent to accessing ``.reply_to.reply_to_msg_id``.
"""
return self.reply_to.reply_to_msg_id if self.reply_to else None
return (
self.reply_to.reply_to_msg_id
if isinstance(self.reply_to, types.MessageReplyHeader)
else None
)
@property
def to_id(self):
@ -715,7 +787,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
The result will be cached after its first use.
"""
if self._reply_message is None and self._client:
if not self.reply_to:
if not isinstance(self.reply_to, types.MessageReplyHeader):
return None
# Bots cannot access other bots' messages by their ID.
@ -774,12 +846,26 @@ class Message(ChatGetter, SenderGetter, TLObject):
async def edit(self, *args, **kwargs):
"""
Edits the message iff it's outgoing. Shorthand for
Edits the message if it's outgoing. Shorthand for
`telethon.client.messages.MessageMethods.edit_message`
with both ``entity`` and ``message`` already set.
Returns `None` if the message was incoming,
or the edited `Message` otherwise.
Returns
The edited `Message <telethon.tl.custom.message.Message>`,
unless `entity` was a :tl:`InputBotInlineMessageID` or :tl:`InputBotInlineMessageID64` in which
case this method returns a boolean.
Raises
``MessageAuthorRequiredError`` if you're not the author of the
message but tried editing it anyway.
``MessageNotModifiedError`` if the contents of the message were
not modified at all.
``MessageIdInvalidError`` if the ID of the message is invalid
(the ID itself may be correct, but the message with that ID
cannot be edited). For example, when trying to edit messages
with a reply markup (or clear markup) this error will be raised.
.. note::
@ -793,9 +879,6 @@ class Message(ChatGetter, SenderGetter, TLObject):
This is generally the most desired and convenient behaviour,
and will work for link previews and message buttons.
"""
if self.fwd_from or not self.out or not self._client:
return None # We assume self.out was patched for our chat
if 'link_preview' not in kwargs:
kwargs['link_preview'] = bool(self.web_preview)
@ -838,7 +921,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
async def click(self, i=None, j=None,
*, text=None, filter=None, data=None, share_phone=None,
share_geo=None, password=None):
share_geo=None, password=None, open_url=None):
"""
Calls :tl:`SendVote` with the specified poll option
or `button.click <telethon.tl.custom.messagebutton.MessageButton.click>`
@ -922,7 +1005,13 @@ class Message(ChatGetter, SenderGetter, TLObject):
button to transfer ownership), if your account has 2FA enabled,
you need to provide your account's password. Otherwise,
`teltehon.errors.PasswordHashInvalidError` is raised.
open_url (`bool`):
When clicking on an inline keyboard URL button :tl:`KeyboardButtonUrl`
By default it will return URL of the button, passing ``click(open_url=True)``
will lunch the default browser with given URL of the button and
return `True` on success.
Example:
.. code-block:: python
@ -952,7 +1041,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
but = types.KeyboardButtonCallback('', data)
return await MessageButton(self._client, but, chat, None, self.id).click(
share_phone=share_phone, share_geo=share_geo, password=password)
share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)
if sum(int(x is not None) for x in (i, text, filter)) >= 2:
raise ValueError('You can only set either of i, text or filter')
@ -1025,7 +1114,7 @@ class Message(ChatGetter, SenderGetter, TLObject):
button = find_button()
if button:
return await button.click(
share_phone=share_phone, share_geo=share_geo, password=password)
share_phone=share_phone, share_geo=share_geo, password=password, open_url=open_url)
async def mark_read(self):
"""
@ -1128,8 +1217,9 @@ class Message(ChatGetter, SenderGetter, TLObject):
return bot
else:
try:
return self._client._entity_cache[self.via_bot_id]
except KeyError:
return self._client._mb_entity_cache.get(
utils.resolve_id(self.via_bot_id)[0])._as_input_peer()
except AttributeError:
raise ValueError('No input sender') from None
def _document_by_attribute(self, kind, condition=None):

View File

@ -1,7 +1,11 @@
from .. import types, functions
from ... import password as pwd_mod
from ...errors import BotResponseTimeoutError
import webbrowser
try:
import webbrowser
except ModuleNotFoundError:
pass
import sys
import os
@ -61,7 +65,7 @@ class MessageButton:
if isinstance(self.button, types.KeyboardButtonUrl):
return self.button.url
async def click(self, share_phone=None, share_geo=None, *, password=None):
async def click(self, share_phone=None, share_geo=None, *, password=None, open_url=None):
"""
Emulates the behaviour of clicking this button.
@ -75,7 +79,8 @@ class MessageButton:
:tl:`StartBotRequest` will be invoked and the resulting updates
returned.
If it's a :tl:`KeyboardButtonUrl`, the URL of the button will
If it's a :tl:`KeyboardButtonUrl`, the ``URL`` of the button will
be returned. If you pass ``open_url=True`` the URL of the button will
be passed to ``webbrowser.open`` and return `True` on success.
If it's a :tl:`KeyboardButtonRequestPhone`, you must indicate that you
@ -112,7 +117,10 @@ class MessageButton:
bot=self._bot, peer=self._chat, start_param=self.button.query
))
elif isinstance(self.button, types.KeyboardButtonUrl):
return webbrowser.open(self.button.url)
if open_url:
if "webbrowser" in sys.modules:
return webbrowser.open(self.button.url)
return self.button.url
elif isinstance(self.button, types.KeyboardButtonGame):
req = functions.messages.GetBotCallbackAnswerRequest(
peer=self._chat, msg_id=self._msg_id, game=True

View File

@ -113,7 +113,7 @@ class QRLogin:
if isinstance(resp, types.auth.LoginTokenSuccess):
user = resp.authorization.user
self._client._on_login(user)
await self._client._on_login(user)
return user
raise TypeError('Login token response was unexpected: {}'.format(resp))

View File

@ -1,5 +1,7 @@
import abc
from ... import utils
class SenderGetter(abc.ABC):
"""
@ -46,11 +48,14 @@ class SenderGetter(abc.ABC):
# cached information, they may use the property instead.
if (self._sender is None or getattr(self._sender, 'min', None)) \
and await self.get_input_sender():
try:
self._sender =\
await self._client.get_entity(self._input_sender)
except ValueError:
await self._refetch_sender()
# self.get_input_sender may refresh in which case the sender may no longer be min
# However it could still incur a cost so the cheap check is done twice instead.
if self._sender is None or getattr(self._sender, 'min', None):
try:
self._sender =\
await self._client.get_entity(self._input_sender)
except ValueError:
await self._refetch_sender()
return self._sender
@property
@ -66,9 +71,9 @@ class SenderGetter(abc.ABC):
"""
if self._input_sender is None and self._sender_id and self._client:
try:
self._input_sender = \
self._client._entity_cache[self._sender_id]
except KeyError:
self._input_sender = self._client._mb_entity_cache.get(
utils.resolve_id(self._sender_id)[0])._as_input_peer()
except AttributeError:
pass
return self._input_sender

1
telethon/types.py Normal file
View File

@ -0,0 +1 @@
from .tl.types import *

View File

@ -4,7 +4,6 @@ to convert between an entity like a User, Chat, etc. into its Input version)
"""
import base64
import binascii
import imghdr
import inspect
import io
import itertools
@ -15,6 +14,7 @@ import os
import pathlib
import re
import struct
import warnings
from collections import namedtuple
from mimetypes import guess_extension
from types import GeneratorType
@ -54,20 +54,14 @@ mimetypes.add_type('audio/flac', '.flac')
mimetypes.add_type('application/x-tgsticker', '.tgs')
USERNAME_RE = re.compile(
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|joinchat/)?'
r'@|(?:https?://)?(?:www\.)?(?:telegram\.(?:me|dog)|t\.me)/(@|\+|joinchat/)?'
)
TG_JOIN_RE = re.compile(
r'tg://(join)\?invite='
)
# The only shorter-than-five-characters usernames are those used for some
# special, very well known bots. This list may be incomplete though:
# "[...] @gif, @vid, @pic, @bing, @wiki, @imdb and @bold [...]"
#
# See https://telegram.org/blog/inline-bots#how-does-it-work
VALID_USERNAME_RE = re.compile(
r'^([a-z](?:(?!__)\w){3,30}[a-z\d]'
r'|gif|vid|pic|bing|wiki|imdb|bold|vote|like|coub)$',
r'^[a-z](?:(?!__)\w){1,30}[a-z\d]$',
re.IGNORECASE
)
@ -102,7 +96,8 @@ def get_display_name(entity):
else:
return ''
elif isinstance(entity, (types.Chat, types.ChatForbidden, types.Channel)):
elif isinstance(entity, (
types.Chat, types.ChatForbidden, types.Channel, types.ChannelForbidden)):
return entity.title
return ''
@ -443,15 +438,16 @@ def get_input_media(
if media.SUBCLASS_OF_ID == 0xfaf846f4: # crc32(b'InputMedia')
return media
elif media.SUBCLASS_OF_ID == 0x846363e0: # crc32(b'InputPhoto')
return types.InputMediaPhoto(media, ttl_seconds=ttl)
return types.InputMediaPhoto(media, ttl_seconds=ttl, spoiler=media.spoiler)
elif media.SUBCLASS_OF_ID == 0xf33fdb68: # crc32(b'InputDocument')
return types.InputMediaDocument(media, ttl_seconds=ttl)
return types.InputMediaDocument(media, ttl_seconds=ttl, spoiler=media.spoiler)
except AttributeError:
_raise_cast_fail(media, 'InputMedia')
if isinstance(media, types.MessageMediaPhoto):
return types.InputMediaPhoto(
id=get_input_photo(media.photo),
spoiler=media.spoiler,
ttl_seconds=ttl or media.ttl_seconds
)
@ -506,6 +502,14 @@ def get_input_media(
if isinstance(media, types.MessageMediaGeo):
return types.InputMediaGeoPoint(geo_point=get_input_geo(media.geo))
if isinstance(media, types.MessageMediaGeoLive):
return types.InputMediaGeoLive(
geo_point=get_input_geo(media.geo),
period=media.period,
heading=media.heading,
proximity_notification_radius=media.proximity_notification_radius,
)
if isinstance(media, types.MessageMediaVenue):
return types.InputMediaVenue(
geo_point=get_input_geo(media.geo),
@ -583,11 +587,14 @@ def _get_entity_pair(entity_id, entities, cache,
"""
Returns ``(entity, input_entity)`` for the given entity ID.
"""
if not entity_id:
return None, None
entity = entities.get(entity_id)
try:
input_entity = cache[entity_id]
except KeyError:
# KeyError is unlikely, so another TypeError won't hurt
input_entity = cache.get(resolve_id(entity_id)[0])._as_input_peer()
except AttributeError:
# AttributeError is unlikely, so another TypeError won't hurt
try:
input_entity = get_input_peer(entity)
except TypeError:
@ -604,6 +611,9 @@ def get_message_id(message):
if isinstance(message, int):
return message
if isinstance(message, types.InputMessageID):
return message.id
try:
if message.SUBCLASS_OF_ID == 0x790009e3:
# hex(crc32(b'Message')) = 0x790009e3
@ -760,7 +770,10 @@ def sanitize_parse_mode(mode):
if not mode:
return None
if callable(mode):
if (all(hasattr(mode, x) for x in ('parse', 'unparse'))
and all(callable(x) for x in (mode.parse, mode.unparse))):
return mode
elif callable(mode):
class CustomMode:
@staticmethod
def unparse(text, entities):
@ -768,9 +781,6 @@ def sanitize_parse_mode(mode):
CustomMode.parse = mode
return CustomMode
elif (all(hasattr(mode, x) for x in ('parse', 'unparse'))
and all(callable(x) for x in (mode.parse, mode.unparse))):
return mode
elif isinstance(mode, str):
try:
return {
@ -838,12 +848,6 @@ def _get_extension(file):
return os.path.splitext(file)[-1]
elif isinstance(file, pathlib.Path):
return file.suffix
elif isinstance(file, bytes):
kind = imghdr.what(io.BytesIO(file))
return ('.' + kind) if kind else ''
elif isinstance(file, io.IOBase) and not isinstance(file, io.TextIOBase) and file.seekable():
kind = imghdr.what(file)
return ('.' + kind) if kind is not None else ''
elif getattr(file, 'name', None):
# Note: ``file.name`` works for :tl:`InputFile` and some `IOBase`
return _get_extension(file.name)
@ -906,7 +910,7 @@ def is_list_like(obj):
enough. Things like ``open()`` are also iterable (and probably many
other things), so just support the commonly known list-like objects.
"""
return isinstance(obj, (list, tuple, set, dict, GeneratorType))
return isinstance(obj, (list, tuple, set, dict, range, GeneratorType))
def parse_phone(phone):
@ -1341,10 +1345,7 @@ def get_appropriated_part_size(file_size):
return 128
if file_size <= 786432000: # 750MB
return 256
if file_size <= 2097152000: # 2000MB
return 512
raise ValueError('File size too large')
return 512
def encode_waveform(waveform):
@ -1557,3 +1558,11 @@ def _photo_size_byte_count(size):
return max(size.sizes)
else:
return None
async def maybe_async(coro):
result = coro
if inspect.isawaitable(result):
warnings.warn('Using async sessions support is an experimental feature')
result = await result
return result

View File

@ -1,3 +1,3 @@
# Versions should comply with PEP440.
# This line is parsed in setup.py:
__version__ = '1.24.0'
__version__ = '1.40.0'

View File

@ -136,7 +136,7 @@ assumes some [`asyncio`] knowledge, but otherwise is easy to follow.
![Screenshot of the tkinter GUI][tkinter GUI]
### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/payment.py)
### [`payment.py`](https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/payment.py)
* Usable as: **bot**.
* Difficulty: **medium**.
@ -150,18 +150,18 @@ It makes use of the ["raw API"](https://tl.telethon.dev) (that is, no friendly `
[Telethon]: https://github.com/LonamiWebs/Telethon
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/LICENSE
[CC0 License]: https://github.com/LonamiWebs/Telethon/blob/v1/telethon_examples/LICENSE
[@BotFather]: https://t.me/BotFather
[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/assistant.py
[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/quart_login.py
[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/gui.py
[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/interactive_telegram_client.py
[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_messages.py
[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/print_updates.py
[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/replier.py
[`assistant.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/assistant.py
[`quart_login.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/quart_login.py
[`gui.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/gui.py
[`interactive_telegram_client.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/interactive_telegram_client.py
[`print_messages.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_messages.py
[`print_updates.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/print_updates.py
[`replier.py`]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/replier.py
[@TelethonianBot]: https://t.me/TelethonianBot
[official Telethon's chat]: https://t.me/TelethonChat
[`asyncio`]: https://docs.python.org/3/library/asyncio.html
[`tkinter`]: https://docs.python.org/3/library/tkinter.html
[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_examples/screenshot-gui.jpg
[`events.NewMessage`]: https://docs.telethon.dev/en/latest/modules/events.html#telethon.events.newmessage.NewMessage
[tkinter GUI]: https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_examples/screenshot-gui.jpg
[`events.NewMessage`]: https://docs.telethon.dev/en/stable/modules/events.html#telethon.events.newmessage.NewMessage

View File

@ -53,7 +53,7 @@ def callback(func):
def wrapped(*args, **kwargs):
result = func(*args, **kwargs)
if inspect.iscoroutine(result):
aio_loop.create_task(result)
asyncio.create_task(result)
return wrapped
@ -369,10 +369,4 @@ async def main(interval=0.05):
if __name__ == "__main__":
# Some boilerplate code to set up the main method
aio_loop = asyncio.get_event_loop()
try:
aio_loop.run_until_complete(main())
finally:
if not aio_loop.is_closed():
aio_loop.close()
asyncio.run(main())

View File

@ -9,9 +9,6 @@ from telethon.errors import SessionPasswordNeededError
from telethon.network import ConnectionTcpAbridged
from telethon.utils import get_display_name
# Create a global variable to hold the loop we will be using
loop = asyncio.get_event_loop()
def sprint(string, *args, **kwargs):
"""Safe Print (handle UnicodeEncodeErrors on some terminals)"""
@ -50,7 +47,7 @@ async def async_input(prompt):
let the loop run while we wait for input.
"""
print(prompt, end='', flush=True)
return (await loop.run_in_executor(None, sys.stdin.readline)).rstrip()
return (await asyncio.get_running_loop().run_in_executor(None, sys.stdin.readline)).rstrip()
def get_env(name, message, cast=str):
@ -109,34 +106,34 @@ class InteractiveTelegramClient(TelegramClient):
# media known the message ID, for every message having media.
self.found_media = {}
async def init(self):
# Calling .connect() may raise a connection error False, so you need
# to except those before continuing. Otherwise you may want to retry
# as done here.
print('Connecting to Telegram servers...')
try:
loop.run_until_complete(self.connect())
await self.connect()
except IOError:
# We handle IOError and not ConnectionError because
# PySocks' errors do not subclass ConnectionError
# (so this will work with and without proxies).
print('Initial connection failed. Retrying...')
loop.run_until_complete(self.connect())
await self.connect()
# If the user hasn't called .sign_in() or .sign_up() yet, they won't
# If the user hasn't called .sign_in() yet, they won't
# be authorized. The first thing you must do is authorize. Calling
# .sign_in() should only be done once as the information is saved on
# the *.session file so you don't need to enter the code every time.
if not loop.run_until_complete(self.is_user_authorized()):
if not await self.is_user_authorized():
print('First run. Sending code request...')
user_phone = input('Enter your phone: ')
loop.run_until_complete(self.sign_in(user_phone))
await self.sign_in(user_phone)
self_user = None
while self_user is None:
code = input('Enter the code you just received: ')
try:
self_user =\
loop.run_until_complete(self.sign_in(code=code))
self_user = await self.sign_in(code=code)
# Two-step verification may be enabled, and .sign_in will
# raise this error. If that's the case ask for the password.
@ -146,8 +143,7 @@ class InteractiveTelegramClient(TelegramClient):
pw = getpass('Two step verification is enabled. '
'Please enter your password: ')
self_user =\
loop.run_until_complete(self.sign_in(password=pw))
self_user = await self.sign_in(password=pw)
async def run(self):
"""Main loop of the TelegramClient, will wait for user action"""
@ -397,9 +393,14 @@ class InteractiveTelegramClient(TelegramClient):
))
if __name__ == '__main__':
async def main():
SESSION = os.environ.get('TG_SESSION', 'interactive')
API_ID = get_env('TG_API_ID', 'Enter your API ID: ', int)
API_HASH = get_env('TG_API_HASH', 'Enter your API hash: ')
client = InteractiveTelegramClient(SESSION, API_ID, API_HASH)
loop.run_until_complete(client.run())
await client.init()
await client.run()
if __name__ == '__main__':
asyncio.run()

View File

@ -7,8 +7,6 @@ import os
import time
import sys
loop = asyncio.get_event_loop()
"""
Provider token can be obtained via @BotFather. more info at https://core.telegram.org/bots/payments#getting-a-token
@ -85,9 +83,9 @@ async def payment_received_handler(event):
payment: types.MessageActionPaymentSentMe = event.message.action
# do something after payment was received
if payment.payload.decode('UTF-8') == 'product A':
await bot.send_message(event.message.from_id, 'Thank you for buying product A!')
await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product A!')
elif payment.payload.decode('UTF-8') == 'product B':
await bot.send_message(event.message.from_id, 'Thank you for buying product B!')
await bot.send_message(event.message.peer_id.user_id, 'Thank you for buying product B!')
raise events.StopPropagation
@ -180,4 +178,4 @@ if __name__ == '__main__':
if not provider_token:
logger.error("No provider token supplied.")
exit(1)
loop.run_until_complete(main())
asyncio.run(main())

View File

@ -1,7 +1,6 @@
import base64
import os
import hypercorn.asyncio
from quart import Quart, render_template_string, request
from telethon import TelegramClient, utils
@ -82,6 +81,8 @@ async def format_message(message):
# Connect the client before we start serving with Quart
@app.before_serving
async def startup():
# After connecting, the client will create additional asyncio tasks that run until it's disconnected again.
# Be careful to not mix different asyncio loops during a client's lifetime, or things won't work properly!
await client.connect()
@ -129,24 +130,11 @@ async def root():
return await render_template_string(BASE_TEMPLATE, content=CODE_FORM)
async def main():
await hypercorn.asyncio.serve(app, hypercorn.Config())
# By default, `Quart.run` uses `asyncio.run()`, which creates a new asyncio
# event loop. If we create the `TelegramClient` before, `telethon` will
# use `asyncio.get_event_loop()`, which is the implicit loop in the main
# thread. These two loops are different, and it won't work.
# event loop. If we had connected the `TelegramClient` before, `telethon` will
# use `asyncio.get_running_loop()` to create some additional tasks. If these
# loops are different, it won't work.
#
# So, we have to manually pass the same `loop` to both applications to
# make 100% sure it works and to avoid headaches.
#
# To run Quart inside `async def`, we must use `hypercorn.asyncio.serve()`
# directly.
#
# This example creates a global client outside of Quart handlers.
# If you create the client inside the handlers (common case), you
# won't have to worry about any of this, but it's still good to be
# explicit about the event loop.
# To keep things simple, be sure to not create multiple asyncio loops!
if __name__ == '__main__':
client.loop.run_until_complete(main())
app.run()

File diff suppressed because it is too large Load Diff

View File

@ -5,14 +5,15 @@ ACCESS_TOKEN_EXPIRED,400,Bot token expired
ACCESS_TOKEN_INVALID,400,The provided token is not valid
ACTIVE_USER_REQUIRED,401,The method is only available to already activated users
ADMINS_TOO_MUCH,400,Too many admins
ADMIN_ID_INVALID,400,The specified admin ID is invalid
ADMIN_RANK_EMOJI_NOT_ALLOWED,400,Emoji are not allowed in admin titles or ranks
ADMIN_RANK_INVALID,400,The given admin title or rank was invalid (possibly larger than 16 characters)
ALBUM_PHOTOS_TOO_MANY,400,Too many photos were included in the album
API_ID_INVALID,400,The api_id/api_hash combination is invalid
API_ID_PUBLISHED_FLOOD,400,"This API id was published somewhere, you can't use it now"
ARTICLE_TITLE_EMPTY,400,The title of the article is empty
AUDIO_CONTENT_URL_EMPTY,400,The remote URL specified in the content field is empty
AUDIO_TITLE_EMPTY,400,The title attribute of the audio must be non-empty
AUDIO_CONTENT_URL_EMPTY,400,
AUTH_BYTES_INVALID,400,The provided authorization is invalid
AUTH_KEY_DUPLICATED,406,"The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions"
AUTH_KEY_INVALID,401,The key is invalid
@ -20,17 +21,19 @@ AUTH_KEY_PERM_EMPTY,401,"The method is unavailable for temporary authorization k
AUTH_KEY_UNREGISTERED,401,The key is not registered in the system
AUTH_RESTART,500,Restart the authorization process
AUTH_TOKEN_ALREADY_ACCEPTED,400,The authorization token was already used
AUTH_TOKEN_EXCEPTION,400,An error occurred while importing the auth token
AUTH_TOKEN_EXPIRED,400,The provided authorization token has expired and the updated QR-code must be re-scanned
AUTH_TOKEN_INVALID,400,An invalid authorization token was provided
AUTH_TOKEN_INVALID2,400,An invalid authorization token was provided
AUTH_TOKEN_INVALIDX,400,The specified auth token is invalid
AUTOARCHIVE_NOT_AVAILABLE,400,You cannot use this feature yet
BANK_CARD_NUMBER_INVALID,400,Incorrect credit card number
BASE_PORT_LOC_INVALID,400,Base port location invalid
BANNED_RIGHTS_INVALID,400,"You cannot use that set of permissions in this request, i.e. restricting view_messages as a default"
BASE_PORT_LOC_INVALID,400,Base port location invalid
BOTS_TOO_MUCH,400,There are too many bots in this chat/channel
BOT_ONESIDE_NOT_AVAIL,400,
BOT_CHANNELS_NA,400,Bots can't edit admin privileges
BOT_COMMAND_DESCRIPTION_INVALID,400,"The command description was empty, too long or had invalid characters used"
BOT_COMMAND_INVALID,400,
BOT_COMMAND_INVALID,400,The specified command is invalid
BOT_DOMAIN_INVALID,400,The domain used for the auth button does not match the one configured in @BotFather
BOT_GAMES_DISABLED,400,Bot games cannot be used in this type of chat
BOT_GROUPS_BLOCKED,400,This bot can't be added to groups
@ -38,73 +41,100 @@ BOT_INLINE_DISABLED,400,This bot can't be used in inline mode
BOT_INVALID,400,This is not a valid bot
BOT_METHOD_INVALID,400,The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot
BOT_MISSING,400,This method can only be run by a bot
BOT_ONESIDE_NOT_AVAIL,400,Bots can't pin messages in PM just for themselves
BOT_PAYMENTS_DISABLED,400,This method can only be run by a bot
BOT_POLLS_DISABLED,400,You cannot create polls under a bot account
BOT_RESPONSE_TIMEOUT,400,The bot did not answer to the callback query in time
BOT_SCORE_NOT_MODIFIED,400,The score wasn't modified
BROADCAST_CALLS_DISABLED,400,
BROADCAST_FORBIDDEN,403,The request cannot be used in broadcast channels
BROADCAST_ID_INVALID,400,The channel is invalid
BROADCAST_PUBLIC_VOTERS_FORBIDDEN,400,You cannot broadcast polls where the voters are public
BROADCAST_REQUIRED,400,The request can only be used with a broadcast channel
BUTTON_DATA_INVALID,400,The provided button data is invalid
BUTTON_TEXT_INVALID,400,The specified button text is invalid
BUTTON_TYPE_INVALID,400,The type of one of the buttons you provided is invalid
BUTTON_URL_INVALID,400,Button URL invalid
BUTTON_USER_PRIVACY_RESTRICTED,400,The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button
CALL_ALREADY_ACCEPTED,400,The call was already accepted
CALL_ALREADY_DECLINED,400,The call was already declined
CALL_OCCUPY_FAILED,500,The call failed because the user is already making another call
CALL_PEER_INVALID,400,The provided call peer object is invalid
CALL_PROTOCOL_FLAGS_INVALID,400,Call protocol flags invalid
CDN_METHOD_INVALID,400,This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods
CDN_UPLOAD_TIMEOUT,500,A server-side timeout occurred while reuploading the file to the CDN DC
CHANNELS_ADMIN_LOCATED_TOO_MUCH,400,The user has reached the limit of public geogroups
CHANNELS_ADMIN_PUBLIC_TOO_MUCH,400,"You're admin of too many public channels, make some channels private to change the username of this channel"
CHANNELS_TOO_MUCH,400,You have joined too many channels/supergroups
CHANNEL_BANNED,400,The channel is banned
CHANNEL_FORUM_MISSING,400,
CHANNEL_ID_INVALID,400,The specified supergroup ID is invalid
CHANNEL_INVALID,400,"Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited"
CHANNEL_PRIVATE,400,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
CHANNEL_PARICIPANT_MISSING,400,The current user is not in the channel
CHANNEL_PRIVATE,400 406,The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
CHANNEL_PUBLIC_GROUP_NA,403,channel/supergroup not available
CHANNEL_TOO_LARGE,406,
CHANNEL_TOO_BIG,400,
CHANNEL_TOO_LARGE,400 406,Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change)
CHAT_ABOUT_NOT_MODIFIED,400,About text has not changed
CHAT_ABOUT_TOO_LONG,400,Chat about too long
CHAT_ADMIN_INVITE_REQUIRED,403,You do not have the rights to do this
CHAT_ADMIN_REQUIRED,400,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group"
CHAT_ADMIN_REQUIRED,400 403,"Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group"
CHAT_DISCUSSION_UNALLOWED,400,
CHAT_FORBIDDEN,403,You cannot write in this chat
CHAT_FORWARDS_RESTRICTED,400 406,You can't forward messages from a protected chat
CHAT_GET_FAILED,500,
CHAT_GUEST_SEND_FORBIDDEN,403,"You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info"
CHAT_ID_EMPTY,400,The provided chat ID is empty
CHAT_ID_GENERATE_FAILED,500,Failure while generating the chat ID
CHAT_ID_INVALID,400,"Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead"
CHAT_INVALID,400,The chat is invalid for this request
CHAT_INVITE_PERMANENT,400,You can't set an expiration date on permanent invite links
CHAT_LINK_EXISTS,400,The chat is linked to a channel and cannot be used in that request
CHAT_NOT_MODIFIED,400,"The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)"
CHAT_RESTRICTED,400,The chat is restricted and cannot be used in that request
CHAT_REVOKE_DATE_UNSUPPORTED,400,`min_date` and `max_date` are not available for using with non-user peers
CHAT_SEND_GAME_FORBIDDEN,403,You can't send a game to this chat
CHAT_SEND_GIFS_FORBIDDEN,403,You can't send gifs in this chat
CHAT_SEND_INLINE_FORBIDDEN,400,You cannot send inline results in this chat
CHAT_SEND_INLINE_FORBIDDEN,400 403,You cannot send inline results in this chat
CHAT_SEND_MEDIA_FORBIDDEN,403,You can't send media in this chat
CHAT_SEND_POLL_FORBIDDEN,403,You can't send polls in this chat
CHAT_SEND_STICKERS_FORBIDDEN,403,You can't send stickers in this chat
CHAT_TITLE_EMPTY,400,No chat title provided
CHAT_TOO_BIG,400,
CHAT_TOO_BIG,400,"This method is not available for groups with more than `chat_read_mark_size_threshold` members, [see client configuration](https://core.telegram.org/api/config#client-configuration)"
CHAT_WRITE_FORBIDDEN,403,You can't write in this chat
CHP_CALL_FAIL,500,The statistics cannot be retrieved at this time
CODE_EMPTY,400,The provided code is empty
CODE_HASH_INVALID,400,Code hash invalid
CODE_INVALID,400,Code invalid (i.e. from email)
CONNECTION_API_ID_INVALID,400,The provided API id is invalid
CONNECTION_APP_VERSION_EMPTY,400,App version is empty
CONNECTION_DEVICE_MODEL_EMPTY,400,Device model empty
CONNECTION_LANG_PACK_INVALID,400,"The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty"
CONNECTION_LAYER_INVALID,400,The very first request must always be InvokeWithLayerRequest
CONNECTION_NOT_INITED,400,Connection not initialized
CONNECTION_SYSTEM_EMPTY,400,Connection system empty
CONNECTION_SYSTEM_LANG_CODE_EMPTY,400,The system language string was empty during connection
CONTACT_ADD_MISSING,400,Contact to add is missing
CONTACT_ID_INVALID,400,The provided contact ID is invalid
CONTACT_NAME_EMPTY,400,The provided contact name cannot be empty
CURRENCY_TOTAL_AMOUNT_INVALID,400,
CONTACT_REQ_MISSING,400,Missing contact request
CREATE_CALL_FAILED,400,An error occurred while creating the call
CURRENCY_TOTAL_AMOUNT_INVALID,400,The total amount of all prices is invalid
DATA_INVALID,400,Encrypted data invalid
DATA_JSON_INVALID,400,The provided JSON data is invalid
DATA_TOO_LONG,400,Data too long
DATE_EMPTY,400,Date empty
DC_ID_INVALID,400,This occurs when an authorization is tried to be exported for the same data center one is currently connected to
DH_G_A_INVALID,400,g_a invalid
DOCUMENT_INVALID,400,The document file was invalid and can't be used in inline mode
EDIT_BOT_INVITE_FORBIDDEN,403,Normal users can't edit invites that were created by bots
EMAIL_HASH_EXPIRED,400,The email hash expired and cannot be used to verify it
EMAIL_INVALID,400,The given email is invalid
EMAIL_UNCONFIRMED,400,Email unconfirmed
EMAIL_UNCONFIRMED_X,400,"Email unconfirmed, the length of the code must be {code_length}"
EMOJI_INVALID,400,
EMOJI_NOT_MODIFIED,400,
EMAIL_VERIFY_EXPIRED,400,The verification email has expired
EMOJI_INVALID,400,The specified theme emoji is valid
EMOJI_NOT_MODIFIED,400,The theme wasn't changed
EMOTICON_EMPTY,400,The emoticon field cannot be empty
EMOTICON_INVALID,400,The specified emoticon cannot be used or was not a emoticon
EMOTICON_STICKERPACK_MISSING,400,The emoticon sticker pack you are trying to get is missing
@ -115,15 +145,18 @@ ENCRYPTION_DECLINED,400,The secret chat was declined
ENCRYPTION_ID_INVALID,400,The provided secret chat ID is invalid
ENCRYPTION_OCCUPY_FAILED,500,TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420
ENTITIES_TOO_LONG,400,It is no longer possible to send such long data inside entity tags (for example inline text URLs)
ENTITY_BOUNDS_INVALID,400,Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string)
ENTITY_MENTION_USER_INVALID,400,You can't use this entity
ERROR_TEXT_EMPTY,400,The provided error message is empty
EXPIRE_DATE_INVALID,400,The specified expiration date is invalid
EXPIRE_FORBIDDEN,400,
EXPORT_CARD_INVALID,400,Provided card is invalid
EXTERNAL_URL_INVALID,400,External URL invalid
FIELD_NAME_EMPTY,400,The field with the name FIELD_NAME is missing
FIELD_NAME_INVALID,400,The field with the name FIELD_NAME is invalid
FILEREF_UPGRADE_NEEDED,406,The file reference needs to be refreshed before being used again
FILE_CONTENT_TYPE_INVALID,400,
FILE_CONTENT_TYPE_INVALID,400,File content-type is invalid
FILE_EMTPY,400,An empty file was provided
FILE_ID_INVALID,400,"The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)"
FILE_MIGRATE_X,303,The file to be accessed is currently stored in DC {new_dc}
FILE_PARTS_INVALID,400,The number of file parts is invalid
@ -133,39 +166,51 @@ FILE_PART_INVALID,400,The file part number is invalid
FILE_PART_LENGTH_INVALID,400,The length of a file part is invalid
FILE_PART_SIZE_CHANGED,400,The file part size (chunk size) cannot change during upload
FILE_PART_SIZE_INVALID,400,The provided file part size is invalid
FILE_PART_TOO_BIG,400,The uploaded file part is too big
FILE_PART_X_MISSING,400,Part {which} of the file is missing from storage
FILE_REFERENCE_EMPTY,400,The file reference must exist to access the media and it cannot be empty
FILE_REFERENCE_EXPIRED,400,The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent
FILE_REFERENCE_INVALID,400,The file reference is invalid or you can't do that operation on such message
FILE_TITLE_EMPTY,400,
FILE_TITLE_EMPTY,400,An empty file title was specified
FILTER_ID_INVALID,400,The specified filter ID is invalid
FILTER_INCLUDE_EMPTY,400,The include_peers vector of the filter is empty
FILTER_NOT_SUPPORTED,400,The specified filter cannot be used in this context
FILTER_TITLE_EMPTY,400,The title field of the filter is empty
FIRSTNAME_INVALID,400,The first name is invalid
FLOOD_TEST_PHONE_WAIT_X,420,A wait of {seconds} seconds is required in the test servers
FLOOD_WAIT_X,420,A wait of {seconds} seconds is required
FLOOD_PREMIUM_WAIT_X,420,A wait of {seconds} seconds is required in non-premium accounts
FOLDER_ID_EMPTY,400,The folder you tried to delete was already empty
FOLDER_ID_INVALID,400,The folder you tried to use was not valid
FRESH_CHANGE_ADMINS_FORBIDDEN,400,Recently logged-in users cannot add or change admins
FRESH_CHANGE_ADMINS_FORBIDDEN,400 406,Recently logged-in users cannot add or change admins
FRESH_CHANGE_PHONE_FORBIDDEN,406,Recently logged-in users cannot use this request
FRESH_RESET_AUTHORISATION_FORBIDDEN,406,The current session is too new and cannot be used to reset other authorisations yet
FROM_MESSAGE_BOT_DISABLED,400,Bots can't use fromMessage min constructors
FROM_PEER_INVALID,400,The given from_user peer cannot be used for the parameter
GAME_BOT_INVALID,400,You cannot send that game with the current bot
GIF_CONTENT_TYPE_INVALID,400,
GEO_POINT_INVALID,400,Invalid geoposition provided
GIF_CONTENT_TYPE_INVALID,400,GIF content-type invalid
GIF_ID_INVALID,400,The provided GIF ID is invalid
GRAPH_INVALID_RELOAD,400,
GRAPH_EXPIRED_RELOAD,400,"This graph has expired, please obtain a new graph token"
GRAPH_INVALID_RELOAD,400,"Invalid graph token provided, please reload the stats and provide the updated token"
GRAPH_OUTDATED_RELOAD,400,"Data can't be used for the channel statistics, graphs outdated"
GROUPCALL_ADD_PARTICIPANTS_FAILED,500,
GROUPCALL_ALREADY_DISCARDED,400,
GROUPCALL_FORBIDDEN,403,
GROUPCALL_JOIN_MISSING,400,
GROUPCALL_SSRC_DUPLICATE_MUCH,400,
GROUPCALL_NOT_MODIFIED,400,
GROUPCALL_ALREADY_DISCARDED,400,The group call was already discarded
GROUPCALL_ALREADY_STARTED,403,"The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)"
GROUPCALL_FORBIDDEN,403,The group call has already ended
GROUPCALL_INVALID,400,The specified group call is invalid
GROUPCALL_JOIN_MISSING,400,You haven't joined this group call
GROUPCALL_NOT_MODIFIED,400,Group call settings weren't modified
GROUPCALL_SSRC_DUPLICATE_MUCH,400,The app needs to retry joining the group call with a new SSRC value
GROUPED_MEDIA_INVALID,400,Invalid grouped media
GROUP_CALL_INVALID,400,Group call invalid
HASH_INVALID,400,The provided hash is invalid
HIDE_REQUESTER_MISSING,400,The join request was missing or was already handled
HISTORY_GET_FAILED,500,Fetching of history failed
IMAGE_PROCESS_FAILED,400,Failure while processing image
IMPORT_FILE_INVALID,400,The file is too large to be imported
IMPORT_FORMAT_UNRECOGNIZED,400,Unknown import format
IMPORT_ID_INVALID,400,
IMPORT_ID_INVALID,400,The specified import ID is invalid
INLINE_BOT_REQUIRED,403,The action must be performed through an inline bot callback
INLINE_RESULT_EXPIRED,400,The inline query expired
INPUT_CONSTRUCTOR_INVALID,400,The provided constructor is invalid
@ -175,25 +220,32 @@ INPUT_FILTER_INVALID,400,The search query filter is invalid
INPUT_LAYER_INVALID,400,The provided layer is invalid
INPUT_METHOD_INVALID,400,The invoked method does not exist anymore or has never existed
INPUT_REQUEST_TOO_LONG,400,The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message)
INPUT_TEXT_EMPTY,400,The specified text is empty
INPUT_USER_DEACTIVATED,400,The specified user was deleted
INTERDC_X_CALL_ERROR,500,An error occurred while communicating with DC {dc}
INTERDC_X_CALL_RICH_ERROR,500,A rich error occurred while communicating with DC {dc}
INVITE_FORBIDDEN_WITH_JOINAS,400,
INVITE_FORBIDDEN_WITH_JOINAS,400,"If the user has anonymously joined a group call as a channel, they can't invite other users to the group call because that would cause deanonymization, because the invite would be sent using the original user ID, not the anonymized channel ID"
INVITE_HASH_EMPTY,400,The invite hash is empty
INVITE_HASH_EXPIRED,400,The chat the user tried to join has expired and is not valid anymore
INVITE_HASH_EXPIRED,400 406,The chat the user tried to join has expired and is not valid anymore
INVITE_HASH_INVALID,400,The invite hash is invalid
LANG_CODE_INVALID,400,
INVITE_REQUEST_SENT,400,You have successfully requested to join this chat or channel
INVITE_REVOKED_MISSING,400,The specified invite link was already revoked or is invalid
INVOICE_PAYLOAD_INVALID,400,The specified invoice payload is invalid
JOIN_AS_PEER_INVALID,400,The specified peer cannot be used to join a group call
LANG_CODE_INVALID,400,The specified language code is invalid
LANG_CODE_NOT_SUPPORTED,400,The specified language code is not supported
LANG_PACK_INVALID,400,The provided language pack is invalid
LASTNAME_INVALID,400,The last name is invalid
LIMIT_INVALID,400,An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files
LINK_NOT_MODIFIED,400,The channel is already linked to this group
LOCATION_INVALID,400,The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files
MAX_DATE_INVALID,400,The specified maximum date is invalid
MAX_ID_INVALID,400,The provided max ID is invalid
MAX_QTS_INVALID,400,The provided QTS were invalid
MD5_CHECKSUM_INVALID,400,The MD5 check-sums do not match
MEDIA_CAPTION_TOO_LONG,400,The caption is too long
MEDIA_EMPTY,400,The provided media object is invalid or the current account may not be able to send it (such as games as users)
MEDIA_GROUPED_INVALID,400,
MEDIA_GROUPED_INVALID,400,You tried to send media of different types in an album
MEDIA_INVALID,400,Media invalid
MEDIA_NEW_INVALID,400,The new media to edit the message with is invalid (such as stickers or voice notes)
MEDIA_PREV_INVALID,400,The old media cannot be edited with anything else (such as stickers or voice notes)
@ -208,13 +260,15 @@ MESSAGE_DELETE_FORBIDDEN,403,"You can't delete one of the messages you tried to
MESSAGE_EDIT_TIME_EXPIRED,400,"You can't edit this message anymore, too much time has passed since its creation."
MESSAGE_EMPTY,400,Empty or invalid UTF-8 message was sent
MESSAGE_IDS_EMPTY,400,No message ids were provided
MESSAGE_ID_INVALID,400,"The specified message ID is invalid or you can't do that operation on such message"
MESSAGE_ID_INVALID,400,The specified message ID is invalid or you can't do that operation on such message
MESSAGE_NOT_MODIFIED,400,Content of the message was not modified
MESSAGE_POLL_CLOSED,400,The poll was closed and can no longer be voted on
MESSAGE_TOO_LONG,400,Message was too long. Current maximum length is 4096 UTF-8 characters
MESSAGE_TOO_LONG,400,Message was too long
METHOD_INVALID,400,The API method is invalid and cannot be used
MIN_DATE_INVALID,400,The specified minimum date is invalid
MSGID_DECREASE_RETRY,500,The request should be retried with a lower message ID
MSG_ID_INVALID,400,The message ID used in the peer was invalid
MSG_TOO_OLD,400,"[`chat_read_mark_expire_period` seconds](https://core.telegram.org/api/config#chat-read-mark-expire-period) have passed since the message was sent, read receipts were deleted"
MSG_WAIT_FAILED,400,A waiting call returned an error
MT_SEND_QUEUE_TOO_LONG,500,
MULTI_MEDIA_TOO_LONG,400,Too many media files were included in the same album
@ -222,27 +276,32 @@ NEED_CHAT_INVALID,500,The provided chat is invalid
NEED_MEMBER_INVALID,500,The provided member is invalid or does not exist (for example a thumb size)
NETWORK_MIGRATE_X,303,The source IP address is associated with DC {new_dc}
NEW_SALT_INVALID,400,The new salt is invalid
NEW_SETTINGS_EMPTY,400,"No password is set on the current account, and no new password was specified in `new_settings`"
NEW_SETTINGS_INVALID,400,The new settings are invalid
NEXT_OFFSET_INVALID,400,The value for next_offset is invalid. Check that it has normal characters and is not too long
NOT_ALLOWED,403,
OFFSET_INVALID,400,"The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files"
OFFSET_PEER_ID_INVALID,400,The provided offset peer is invalid
OPTIONS_TOO_MUCH,400,You defined too many options for the poll
OPTION_INVALID,400,The option specified is invalid and does not exist in the target poll
PACK_SHORT_NAME_INVALID,400,"Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in ""_by_<bot username>""."
PACK_SHORT_NAME_OCCUPIED,400,A stickerpack with this name already exists
PACK_TITLE_INVALID,400,The stickerpack title is invalid
PARTICIPANTS_TOO_FEW,400,Not enough participants
PARTICIPANT_CALL_FAILED,500,Failure while making call
PARTICIPANT_JOIN_MISSING,403,
PARTICIPANT_ID_INVALID,400,The specified participant ID is invalid
PARTICIPANT_JOIN_MISSING,400 403,"Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)"
PARTICIPANT_VERSION_OUTDATED,400,The other participant does not use an up to date telegram client with support for calls
PASSWORD_EMPTY,400,The provided password is empty
PASSWORD_HASH_INVALID,400,The password (and thus its hash value) you entered is invalid
PASSWORD_MISSING,400,The account must have 2-factor authentication enabled (a password) before this method can be used
PASSWORD_RECOVERY_EXPIRED,400,
PASSWORD_RECOVERY_NA,400,
PASSWORD_RECOVERY_EXPIRED,400,The recovery code has expired
PASSWORD_RECOVERY_NA,400,"No email was set, can't recover password via email"
PASSWORD_REQUIRED,400,The account must have 2-factor authentication enabled (a password) before this method can be used
PASSWORD_TOO_FRESH_X,400,The password was added too recently and {seconds} seconds must pass before using the method
PAYMENT_PROVIDER_INVALID,400,The payment provider was not recognised or its token was invalid
PEER_FLOOD,400,Too many requests
PEER_HISTORY_EMPTY,400,
PEER_ID_INVALID,400,"An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)"
PEER_ID_NOT_SUPPORTED,400,The provided peer ID is not supported
PERSISTENT_TIMESTAMP_EMPTY,400,Persistent timestamp empty
@ -252,7 +311,9 @@ PHONE_CODE_EMPTY,400,The phone code is missing
PHONE_CODE_EXPIRED,400,The confirmation code has expired
PHONE_CODE_HASH_EMPTY,400,The phone code hash is missing
PHONE_CODE_INVALID,400,The phone code entered was invalid
PHONE_HASH_EXPIRED,400,An invalid or expired `phone_code_hash` was provided
PHONE_MIGRATE_X,303,The phone number a user is trying to use for authorization is associated with DC {new_dc}
PHONE_NOT_OCCUPIED,400,No user is associated to the specified phone number
PHONE_NUMBER_APP_SIGNUP_FORBIDDEN,400,You can't sign up using this app
PHONE_NUMBER_BANNED,400,The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam
PHONE_NUMBER_FLOOD,400,You asked for the code too many times.
@ -261,64 +322,81 @@ PHONE_NUMBER_OCCUPIED,400,The phone number is already in use
PHONE_NUMBER_UNOCCUPIED,400,The phone number is not yet being used
PHONE_PASSWORD_FLOOD,406,You have tried logging in too many times
PHONE_PASSWORD_PROTECTED,400,This phone is password protected
PHOTO_CONTENT_TYPE_INVALID,400,
PHOTO_CONTENT_TYPE_INVALID,400,Photo mime-type invalid
PHOTO_CONTENT_URL_EMPTY,400,The content from the URL used as a photo appears to be empty or has caused another HTTP error
PHOTO_CROP_FILE_MISSING,400,Photo crop file missing
PHOTO_CROP_SIZE_SMALL,400,Photo is too small
PHOTO_EXT_INVALID,400,The extension of the photo is invalid
PHOTO_FILE_MISSING,400,Profile photo file missing
PHOTO_ID_INVALID,400,Photo id is invalid
PHOTO_INVALID,400,Photo invalid
PHOTO_INVALID_DIMENSIONS,400,The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images)
PHOTO_SAVE_FILE_INVALID,400,The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally
PHOTO_THUMB_URL_EMPTY,400,The URL used as a thumbnail appears to be empty or has caused another HTTP error
PINNED_DIALOGS_TOO_MUCH,400,Too many pinned dialogs
PIN_RESTRICTED,400,You can't pin messages in private chats with other people
PINNED_DIALOGS_TOO_MUCH,400,
POLL_ANSWERS_INVALID,400,The poll did not have enough answers or had too many
POLL_ANSWER_INVALID,400,One of the poll answers is not acceptable
POLL_OPTION_DUPLICATE,400,A duplicate option was sent in the same poll
POLL_OPTION_INVALID,400,A poll option used invalid data (the data may be too long)
POLL_QUESTION_INVALID,400,The poll question was either empty or too long
POLL_UNSUPPORTED,400,This layer does not support polls in the issued method
POLL_VOTE_REQUIRED,403,
POLL_VOTE_REQUIRED,403,Cast a vote in the poll before calling this method
POSTPONED_TIMEOUT,500,The postponed call has timed out
PREMIUM_ACCOUNT_REQUIRED,403,A premium account is required to execute this action
PREMIUM_CURRENTLY_UNAVAILABLE,406,
PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN,406,"Similar to a flood wait, must wait {minutes} minutes"
PRIVACY_KEY_INVALID,400,The privacy key is invalid
PRIVACY_TOO_LONG,400,Cannot add that many entities in a single request
PRIVACY_VALUE_INVALID,400,The privacy value is invalid
PTS_CHANGE_EMPTY,500,No PTS change
PUBLIC_KEY_REQUIRED,400,
PUBLIC_CHANNEL_MISSING,403,You can only export group call invite links for public chats or channels
PUBLIC_KEY_REQUIRED,400,A public key is required
QUERY_ID_EMPTY,400,The query ID is empty
QUERY_ID_INVALID,400,The query ID is invalid
QUERY_TOO_SHORT,400,The query string is too short
QUIZ_ANSWER_MISSING,400,
QUIZ_ANSWER_MISSING,400,You can forward a quiz while hiding the original author only after choosing an option in the quiz
QUIZ_CORRECT_ANSWERS_EMPTY,400,A quiz must specify one correct answer
QUIZ_CORRECT_ANSWERS_TOO_MUCH,400,There can only be one correct answer
QUIZ_CORRECT_ANSWER_INVALID,400,The correct answer is not an existing answer
QUIZ_MULTIPLE_INVALID,400,A poll cannot be both multiple choice and quiz
RANDOM_ID_DUPLICATE,500,You provided a random ID that was already used
RANDOM_ID_EMPTY,400,Random ID empty
RANDOM_ID_INVALID,400,A provided random ID is invalid
RANDOM_LENGTH_INVALID,400,Random length invalid
RANGES_INVALID,400,Invalid range provided
REACTIONS_TOO_MANY,400,"The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)"
REACTION_EMPTY,400,No reaction provided
REACTION_INVALID,400,Invalid reaction provided (only emoji are allowed)
REFLECTOR_NOT_AVAILABLE,400,Invalid call reflector server
REG_ID_GENERATE_FAILED,500,Failure while generating registration ID
REPLY_MARKUP_BUY_EMPTY,400,Reply markup for buy button empty
REPLY_MARKUP_GAME_EMPTY,400,The provided reply markup for the game is empty
REPLY_MARKUP_INVALID,400,The provided reply markup is invalid
REPLY_MARKUP_TOO_LONG,400,The data embedded in the reply markup buttons was too much
RESET_REQUEST_MISSING,400,
RESET_REQUEST_MISSING,400,No password reset is in progress
RESULTS_TOO_MUCH,400,"You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit"
RESULT_ID_DUPLICATE,400,Duplicated IDs on the sent results. Make sure to use unique IDs
RESULT_ID_EMPTY,400,Result ID empty
RESULT_ID_INVALID,400,The given result cannot be used to send the selection to the bot
RESULT_TYPE_INVALID,400,Result type invalid
REVOTE_NOT_ALLOWED,400,You cannot change your vote
RIGHTS_NOT_MODIFIED,400,"The new admin rights are equal to the old rights, no change was made"
RIGHT_FORBIDDEN,403,Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa)
RPC_CALL_FAIL,500,"Telegram is having internal issues, please try again later."
RPC_MCGET_FAIL,500,"Telegram is having internal issues, please try again later."
RSA_DECRYPT_FAILED,400,Internal RSA decryption failed
SCHEDULE_BOT_NOT_ALLOWED,400,Bots are not allowed to schedule messages
SCHEDULE_DATE_INVALID,400,
SCHEDULE_DATE_INVALID,400,Invalid schedule date provided
SCHEDULE_DATE_TOO_LATE,400,The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours)
SCHEDULE_STATUS_PRIVATE,400,You cannot schedule a message until the person comes online if their privacy does not show this information
SCHEDULE_TOO_MUCH,400,You cannot schedule more messages in this chat (last known limit of 100 per chat)
SCORE_INVALID,400,The specified game score is invalid
SEARCH_QUERY_EMPTY,400,The search query is empty
SEARCH_WITH_LINK_NOT_SUPPORTED,400,You cannot provide a search query and an invite link at the same time
SECONDS_INVALID,400,"Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)"
SEND_AS_PEER_INVALID,400,You can't send messages as the specified peer
SEND_CODE_UNAVAILABLE,406,"Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)"
SEND_MESSAGE_MEDIA_INVALID,400,The message media was invalid or not specified
SEND_MESSAGE_TYPE_INVALID,400,The message type is invalid
SENSITIVE_CHANGE_FORBIDDEN,403,Your sensitive content settings cannot be changed at this time
@ -326,72 +404,99 @@ SESSION_EXPIRED,401,The authorization has expired
SESSION_PASSWORD_NEEDED,401,Two-steps verification is enabled and a password is required
SESSION_REVOKED,401,"The authorization has been invalidated, because of the user terminating all sessions"
SESSION_TOO_FRESH_X,400,The session logged in too recently and {seconds} seconds must pass before calling the method
SETTINGS_INVALID,400,Invalid settings were provided
SHA256_HASH_INVALID,400,The provided SHA256 hash is invalid
SHORTNAME_OCCUPY_FAILED,400,An error occurred when trying to register the short-name used for the sticker pack. Try a different name
SHORT_NAME_INVALID,400,
SHORT_NAME_OCCUPIED,400,
SHORT_NAME_INVALID,400,The specified short name is invalid
SHORT_NAME_OCCUPIED,400,The specified short name is already in use
SIGN_IN_FAILED,500,Failure while signing in
SLOWMODE_MULTI_MSGS_DISABLED,400,"Slowmode is enabled, you cannot forward multiple messages to this group"
SLOWMODE_WAIT_X,420,A wait of {seconds} seconds is required before sending another message in this chat
SRP_ID_INVALID,400,
SMS_CODE_CREATE_FAILED,400,An error occurred while creating the SMS code
SRP_ID_INVALID,400,Invalid SRP ID provided
SRP_PASSWORD_CHANGED,400,Password has changed
START_PARAM_EMPTY,400,The start parameter is empty
START_PARAM_INVALID,400,Start parameter invalid
START_PARAM_TOO_LONG,400,Start parameter is too long
STATS_MIGRATE_X,303,The channel statistics must be fetched from DC {dc}
STICKERSET_INVALID,400,The provided sticker set is invalid
STICKERPACK_STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more"
STICKERSET_INVALID,400 406,The provided sticker set is invalid
STICKERSET_OWNER_ANONYMOUS,406,This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins
STICKERS_EMPTY,400,No sticker provided
STICKERS_TOO_MUCH,400,"There are too many stickers in this stickerpack, you can't add any more"
STICKER_DOCUMENT_INVALID,400,"The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)"
STICKER_EMOJI_INVALID,400,Sticker emoji invalid
STICKER_FILE_INVALID,400,Sticker file invalid
STICKER_GIF_DIMENSIONS,400,The specified video sticker has invalid dimensions
STICKER_ID_INVALID,400,The provided sticker ID is invalid
STICKER_INVALID,400,The provided sticker is invalid
STICKER_MIME_INVALID,400,Make sure to pass a valid image file for the right InputFile parameter
STICKER_PNG_DIMENSIONS,400,Sticker png dimensions invalid
STICKER_PNG_NOPNG,400,Stickers must be a png file but the used image was not a png
STICKER_TGS_NODOC,400,
STICKER_TGS_NODOC,400,You must send the animated sticker as a document
STICKER_TGS_NOTGS,400,Stickers must be a tgs file but the used file was not a tgs
STICKER_THUMB_PNG_NOPNG,400,Stickerset thumb must be a png file but the used file was not png
STICKER_THUMB_TGS_NOTGS,400,Stickerset thumb must be a tgs file but the used file was not tgs
STICKER_VIDEO_BIG,400,The specified video sticker is too big
STICKER_VIDEO_NODOC,400,You must send the video sticker as a document
STICKER_VIDEO_NOWEBM,400,The specified video sticker is not in webm format
STORAGE_CHECK_FAILED,500,Server storage check failed
STORE_INVALID_SCALAR_TYPE,500,
SWITCH_PM_TEXT_EMPTY,400,The switch_pm.text field was empty
TAKEOUT_INIT_DELAY_X,420,A wait of {seconds} seconds is required before being able to initiate the takeout
TAKEOUT_INVALID,400,The takeout session has been invalidated by another data export session
TAKEOUT_REQUIRED,400,You must initialize a takeout request first
TAKEOUT_REQUIRED,400 403,You must initialize a takeout request first
TEMP_AUTH_KEY_ALREADY_BOUND,400,The passed temporary key is already bound to another **perm_auth_key_id**
TEMP_AUTH_KEY_EMPTY,400,No temporary auth key provided
TIMEOUT,500,A timeout occurred while fetching data from the worker
TITLE_INVALID,400,
THEME_FILE_INVALID,400,Invalid theme file provided
THEME_FORMAT_INVALID,400,Invalid theme format provided
THEME_INVALID,400,Theme invalid
THEME_MIME_INVALID,400,"You cannot create this theme, the mime-type is invalid"
THEME_TITLE_INVALID,400,The specified theme title is invalid
TIMEOUT,500,A timeout occurred while fetching data from the worker
TITLE_INVALID,400,The specified stickerpack title is invalid
TMP_PASSWORD_DISABLED,400,The temporary password is disabled
TMP_PASSWORD_INVALID,400,Password auth needs to be regenerated
TOKEN_INVALID,400,The provided token is invalid
TOPIC_DELETED,400,The topic was deleted
TO_LANG_INVALID,400,The specified destination language is invalid
TTL_DAYS_INVALID,400,The provided TTL is invalid
TTL_MEDIA_INVALID,400,The provided media cannot be used with a TTL
TTL_PERIOD_INVALID,400,The provided TTL Period is invalid
TYPES_EMPTY,400,The types field is empty
TYPE_CONSTRUCTOR_INVALID,400,The type constructor is invalid
Timedout,-503,Timeout while fetching data
Timeout,-503,Timeout while fetching data
UNKNOWN_ERROR,400,
UNKNOWN_METHOD,500,The method you tried to call cannot be called on non-CDN DCs
UNTIL_DATE_INVALID,400,That date cannot be specified in this request (try using None)
UPDATE_APP_TO_LOGIN,406,
URL_INVALID,400,The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)
USER_VOLUME_INVALID,400,
USAGE_LIMIT_INVALID,400,The specified usage limit is invalid
USERNAME_INVALID,400,"Nobody is using this username, or the username is unacceptable. If the latter, it must match r""[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"""
USERNAME_NOT_MODIFIED,400,The username is not different from the current username
USERNAME_NOT_OCCUPIED,400,The username is not in use by anyone else yet
USERNAME_OCCUPIED,400,The username is already taken
USERNAME_PURCHASE_AVAILABLE,400,
USERPIC_PRIVACY_REQUIRED,406,You need to disable privacy settings for your profile picture in order to make your geolocation public
USERPIC_UPLOAD_REQUIRED,400 406,You must have a profile picture before using this method
USERS_TOO_FEW,400,"Not enough users (to create a chat, for example)"
USERS_TOO_MUCH,400,"The maximum number of users has been exceeded (to create a chat, for example)"
USER_ADMIN_INVALID,400,Either you're not an admin or you tried to ban an admin that you didn't promote
USER_ALREADY_INVITED,400,
USER_ALREADY_INVITED,400,You have already invited this user
USER_ALREADY_PARTICIPANT,400,The authenticated user is already a participant of the chat
USER_BANNED_IN_CHANNEL,400,You're banned from sending messages in supergroups/channels
USER_BLOCKED,400,User blocked
USER_BOT,400,Bots can only be admins in channels.
USER_BOT_INVALID,400 403,This method can only be called by a bot
USER_BOT_REQUIRED,400,This method can only be called by a bot
USER_CHANNELS_TOO_MUCH,403,One of the users you tried to add is already in too many channels/supergroups
USER_CHANNELS_TOO_MUCH,400 403,One of the users you tried to add is already in too many channels/supergroups
USER_CREATOR,400,"You can't leave this channel, because you're its creator"
USER_DEACTIVATED,401,The user has been deleted/deactivated
USER_DEACTIVATED_BAN,401,The user has been deleted/deactivated
USER_DELETED,403,You can't send this secret message because the other participant deleted their account
USER_ID_INVALID,400,"Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited"
USER_INVALID,400,The given user was invalid
USER_INVALID,400 403,The given user was invalid
USER_IS_BLOCKED,400 403,User is blocked
USER_IS_BOT,400,Bots can't send messages to other bots
USER_KICKED,400,This user was kicked from this supergroup/channel
@ -399,18 +504,26 @@ USER_MIGRATE_X,303,The user whose identity is being used to execute queries is a
USER_NOT_MUTUAL_CONTACT,400 403,The provided user is not a mutual contact
USER_NOT_PARTICIPANT,400,The target user is not a member of the specified megagroup or channel
USER_PRIVACY_RESTRICTED,403,The user's privacy settings do not allow you to do this
USER_RESTRICTED,403,"You're spamreported, you can't create channels or chats."
USERPIC_UPLOAD_REQUIRED,400,You must have a profile picture before using this method
USER_RESTRICTED,403 406,"You're spamreported, you can't create channels or chats."
USER_VOLUME_INVALID,400,The specified user volume is invalid
VIDEO_CONTENT_TYPE_INVALID,400,The video content type is not supported with the given parameters (i.e. supports_streaming)
VIDEO_FILE_INVALID,400,The given video cannot be used
VIDEO_TITLE_EMPTY,400,
VIDEO_TITLE_EMPTY,400,The specified video title is empty
VOICE_MESSAGES_FORBIDDEN,400,This user's privacy settings forbid you from sending voice messages
WALLPAPER_FILE_INVALID,400,The given file cannot be used as a wallpaper
WALLPAPER_INVALID,400,The input wallpaper was not valid
WALLPAPER_MIME_INVALID,400,
WALLPAPER_MIME_INVALID,400,The specified wallpaper MIME type is invalid
WC_CONVERT_URL_INVALID,400,WC convert URL invalid
WEBDOCUMENT_MIME_INVALID,400,
WEBDOCUMENT_INVALID,400,Invalid webdocument URL provided
WEBDOCUMENT_MIME_INVALID,400,Invalid webdocument mime type provided
WEBDOCUMENT_SIZE_TOO_BIG,400,Webdocument is too big!
WEBDOCUMENT_URL_INVALID,400,The given URL cannot be used
WEBPAGE_CURL_FAILED,400,Failure while fetching the webpage with cURL
WEBPAGE_MEDIA_EMPTY,400,Webpage media empty
WEBPUSH_AUTH_INVALID,400,The specified web push authentication secret is invalid
WEBPUSH_KEY_INVALID,400,The specified web push elliptic curve Diffie-Hellman public key is invalid
WEBPUSH_TOKEN_INVALID,400,The specified web push token is invalid
WORKER_BUSY_TOO_LONG_RETRY,500,Telegram workers are too busy to respond immediately
YOU_BLOCKED_USER,400,You blocked this user
FROZEN_METHOD_INVALID,420,You tried to use a method that is not available for frozen accounts
FROZEN_PARTICIPANT_MISSING,400,Your account is frozen and can't access the chat

1 name codes description
5 ACCESS_TOKEN_INVALID 400 The provided token is not valid
6 ACTIVE_USER_REQUIRED 401 The method is only available to already activated users
7 ADMINS_TOO_MUCH 400 Too many admins
8 ADMIN_ID_INVALID 400 The specified admin ID is invalid
9 ADMIN_RANK_EMOJI_NOT_ALLOWED 400 Emoji are not allowed in admin titles or ranks
10 ADMIN_RANK_INVALID 400 The given admin title or rank was invalid (possibly larger than 16 characters)
11 ALBUM_PHOTOS_TOO_MANY 400 Too many photos were included in the album
12 API_ID_INVALID 400 The api_id/api_hash combination is invalid
13 API_ID_PUBLISHED_FLOOD 400 This API id was published somewhere, you can't use it now
14 ARTICLE_TITLE_EMPTY 400 The title of the article is empty
15 AUDIO_CONTENT_URL_EMPTY 400 The remote URL specified in the content field is empty
16 AUDIO_TITLE_EMPTY 400 The title attribute of the audio must be non-empty
AUDIO_CONTENT_URL_EMPTY 400
17 AUTH_BYTES_INVALID 400 The provided authorization is invalid
18 AUTH_KEY_DUPLICATED 406 The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions
19 AUTH_KEY_INVALID 401 The key is invalid
21 AUTH_KEY_UNREGISTERED 401 The key is not registered in the system
22 AUTH_RESTART 500 Restart the authorization process
23 AUTH_TOKEN_ALREADY_ACCEPTED 400 The authorization token was already used
24 AUTH_TOKEN_EXCEPTION 400 An error occurred while importing the auth token
25 AUTH_TOKEN_EXPIRED 400 The provided authorization token has expired and the updated QR-code must be re-scanned
26 AUTH_TOKEN_INVALID 400 An invalid authorization token was provided
27 AUTH_TOKEN_INVALID2 400 An invalid authorization token was provided
28 AUTH_TOKEN_INVALIDX 400 The specified auth token is invalid
29 AUTOARCHIVE_NOT_AVAILABLE 400 You cannot use this feature yet
30 BANK_CARD_NUMBER_INVALID 400 Incorrect credit card number
BASE_PORT_LOC_INVALID 400 Base port location invalid
31 BANNED_RIGHTS_INVALID 400 You cannot use that set of permissions in this request, i.e. restricting view_messages as a default
32 BASE_PORT_LOC_INVALID 400 Base port location invalid
33 BOTS_TOO_MUCH 400 There are too many bots in this chat/channel
BOT_ONESIDE_NOT_AVAIL 400
34 BOT_CHANNELS_NA 400 Bots can't edit admin privileges
35 BOT_COMMAND_DESCRIPTION_INVALID 400 The command description was empty, too long or had invalid characters used
36 BOT_COMMAND_INVALID 400 The specified command is invalid
37 BOT_DOMAIN_INVALID 400 The domain used for the auth button does not match the one configured in @BotFather
38 BOT_GAMES_DISABLED 400 Bot games cannot be used in this type of chat
39 BOT_GROUPS_BLOCKED 400 This bot can't be added to groups
41 BOT_INVALID 400 This is not a valid bot
42 BOT_METHOD_INVALID 400 The API access for bot users is restricted. The method you tried to invoke cannot be executed as a bot
43 BOT_MISSING 400 This method can only be run by a bot
44 BOT_ONESIDE_NOT_AVAIL 400 Bots can't pin messages in PM just for themselves
45 BOT_PAYMENTS_DISABLED 400 This method can only be run by a bot
46 BOT_POLLS_DISABLED 400 You cannot create polls under a bot account
47 BOT_RESPONSE_TIMEOUT 400 The bot did not answer to the callback query in time
48 BOT_SCORE_NOT_MODIFIED 400 The score wasn't modified
49 BROADCAST_CALLS_DISABLED 400
50 BROADCAST_FORBIDDEN 403 The request cannot be used in broadcast channels
51 BROADCAST_ID_INVALID 400 The channel is invalid
52 BROADCAST_PUBLIC_VOTERS_FORBIDDEN 400 You cannot broadcast polls where the voters are public
53 BROADCAST_REQUIRED 400 The request can only be used with a broadcast channel
54 BUTTON_DATA_INVALID 400 The provided button data is invalid
55 BUTTON_TEXT_INVALID 400 The specified button text is invalid
56 BUTTON_TYPE_INVALID 400 The type of one of the buttons you provided is invalid
57 BUTTON_URL_INVALID 400 Button URL invalid
58 BUTTON_USER_PRIVACY_RESTRICTED 400 The privacy setting of the user specified in a [inputKeyboardButtonUserProfile](/constructor/inputKeyboardButtonUserProfile) button do not allow creating such a button
59 CALL_ALREADY_ACCEPTED 400 The call was already accepted
60 CALL_ALREADY_DECLINED 400 The call was already declined
61 CALL_OCCUPY_FAILED 500 The call failed because the user is already making another call
62 CALL_PEER_INVALID 400 The provided call peer object is invalid
63 CALL_PROTOCOL_FLAGS_INVALID 400 Call protocol flags invalid
64 CDN_METHOD_INVALID 400 This method cannot be invoked on a CDN server. Refer to https://core.telegram.org/cdn#schema for available methods
65 CDN_UPLOAD_TIMEOUT 500 A server-side timeout occurred while reuploading the file to the CDN DC
66 CHANNELS_ADMIN_LOCATED_TOO_MUCH 400 The user has reached the limit of public geogroups
67 CHANNELS_ADMIN_PUBLIC_TOO_MUCH 400 You're admin of too many public channels, make some channels private to change the username of this channel
68 CHANNELS_TOO_MUCH 400 You have joined too many channels/supergroups
69 CHANNEL_BANNED 400 The channel is banned
70 CHANNEL_FORUM_MISSING 400
71 CHANNEL_ID_INVALID 400 The specified supergroup ID is invalid
72 CHANNEL_INVALID 400 Invalid channel object. Make sure to pass the right types, for instance making sure that the request is designed for channels or otherwise look for a different one more suited
73 CHANNEL_PRIVATE CHANNEL_PARICIPANT_MISSING 400 The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it The current user is not in the channel
74 CHANNEL_PRIVATE 400 406 The channel specified is private and you lack permission to access it. Another reason may be that you were banned from it
75 CHANNEL_PUBLIC_GROUP_NA 403 channel/supergroup not available
76 CHANNEL_TOO_LARGE CHANNEL_TOO_BIG 406 400
77 CHANNEL_TOO_LARGE 400 406 Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change)
78 CHAT_ABOUT_NOT_MODIFIED 400 About text has not changed
79 CHAT_ABOUT_TOO_LONG 400 Chat about too long
80 CHAT_ADMIN_INVITE_REQUIRED 403 You do not have the rights to do this
81 CHAT_ADMIN_REQUIRED 400 400 403 Chat admin privileges are required to do that in the specified chat (for example, to send a message in a channel which is not yours), or invalid permissions used for the channel or group
82 CHAT_DISCUSSION_UNALLOWED 400
83 CHAT_FORBIDDEN 403 You cannot write in this chat
84 CHAT_FORWARDS_RESTRICTED 400 406 You can't forward messages from a protected chat
85 CHAT_GET_FAILED 500
86 CHAT_GUEST_SEND_FORBIDDEN 403 You join the discussion group before commenting, see [here](/api/discussion#requiring-users-to-join-the-group) for more info
87 CHAT_ID_EMPTY 400 The provided chat ID is empty
88 CHAT_ID_GENERATE_FAILED 500 Failure while generating the chat ID
89 CHAT_ID_INVALID 400 Invalid object ID for a chat. Make sure to pass the right types, for instance making sure that the request is designed for chats (not channels/megagroups) or otherwise look for a different one more suited\nAn example working with a megagroup and AddChatUserRequest, it will fail because megagroups are channels. Use InviteToChannelRequest instead
90 CHAT_INVALID 400 The chat is invalid for this request
91 CHAT_INVITE_PERMANENT 400 You can't set an expiration date on permanent invite links
92 CHAT_LINK_EXISTS 400 The chat is linked to a channel and cannot be used in that request
93 CHAT_NOT_MODIFIED 400 The chat or channel wasn't modified (title, invites, username, admins, etc. are the same)
94 CHAT_RESTRICTED 400 The chat is restricted and cannot be used in that request
95 CHAT_REVOKE_DATE_UNSUPPORTED 400 `min_date` and `max_date` are not available for using with non-user peers
96 CHAT_SEND_GAME_FORBIDDEN 403 You can't send a game to this chat
97 CHAT_SEND_GIFS_FORBIDDEN 403 You can't send gifs in this chat
98 CHAT_SEND_INLINE_FORBIDDEN 400 400 403 You cannot send inline results in this chat
99 CHAT_SEND_MEDIA_FORBIDDEN 403 You can't send media in this chat
100 CHAT_SEND_POLL_FORBIDDEN 403 You can't send polls in this chat
101 CHAT_SEND_STICKERS_FORBIDDEN 403 You can't send stickers in this chat
102 CHAT_TITLE_EMPTY 400 No chat title provided
103 CHAT_TOO_BIG 400 This method is not available for groups with more than `chat_read_mark_size_threshold` members, [see client configuration](https://core.telegram.org/api/config#client-configuration)
104 CHAT_WRITE_FORBIDDEN 403 You can't write in this chat
105 CHP_CALL_FAIL 500 The statistics cannot be retrieved at this time
106 CODE_EMPTY 400 The provided code is empty
107 CODE_HASH_INVALID 400 Code hash invalid
108 CODE_INVALID 400 Code invalid (i.e. from email)
109 CONNECTION_API_ID_INVALID 400 The provided API id is invalid
110 CONNECTION_APP_VERSION_EMPTY 400 App version is empty
111 CONNECTION_DEVICE_MODEL_EMPTY 400 Device model empty
112 CONNECTION_LANG_PACK_INVALID 400 The specified language pack is not valid. This is meant to be used by official applications only so far, leave it empty
113 CONNECTION_LAYER_INVALID 400 The very first request must always be InvokeWithLayerRequest
114 CONNECTION_NOT_INITED 400 Connection not initialized
115 CONNECTION_SYSTEM_EMPTY 400 Connection system empty
116 CONNECTION_SYSTEM_LANG_CODE_EMPTY 400 The system language string was empty during connection
117 CONTACT_ADD_MISSING 400 Contact to add is missing
118 CONTACT_ID_INVALID 400 The provided contact ID is invalid
119 CONTACT_NAME_EMPTY 400 The provided contact name cannot be empty
120 CURRENCY_TOTAL_AMOUNT_INVALID CONTACT_REQ_MISSING 400 Missing contact request
121 CREATE_CALL_FAILED 400 An error occurred while creating the call
122 CURRENCY_TOTAL_AMOUNT_INVALID 400 The total amount of all prices is invalid
123 DATA_INVALID 400 Encrypted data invalid
124 DATA_JSON_INVALID 400 The provided JSON data is invalid
125 DATA_TOO_LONG 400 Data too long
126 DATE_EMPTY 400 Date empty
127 DC_ID_INVALID 400 This occurs when an authorization is tried to be exported for the same data center one is currently connected to
128 DH_G_A_INVALID 400 g_a invalid
129 DOCUMENT_INVALID 400 The document file was invalid and can't be used in inline mode
130 EDIT_BOT_INVITE_FORBIDDEN 403 Normal users can't edit invites that were created by bots
131 EMAIL_HASH_EXPIRED 400 The email hash expired and cannot be used to verify it
132 EMAIL_INVALID 400 The given email is invalid
133 EMAIL_UNCONFIRMED 400 Email unconfirmed
134 EMAIL_UNCONFIRMED_X 400 Email unconfirmed, the length of the code must be {code_length}
135 EMOJI_INVALID EMAIL_VERIFY_EXPIRED 400 The verification email has expired
136 EMOJI_NOT_MODIFIED EMOJI_INVALID 400 The specified theme emoji is valid
137 EMOJI_NOT_MODIFIED 400 The theme wasn't changed
138 EMOTICON_EMPTY 400 The emoticon field cannot be empty
139 EMOTICON_INVALID 400 The specified emoticon cannot be used or was not a emoticon
140 EMOTICON_STICKERPACK_MISSING 400 The emoticon sticker pack you are trying to get is missing
145 ENCRYPTION_ID_INVALID 400 The provided secret chat ID is invalid
146 ENCRYPTION_OCCUPY_FAILED 500 TDLib developer claimed it is not an error while accepting secret chats and 500 is used instead of 420
147 ENTITIES_TOO_LONG 400 It is no longer possible to send such long data inside entity tags (for example inline text URLs)
148 ENTITY_BOUNDS_INVALID 400 Some of provided entities have invalid bounds (length is zero or out of the boundaries of the string)
149 ENTITY_MENTION_USER_INVALID 400 You can't use this entity
150 ERROR_TEXT_EMPTY 400 The provided error message is empty
151 EXPIRE_DATE_INVALID 400 The specified expiration date is invalid
152 EXPIRE_FORBIDDEN 400
153 EXPORT_CARD_INVALID 400 Provided card is invalid
154 EXTERNAL_URL_INVALID 400 External URL invalid
155 FIELD_NAME_EMPTY 400 The field with the name FIELD_NAME is missing
156 FIELD_NAME_INVALID 400 The field with the name FIELD_NAME is invalid
157 FILEREF_UPGRADE_NEEDED 406 The file reference needs to be refreshed before being used again
158 FILE_CONTENT_TYPE_INVALID 400 File content-type is invalid
159 FILE_EMTPY 400 An empty file was provided
160 FILE_ID_INVALID 400 The provided file id is invalid. Make sure all parameters are present, have the correct type and are not empty (ID, access hash, file reference, thumb size ...)
161 FILE_MIGRATE_X 303 The file to be accessed is currently stored in DC {new_dc}
162 FILE_PARTS_INVALID 400 The number of file parts is invalid
166 FILE_PART_LENGTH_INVALID 400 The length of a file part is invalid
167 FILE_PART_SIZE_CHANGED 400 The file part size (chunk size) cannot change during upload
168 FILE_PART_SIZE_INVALID 400 The provided file part size is invalid
169 FILE_PART_TOO_BIG 400 The uploaded file part is too big
170 FILE_PART_X_MISSING 400 Part {which} of the file is missing from storage
171 FILE_REFERENCE_EMPTY 400 The file reference must exist to access the media and it cannot be empty
172 FILE_REFERENCE_EXPIRED 400 The file reference has expired and is no longer valid or it belongs to self-destructing media and cannot be resent
173 FILE_REFERENCE_INVALID 400 The file reference is invalid or you can't do that operation on such message
174 FILE_TITLE_EMPTY 400 An empty file title was specified
175 FILTER_ID_INVALID 400 The specified filter ID is invalid
176 FILTER_INCLUDE_EMPTY 400 The include_peers vector of the filter is empty
177 FILTER_NOT_SUPPORTED 400 The specified filter cannot be used in this context
178 FILTER_TITLE_EMPTY 400 The title field of the filter is empty
179 FIRSTNAME_INVALID 400 The first name is invalid
180 FLOOD_TEST_PHONE_WAIT_X 420 A wait of {seconds} seconds is required in the test servers
181 FLOOD_WAIT_X 420 A wait of {seconds} seconds is required
182 FLOOD_PREMIUM_WAIT_X 420 A wait of {seconds} seconds is required in non-premium accounts
183 FOLDER_ID_EMPTY 400 The folder you tried to delete was already empty
184 FOLDER_ID_INVALID 400 The folder you tried to use was not valid
185 FRESH_CHANGE_ADMINS_FORBIDDEN 400 400 406 Recently logged-in users cannot add or change admins
186 FRESH_CHANGE_PHONE_FORBIDDEN 406 Recently logged-in users cannot use this request
187 FRESH_RESET_AUTHORISATION_FORBIDDEN 406 The current session is too new and cannot be used to reset other authorisations yet
188 FROM_MESSAGE_BOT_DISABLED 400 Bots can't use fromMessage min constructors
189 FROM_PEER_INVALID 400 The given from_user peer cannot be used for the parameter
190 GAME_BOT_INVALID 400 You cannot send that game with the current bot
191 GIF_CONTENT_TYPE_INVALID GEO_POINT_INVALID 400 Invalid geoposition provided
192 GIF_CONTENT_TYPE_INVALID 400 GIF content-type invalid
193 GIF_ID_INVALID 400 The provided GIF ID is invalid
194 GRAPH_INVALID_RELOAD GRAPH_EXPIRED_RELOAD 400 This graph has expired, please obtain a new graph token
195 GRAPH_INVALID_RELOAD 400 Invalid graph token provided, please reload the stats and provide the updated token
196 GRAPH_OUTDATED_RELOAD 400 Data can't be used for the channel statistics, graphs outdated
197 GROUPCALL_ADD_PARTICIPANTS_FAILED 500
198 GROUPCALL_ALREADY_DISCARDED 400 The group call was already discarded
199 GROUPCALL_FORBIDDEN GROUPCALL_ALREADY_STARTED 403 The groupcall has already started, you can join directly using [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)
200 GROUPCALL_JOIN_MISSING GROUPCALL_FORBIDDEN 400 403 The group call has already ended
201 GROUPCALL_SSRC_DUPLICATE_MUCH GROUPCALL_INVALID 400 The specified group call is invalid
202 GROUPCALL_NOT_MODIFIED GROUPCALL_JOIN_MISSING 400 You haven't joined this group call
203 GROUPCALL_NOT_MODIFIED 400 Group call settings weren't modified
204 GROUPCALL_SSRC_DUPLICATE_MUCH 400 The app needs to retry joining the group call with a new SSRC value
205 GROUPED_MEDIA_INVALID 400 Invalid grouped media
206 GROUP_CALL_INVALID 400 Group call invalid
207 HASH_INVALID 400 The provided hash is invalid
208 HIDE_REQUESTER_MISSING 400 The join request was missing or was already handled
209 HISTORY_GET_FAILED 500 Fetching of history failed
210 IMAGE_PROCESS_FAILED 400 Failure while processing image
211 IMPORT_FILE_INVALID 400 The file is too large to be imported
212 IMPORT_FORMAT_UNRECOGNIZED 400 Unknown import format
213 IMPORT_ID_INVALID 400 The specified import ID is invalid
214 INLINE_BOT_REQUIRED 403 The action must be performed through an inline bot callback
215 INLINE_RESULT_EXPIRED 400 The inline query expired
216 INPUT_CONSTRUCTOR_INVALID 400 The provided constructor is invalid
220 INPUT_LAYER_INVALID 400 The provided layer is invalid
221 INPUT_METHOD_INVALID 400 The invoked method does not exist anymore or has never existed
222 INPUT_REQUEST_TOO_LONG 400 The input request was too long. This may be a bug in the library as it can occur when serializing more bytes than it should (like appending the vector constructor code at the end of a message)
223 INPUT_TEXT_EMPTY 400 The specified text is empty
224 INPUT_USER_DEACTIVATED 400 The specified user was deleted
225 INTERDC_X_CALL_ERROR 500 An error occurred while communicating with DC {dc}
226 INTERDC_X_CALL_RICH_ERROR 500 A rich error occurred while communicating with DC {dc}
227 INVITE_FORBIDDEN_WITH_JOINAS 400 If the user has anonymously joined a group call as a channel, they can't invite other users to the group call because that would cause deanonymization, because the invite would be sent using the original user ID, not the anonymized channel ID
228 INVITE_HASH_EMPTY 400 The invite hash is empty
229 INVITE_HASH_EXPIRED 400 400 406 The chat the user tried to join has expired and is not valid anymore
230 INVITE_HASH_INVALID 400 The invite hash is invalid
231 LANG_CODE_INVALID INVITE_REQUEST_SENT 400 You have successfully requested to join this chat or channel
232 INVITE_REVOKED_MISSING 400 The specified invite link was already revoked or is invalid
233 INVOICE_PAYLOAD_INVALID 400 The specified invoice payload is invalid
234 JOIN_AS_PEER_INVALID 400 The specified peer cannot be used to join a group call
235 LANG_CODE_INVALID 400 The specified language code is invalid
236 LANG_CODE_NOT_SUPPORTED 400 The specified language code is not supported
237 LANG_PACK_INVALID 400 The provided language pack is invalid
238 LASTNAME_INVALID 400 The last name is invalid
239 LIMIT_INVALID 400 An invalid limit was provided. See https://core.telegram.org/api/files#downloading-files
240 LINK_NOT_MODIFIED 400 The channel is already linked to this group
241 LOCATION_INVALID 400 The location given for a file was invalid. See https://core.telegram.org/api/files#downloading-files
242 MAX_DATE_INVALID 400 The specified maximum date is invalid
243 MAX_ID_INVALID 400 The provided max ID is invalid
244 MAX_QTS_INVALID 400 The provided QTS were invalid
245 MD5_CHECKSUM_INVALID 400 The MD5 check-sums do not match
246 MEDIA_CAPTION_TOO_LONG 400 The caption is too long
247 MEDIA_EMPTY 400 The provided media object is invalid or the current account may not be able to send it (such as games as users)
248 MEDIA_GROUPED_INVALID 400 You tried to send media of different types in an album
249 MEDIA_INVALID 400 Media invalid
250 MEDIA_NEW_INVALID 400 The new media to edit the message with is invalid (such as stickers or voice notes)
251 MEDIA_PREV_INVALID 400 The old media cannot be edited with anything else (such as stickers or voice notes)
260 MESSAGE_EDIT_TIME_EXPIRED 400 You can't edit this message anymore, too much time has passed since its creation.
261 MESSAGE_EMPTY 400 Empty or invalid UTF-8 message was sent
262 MESSAGE_IDS_EMPTY 400 No message ids were provided
263 MESSAGE_ID_INVALID 400 The specified message ID is invalid or you can't do that operation on such message
264 MESSAGE_NOT_MODIFIED 400 Content of the message was not modified
265 MESSAGE_POLL_CLOSED 400 The poll was closed and can no longer be voted on
266 MESSAGE_TOO_LONG 400 Message was too long. Current maximum length is 4096 UTF-8 characters Message was too long
267 METHOD_INVALID 400 The API method is invalid and cannot be used
268 MIN_DATE_INVALID 400 The specified minimum date is invalid
269 MSGID_DECREASE_RETRY 500 The request should be retried with a lower message ID
270 MSG_ID_INVALID 400 The message ID used in the peer was invalid
271 MSG_TOO_OLD 400 [`chat_read_mark_expire_period` seconds](https://core.telegram.org/api/config#chat-read-mark-expire-period) have passed since the message was sent, read receipts were deleted
272 MSG_WAIT_FAILED 400 A waiting call returned an error
273 MT_SEND_QUEUE_TOO_LONG 500
274 MULTI_MEDIA_TOO_LONG 400 Too many media files were included in the same album
276 NEED_MEMBER_INVALID 500 The provided member is invalid or does not exist (for example a thumb size)
277 NETWORK_MIGRATE_X 303 The source IP address is associated with DC {new_dc}
278 NEW_SALT_INVALID 400 The new salt is invalid
279 NEW_SETTINGS_EMPTY 400 No password is set on the current account, and no new password was specified in `new_settings`
280 NEW_SETTINGS_INVALID 400 The new settings are invalid
281 NEXT_OFFSET_INVALID 400 The value for next_offset is invalid. Check that it has normal characters and is not too long
282 NOT_ALLOWED 403
283 OFFSET_INVALID 400 The given offset was invalid, it must be divisible by 1KB. See https://core.telegram.org/api/files#downloading-files
284 OFFSET_PEER_ID_INVALID 400 The provided offset peer is invalid
285 OPTIONS_TOO_MUCH 400 You defined too many options for the poll
286 OPTION_INVALID 400 The option specified is invalid and does not exist in the target poll
287 PACK_SHORT_NAME_INVALID 400 Invalid sticker pack name. It must begin with a letter, can't contain consecutive underscores and must end in "_by_<bot username>".
288 PACK_SHORT_NAME_OCCUPIED 400 A stickerpack with this name already exists
289 PACK_TITLE_INVALID 400 The stickerpack title is invalid
290 PARTICIPANTS_TOO_FEW 400 Not enough participants
291 PARTICIPANT_CALL_FAILED 500 Failure while making call
292 PARTICIPANT_JOIN_MISSING PARTICIPANT_ID_INVALID 403 400 The specified participant ID is invalid
293 PARTICIPANT_JOIN_MISSING 400 403 Trying to enable a presentation, when the user hasn't joined the Video Chat with [phone.joinGroupCall](https://core.telegram.org/method/phone.joinGroupCall)
294 PARTICIPANT_VERSION_OUTDATED 400 The other participant does not use an up to date telegram client with support for calls
295 PASSWORD_EMPTY 400 The provided password is empty
296 PASSWORD_HASH_INVALID 400 The password (and thus its hash value) you entered is invalid
297 PASSWORD_MISSING 400 The account must have 2-factor authentication enabled (a password) before this method can be used
298 PASSWORD_RECOVERY_EXPIRED 400 The recovery code has expired
299 PASSWORD_RECOVERY_NA 400 No email was set, can't recover password via email
300 PASSWORD_REQUIRED 400 The account must have 2-factor authentication enabled (a password) before this method can be used
301 PASSWORD_TOO_FRESH_X 400 The password was added too recently and {seconds} seconds must pass before using the method
302 PAYMENT_PROVIDER_INVALID 400 The payment provider was not recognised or its token was invalid
303 PEER_FLOOD 400 Too many requests
304 PEER_HISTORY_EMPTY 400
305 PEER_ID_INVALID 400 An invalid Peer was used. Make sure to pass the right peer type and that the value is valid (for instance, bots cannot start conversations)
306 PEER_ID_NOT_SUPPORTED 400 The provided peer ID is not supported
307 PERSISTENT_TIMESTAMP_EMPTY 400 Persistent timestamp empty
311 PHONE_CODE_EXPIRED 400 The confirmation code has expired
312 PHONE_CODE_HASH_EMPTY 400 The phone code hash is missing
313 PHONE_CODE_INVALID 400 The phone code entered was invalid
314 PHONE_HASH_EXPIRED 400 An invalid or expired `phone_code_hash` was provided
315 PHONE_MIGRATE_X 303 The phone number a user is trying to use for authorization is associated with DC {new_dc}
316 PHONE_NOT_OCCUPIED 400 No user is associated to the specified phone number
317 PHONE_NUMBER_APP_SIGNUP_FORBIDDEN 400 You can't sign up using this app
318 PHONE_NUMBER_BANNED 400 The used phone number has been banned from Telegram and cannot be used anymore. Maybe check https://www.telegram.org/faq_spam
319 PHONE_NUMBER_FLOOD 400 You asked for the code too many times.
322 PHONE_NUMBER_UNOCCUPIED 400 The phone number is not yet being used
323 PHONE_PASSWORD_FLOOD 406 You have tried logging in too many times
324 PHONE_PASSWORD_PROTECTED 400 This phone is password protected
325 PHOTO_CONTENT_TYPE_INVALID 400 Photo mime-type invalid
326 PHOTO_CONTENT_URL_EMPTY 400 The content from the URL used as a photo appears to be empty or has caused another HTTP error
327 PHOTO_CROP_FILE_MISSING 400 Photo crop file missing
328 PHOTO_CROP_SIZE_SMALL 400 Photo is too small
329 PHOTO_EXT_INVALID 400 The extension of the photo is invalid
330 PHOTO_FILE_MISSING 400 Profile photo file missing
331 PHOTO_ID_INVALID 400 Photo id is invalid
332 PHOTO_INVALID 400 Photo invalid
333 PHOTO_INVALID_DIMENSIONS 400 The photo dimensions are invalid (hint: `pip install pillow` for `send_file` to resize images)
334 PHOTO_SAVE_FILE_INVALID 400 The photo you tried to send cannot be saved by Telegram. A reason may be that it exceeds 10MB. Try resizing it locally
335 PHOTO_THUMB_URL_EMPTY 400 The URL used as a thumbnail appears to be empty or has caused another HTTP error
336 PINNED_DIALOGS_TOO_MUCH 400 Too many pinned dialogs
337 PIN_RESTRICTED 400 You can't pin messages in private chats with other people
PINNED_DIALOGS_TOO_MUCH 400
338 POLL_ANSWERS_INVALID 400 The poll did not have enough answers or had too many
339 POLL_ANSWER_INVALID 400 One of the poll answers is not acceptable
340 POLL_OPTION_DUPLICATE 400 A duplicate option was sent in the same poll
341 POLL_OPTION_INVALID 400 A poll option used invalid data (the data may be too long)
342 POLL_QUESTION_INVALID 400 The poll question was either empty or too long
343 POLL_UNSUPPORTED 400 This layer does not support polls in the issued method
344 POLL_VOTE_REQUIRED 403 Cast a vote in the poll before calling this method
345 POSTPONED_TIMEOUT 500 The postponed call has timed out
346 PREMIUM_ACCOUNT_REQUIRED 403 A premium account is required to execute this action
347 PREMIUM_CURRENTLY_UNAVAILABLE 406
348 PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN 406 Similar to a flood wait, must wait {minutes} minutes
349 PRIVACY_KEY_INVALID 400 The privacy key is invalid
350 PRIVACY_TOO_LONG 400 Cannot add that many entities in a single request
351 PRIVACY_VALUE_INVALID 400 The privacy value is invalid
352 PTS_CHANGE_EMPTY 500 No PTS change
353 PUBLIC_KEY_REQUIRED PUBLIC_CHANNEL_MISSING 400 403 You can only export group call invite links for public chats or channels
354 PUBLIC_KEY_REQUIRED 400 A public key is required
355 QUERY_ID_EMPTY 400 The query ID is empty
356 QUERY_ID_INVALID 400 The query ID is invalid
357 QUERY_TOO_SHORT 400 The query string is too short
358 QUIZ_ANSWER_MISSING 400 You can forward a quiz while hiding the original author only after choosing an option in the quiz
359 QUIZ_CORRECT_ANSWERS_EMPTY 400 A quiz must specify one correct answer
360 QUIZ_CORRECT_ANSWERS_TOO_MUCH 400 There can only be one correct answer
361 QUIZ_CORRECT_ANSWER_INVALID 400 The correct answer is not an existing answer
362 QUIZ_MULTIPLE_INVALID 400 A poll cannot be both multiple choice and quiz
363 RANDOM_ID_DUPLICATE 500 You provided a random ID that was already used
364 RANDOM_ID_EMPTY 400 Random ID empty
365 RANDOM_ID_INVALID 400 A provided random ID is invalid
366 RANDOM_LENGTH_INVALID 400 Random length invalid
367 RANGES_INVALID 400 Invalid range provided
368 REACTIONS_TOO_MANY 400 The message already has exactly `reactions_uniq_max` reaction emojis, you can't react with a new emoji, see [the docs for more info](/api/config#client-configuration)
369 REACTION_EMPTY 400 No reaction provided
370 REACTION_INVALID 400 Invalid reaction provided (only emoji are allowed)
371 REFLECTOR_NOT_AVAILABLE 400 Invalid call reflector server
372 REG_ID_GENERATE_FAILED 500 Failure while generating registration ID
373 REPLY_MARKUP_BUY_EMPTY 400 Reply markup for buy button empty
374 REPLY_MARKUP_GAME_EMPTY 400 The provided reply markup for the game is empty
375 REPLY_MARKUP_INVALID 400 The provided reply markup is invalid
376 REPLY_MARKUP_TOO_LONG 400 The data embedded in the reply markup buttons was too much
377 RESET_REQUEST_MISSING 400 No password reset is in progress
378 RESULTS_TOO_MUCH 400 You sent too many results, see https://core.telegram.org/bots/api#answerinlinequery for the current limit
379 RESULT_ID_DUPLICATE 400 Duplicated IDs on the sent results. Make sure to use unique IDs
380 RESULT_ID_EMPTY 400 Result ID empty
381 RESULT_ID_INVALID 400 The given result cannot be used to send the selection to the bot
382 RESULT_TYPE_INVALID 400 Result type invalid
383 REVOTE_NOT_ALLOWED 400 You cannot change your vote
384 RIGHTS_NOT_MODIFIED 400 The new admin rights are equal to the old rights, no change was made
385 RIGHT_FORBIDDEN 403 Either your admin rights do not allow you to do this or you passed the wrong rights combination (some rights only apply to channels and vice versa)
386 RPC_CALL_FAIL 500 Telegram is having internal issues, please try again later.
387 RPC_MCGET_FAIL 500 Telegram is having internal issues, please try again later.
388 RSA_DECRYPT_FAILED 400 Internal RSA decryption failed
389 SCHEDULE_BOT_NOT_ALLOWED 400 Bots are not allowed to schedule messages
390 SCHEDULE_DATE_INVALID 400 Invalid schedule date provided
391 SCHEDULE_DATE_TOO_LATE 400 The date you tried to schedule is too far in the future (last known limit of 1 year and a few hours)
392 SCHEDULE_STATUS_PRIVATE 400 You cannot schedule a message until the person comes online if their privacy does not show this information
393 SCHEDULE_TOO_MUCH 400 You cannot schedule more messages in this chat (last known limit of 100 per chat)
394 SCORE_INVALID 400 The specified game score is invalid
395 SEARCH_QUERY_EMPTY 400 The search query is empty
396 SEARCH_WITH_LINK_NOT_SUPPORTED 400 You cannot provide a search query and an invite link at the same time
397 SECONDS_INVALID 400 Slow mode only supports certain values (e.g. 0, 10s, 30s, 1m, 5m, 15m and 1h)
398 SEND_AS_PEER_INVALID 400 You can't send messages as the specified peer
399 SEND_CODE_UNAVAILABLE 406 Returned when all available options for this type of number were already used (e.g. flash-call, then SMS, then this error might be returned to trigger a second resend)
400 SEND_MESSAGE_MEDIA_INVALID 400 The message media was invalid or not specified
401 SEND_MESSAGE_TYPE_INVALID 400 The message type is invalid
402 SENSITIVE_CHANGE_FORBIDDEN 403 Your sensitive content settings cannot be changed at this time
404 SESSION_PASSWORD_NEEDED 401 Two-steps verification is enabled and a password is required
405 SESSION_REVOKED 401 The authorization has been invalidated, because of the user terminating all sessions
406 SESSION_TOO_FRESH_X 400 The session logged in too recently and {seconds} seconds must pass before calling the method
407 SETTINGS_INVALID 400 Invalid settings were provided
408 SHA256_HASH_INVALID 400 The provided SHA256 hash is invalid
409 SHORTNAME_OCCUPY_FAILED 400 An error occurred when trying to register the short-name used for the sticker pack. Try a different name
410 SHORT_NAME_INVALID 400 The specified short name is invalid
411 SHORT_NAME_OCCUPIED 400 The specified short name is already in use
412 SIGN_IN_FAILED 500 Failure while signing in
413 SLOWMODE_MULTI_MSGS_DISABLED 400 Slowmode is enabled, you cannot forward multiple messages to this group
414 SLOWMODE_WAIT_X 420 A wait of {seconds} seconds is required before sending another message in this chat
415 SRP_ID_INVALID SMS_CODE_CREATE_FAILED 400 An error occurred while creating the SMS code
416 SRP_ID_INVALID 400 Invalid SRP ID provided
417 SRP_PASSWORD_CHANGED 400 Password has changed
418 START_PARAM_EMPTY 400 The start parameter is empty
419 START_PARAM_INVALID 400 Start parameter invalid
420 START_PARAM_TOO_LONG 400 Start parameter is too long
421 STATS_MIGRATE_X 303 The channel statistics must be fetched from DC {dc}
422 STICKERSET_INVALID STICKERPACK_STICKERS_TOO_MUCH 400 The provided sticker set is invalid There are too many stickers in this stickerpack, you can't add any more
423 STICKERSET_INVALID 400 406 The provided sticker set is invalid
424 STICKERSET_OWNER_ANONYMOUS 406 This sticker set can't be used as the group's official stickers because it was created by one of its anonymous admins
425 STICKERS_EMPTY 400 No sticker provided
426 STICKERS_TOO_MUCH 400 There are too many stickers in this stickerpack, you can't add any more
427 STICKER_DOCUMENT_INVALID 400 The sticker file was invalid (this file has failed Telegram internal checks, make sure to use the correct format and comply with https://core.telegram.org/animated_stickers)
428 STICKER_EMOJI_INVALID 400 Sticker emoji invalid
429 STICKER_FILE_INVALID 400 Sticker file invalid
430 STICKER_GIF_DIMENSIONS 400 The specified video sticker has invalid dimensions
431 STICKER_ID_INVALID 400 The provided sticker ID is invalid
432 STICKER_INVALID 400 The provided sticker is invalid
433 STICKER_MIME_INVALID 400 Make sure to pass a valid image file for the right InputFile parameter
434 STICKER_PNG_DIMENSIONS 400 Sticker png dimensions invalid
435 STICKER_PNG_NOPNG 400 Stickers must be a png file but the used image was not a png
436 STICKER_TGS_NODOC 400 You must send the animated sticker as a document
437 STICKER_TGS_NOTGS 400 Stickers must be a tgs file but the used file was not a tgs
438 STICKER_THUMB_PNG_NOPNG 400 Stickerset thumb must be a png file but the used file was not png
439 STICKER_THUMB_TGS_NOTGS 400 Stickerset thumb must be a tgs file but the used file was not tgs
440 STICKER_VIDEO_BIG 400 The specified video sticker is too big
441 STICKER_VIDEO_NODOC 400 You must send the video sticker as a document
442 STICKER_VIDEO_NOWEBM 400 The specified video sticker is not in webm format
443 STORAGE_CHECK_FAILED 500 Server storage check failed
444 STORE_INVALID_SCALAR_TYPE 500
445 SWITCH_PM_TEXT_EMPTY 400 The switch_pm.text field was empty
446 TAKEOUT_INIT_DELAY_X 420 A wait of {seconds} seconds is required before being able to initiate the takeout
447 TAKEOUT_INVALID 400 The takeout session has been invalidated by another data export session
448 TAKEOUT_REQUIRED 400 400 403 You must initialize a takeout request first
449 TEMP_AUTH_KEY_ALREADY_BOUND 400 The passed temporary key is already bound to another **perm_auth_key_id**
450 TEMP_AUTH_KEY_EMPTY 400 No temporary auth key provided
451 TIMEOUT THEME_FILE_INVALID 500 400 A timeout occurred while fetching data from the worker Invalid theme file provided
452 TITLE_INVALID THEME_FORMAT_INVALID 400 Invalid theme format provided
453 THEME_INVALID 400 Theme invalid
454 THEME_MIME_INVALID 400 You cannot create this theme, the mime-type is invalid
455 THEME_TITLE_INVALID 400 The specified theme title is invalid
456 TIMEOUT 500 A timeout occurred while fetching data from the worker
457 TITLE_INVALID 400 The specified stickerpack title is invalid
458 TMP_PASSWORD_DISABLED 400 The temporary password is disabled
459 TMP_PASSWORD_INVALID 400 Password auth needs to be regenerated
460 TOKEN_INVALID 400 The provided token is invalid
461 TOPIC_DELETED 400 The topic was deleted
462 TO_LANG_INVALID 400 The specified destination language is invalid
463 TTL_DAYS_INVALID 400 The provided TTL is invalid
464 TTL_MEDIA_INVALID 400 The provided media cannot be used with a TTL
465 TTL_PERIOD_INVALID 400 The provided TTL Period is invalid
466 TYPES_EMPTY 400 The types field is empty
467 TYPE_CONSTRUCTOR_INVALID 400 The type constructor is invalid
468 Timedout -503 Timeout while fetching data
469 Timeout -503 Timeout while fetching data
470 UNKNOWN_ERROR 400
471 UNKNOWN_METHOD 500 The method you tried to call cannot be called on non-CDN DCs
472 UNTIL_DATE_INVALID 400 That date cannot be specified in this request (try using None)
473 UPDATE_APP_TO_LOGIN 406
474 URL_INVALID 400 The URL used was invalid (e.g. when answering a callback with a URL that's not t.me/yourbot or your game's URL)
475 USER_VOLUME_INVALID USAGE_LIMIT_INVALID 400 The specified usage limit is invalid
476 USERNAME_INVALID 400 Nobody is using this username, or the username is unacceptable. If the latter, it must match r"[a-zA-Z][\w\d]{3,30}[a-zA-Z\d]"
477 USERNAME_NOT_MODIFIED 400 The username is not different from the current username
478 USERNAME_NOT_OCCUPIED 400 The username is not in use by anyone else yet
479 USERNAME_OCCUPIED 400 The username is already taken
480 USERNAME_PURCHASE_AVAILABLE 400
481 USERPIC_PRIVACY_REQUIRED 406 You need to disable privacy settings for your profile picture in order to make your geolocation public
482 USERPIC_UPLOAD_REQUIRED 400 406 You must have a profile picture before using this method
483 USERS_TOO_FEW 400 Not enough users (to create a chat, for example)
484 USERS_TOO_MUCH 400 The maximum number of users has been exceeded (to create a chat, for example)
485 USER_ADMIN_INVALID 400 Either you're not an admin or you tried to ban an admin that you didn't promote
486 USER_ALREADY_INVITED 400 You have already invited this user
487 USER_ALREADY_PARTICIPANT 400 The authenticated user is already a participant of the chat
488 USER_BANNED_IN_CHANNEL 400 You're banned from sending messages in supergroups/channels
489 USER_BLOCKED 400 User blocked
490 USER_BOT 400 Bots can only be admins in channels.
491 USER_BOT_INVALID 400 403 This method can only be called by a bot
492 USER_BOT_REQUIRED 400 This method can only be called by a bot
493 USER_CHANNELS_TOO_MUCH 403 400 403 One of the users you tried to add is already in too many channels/supergroups
494 USER_CREATOR 400 You can't leave this channel, because you're its creator
495 USER_DEACTIVATED 401 The user has been deleted/deactivated
496 USER_DEACTIVATED_BAN 401 The user has been deleted/deactivated
497 USER_DELETED 403 You can't send this secret message because the other participant deleted their account
498 USER_ID_INVALID 400 Invalid object ID for a user. Make sure to pass the right types, for instance making sure that the request is designed for users or otherwise look for a different one more suited
499 USER_INVALID 400 400 403 The given user was invalid
500 USER_IS_BLOCKED 400 403 User is blocked
501 USER_IS_BOT 400 Bots can't send messages to other bots
502 USER_KICKED 400 This user was kicked from this supergroup/channel
504 USER_NOT_MUTUAL_CONTACT 400 403 The provided user is not a mutual contact
505 USER_NOT_PARTICIPANT 400 The target user is not a member of the specified megagroup or channel
506 USER_PRIVACY_RESTRICTED 403 The user's privacy settings do not allow you to do this
507 USER_RESTRICTED 403 403 406 You're spamreported, you can't create channels or chats.
508 USERPIC_UPLOAD_REQUIRED USER_VOLUME_INVALID 400 You must have a profile picture before using this method The specified user volume is invalid
509 VIDEO_CONTENT_TYPE_INVALID 400 The video content type is not supported with the given parameters (i.e. supports_streaming)
510 VIDEO_FILE_INVALID 400 The given video cannot be used
511 VIDEO_TITLE_EMPTY 400 The specified video title is empty
512 VOICE_MESSAGES_FORBIDDEN 400 This user's privacy settings forbid you from sending voice messages
513 WALLPAPER_FILE_INVALID 400 The given file cannot be used as a wallpaper
514 WALLPAPER_INVALID 400 The input wallpaper was not valid
515 WALLPAPER_MIME_INVALID 400 The specified wallpaper MIME type is invalid
516 WC_CONVERT_URL_INVALID 400 WC convert URL invalid
517 WEBDOCUMENT_MIME_INVALID WEBDOCUMENT_INVALID 400 Invalid webdocument URL provided
518 WEBDOCUMENT_MIME_INVALID 400 Invalid webdocument mime type provided
519 WEBDOCUMENT_SIZE_TOO_BIG 400 Webdocument is too big!
520 WEBDOCUMENT_URL_INVALID 400 The given URL cannot be used
521 WEBPAGE_CURL_FAILED 400 Failure while fetching the webpage with cURL
522 WEBPAGE_MEDIA_EMPTY 400 Webpage media empty
523 WEBPUSH_AUTH_INVALID 400 The specified web push authentication secret is invalid
524 WEBPUSH_KEY_INVALID 400 The specified web push elliptic curve Diffie-Hellman public key is invalid
525 WEBPUSH_TOKEN_INVALID 400 The specified web push token is invalid
526 WORKER_BUSY_TOO_LONG_RETRY 500 Telegram workers are too busy to respond immediately
527 YOU_BLOCKED_USER 400 You blocked this user
528 FROZEN_METHOD_INVALID 420 You tried to use a method that is not available for frozen accounts
529 FROZEN_PARTICIPANT_MISSING 400 Your account is frozen and can't access the chat

View File

@ -1,7 +1,6 @@
ns,friendly,raw
account.AccountMethods,takeout,invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession
auth.AuthMethods,sign_in,auth.signIn auth.importBotAuthorization
auth.AuthMethods,sign_up,auth.signUp
auth.AuthMethods,send_code_request,auth.sendCode auth.resendCode
auth.AuthMethods,log_out,auth.logOut
auth.AuthMethods,edit_2fa,account.updatePasswordSettings

1 ns friendly raw
2 account.AccountMethods takeout invokeWithTakeout account.initTakeoutSession account.finishTakeoutSession
3 auth.AuthMethods sign_in auth.signIn auth.importBotAuthorization
auth.AuthMethods sign_up auth.signUp
4 auth.AuthMethods send_code_request auth.sendCode auth.resendCode
5 auth.AuthMethods log_out auth.logOut
6 auth.AuthMethods edit_2fa account.updatePasswordSettings

View File

@ -54,7 +54,7 @@ users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>
<p>This is <b>not</b> Python code. It's the "TL definition". It's
an easy-to-read line that gives a quick overview on the parameters
and its result. You don't need to worry about this. See
<a href="https://docs.telethon.dev/en/latest/developing/understanding-the-type-language.html">Understanding
<a href="https://docs.telethon.dev/en/stable/developing/understanding-the-type-language.html">Understanding
the Type Language</a> for more details on it.</p>
<h3>Index</h3>
@ -170,7 +170,7 @@ users.getUsers#0d91a548 id:Vector&lt;InputUser&gt; = Vector&lt;User&gt;</pre>
not always run. They are just there to show the right syntax.</p>
<p>You should check out
<a href="https://docs.telethon.dev/en/latest/concepts/full-api.html">how
<a href="https://docs.telethon.dev/en/stable/concepts/full-api.html">how
to access the full API</a> in ReadTheDocs.
</p>
</div>

View File

@ -80,7 +80,7 @@ auth.importLoginToken,user,AUTH_TOKEN_ALREADY_ACCEPTED AUTH_TOKEN_EXPIRED AUTH_T
auth.logOut,both,
auth.recoverPassword,user,CODE_EMPTY NEW_SETTINGS_INVALID
auth.requestPasswordRecovery,user,PASSWORD_EMPTY PASSWORD_RECOVERY_NA
auth.resendCode,user,PHONE_NUMBER_INVALID
auth.resendCode,user,PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE
auth.resetAuthorizations,user,TIMEOUT
auth.sendCode,user,API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED
auth.signIn,user,PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED
@ -92,7 +92,7 @@ channels.checkUsername,user,CHANNEL_INVALID CHAT_ID_INVALID USERNAME_INVALID
channels.convertToGigagroup,user,PARTICIPANTS_TOO_FEW
channels.createChannel,user,CHAT_TITLE_EMPTY USER_RESTRICTED
channels.deleteChannel,user,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE
channels.deleteHistory,user,
channels.deleteHistory,user,CHANNEL_TOO_BIG
channels.deleteMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN
channels.deleteUserHistory,user,CHANNEL_INVALID CHAT_ADMIN_REQUIRED
channels.editAdmin,both,ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
@ -113,17 +113,18 @@ channels.getMessages,both,CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_IDS_EMPTY
channels.getParticipant,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT
channels.getParticipants,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID TIMEOUT
channels.inviteToChannel,user,BOTS_TOO_MUCH BOT_GROUPS_BLOCKED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_INVALID CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED USERS_TOO_MUCH USER_BANNED_IN_CHANNEL USER_BLOCKED USER_BOT USER_CHANNELS_TOO_MUCH USER_ID_INVALID USER_KICKED USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
channels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE
channels.joinChannel,user,CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE INVITE_REQUEST_SENT
channels.leaveChannel,both,CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT
channels.readHistory,user,CHANNEL_INVALID CHANNEL_PRIVATE
channels.readMessageContents,user,CHANNEL_INVALID CHANNEL_PRIVATE
channels.reportSpam,user,CHANNEL_INVALID INPUT_USER_DEACTIVATED
channels.setDiscussionGroup,user,BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN
channels.setStickers,both,CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS
channels.toggleForum,user,CHAT_DISCUSSION_UNALLOWED
channels.togglePreHistoryHidden,user,CHAT_LINK_EXISTS
channels.toggleSignatures,user,CHANNEL_INVALID
channels.toggleSlowMode,user,SECONDS_INVALID
channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED
channels.updateUsername,user,CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED USERNAME_PURCHASE_AVAILABLE
channels.viewSponsoredMessage,user,UNKNOWN_ERROR
contacts.acceptContact,user,
contacts.addContact,user,CONTACT_NAME_EMPTY
@ -148,7 +149,7 @@ folders.deleteFolder,user,FOLDER_ID_EMPTY
folders.editPeerFolders,user,FOLDER_ID_INVALID
getFutureSalts,both,
help.acceptTermsOfService,user,
help.editUserInfo,user,USER_INVALID
help.editUserInfo,user,ENTITY_BOUNDS_INVALID USER_INVALID
help.getAppChangelog,user,
help.getAppConfig,user,
help.getAppUpdate,user,
@ -195,11 +196,11 @@ messages.editChatAdmin,user,CHAT_ID_INVALID
messages.editChatDefaultBannedRights,both,BANNED_RIGHTS_INVALID
messages.editChatPhoto,both,CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID
messages.editChatTitle,both,CHAT_ID_INVALID NEED_CHAT_INVALID
messages.editInlineBotMessage,both,MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED
messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID
messages.exportChatInvite,both,CHAT_ID_INVALID
messages.editInlineBotMessage,both,ENTITY_BOUNDS_INVALID MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED
messages.editMessage,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID
messages.exportChatInvite,both,CHAT_ID_INVALID EXPIRE_DATE_INVALID
messages.faveSticker,user,STICKER_ID_INVALID
messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.forwardMessages,both,BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.getAllChats,user,
messages.getAllDrafts,user,
messages.getAllStickers,user,
@ -248,9 +249,10 @@ messages.getStickers,user,EMOTICON_EMPTY
messages.getSuggestedDialogFilters,user,
messages.getUnreadMentions,user,PEER_ID_INVALID
messages.getWebPage,user,WC_CONVERT_URL_INVALID
messages.getWebPagePreview,user,
messages.getWebPagePreview,user,ENTITY_BOUNDS_INVALID
messages.hideAllChatJoinRequests,user,HIDE_REQUESTER_MISSING
messages.hidePeerSettingsBar,user,
messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT
messages.importChatInvite,user,CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID INVITE_REQUEST_SENT SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT
messages.initHistoryImport,user,IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT
messages.installStickerSet,user,STICKERSET_INVALID
messages.markDialogUnread,user,
@ -261,7 +263,7 @@ messages.readHistory,user,PEER_ID_INVALID TIMEOUT
messages.readMentions,user,
messages.readMessageContents,user,
messages.receivedMessages,user,
messages.receivedQueue,user,MSG_WAIT_FAILED MAX_QTS_INVALID
messages.receivedQueue,user,MAX_QTS_INVALID MSG_WAIT_FAILED
messages.reorderPinnedDialogs,user,PEER_ID_INVALID
messages.reorderStickerSets,user,
messages.report,user,
@ -269,7 +271,7 @@ messages.reportEncryptedSpam,user,CHAT_ID_INVALID
messages.reportSpam,user,PEER_ID_INVALID
messages.requestEncryption,user,DH_G_A_INVALID USER_ID_INVALID
messages.requestUrlAuth,user,
messages.saveDraft,user,PEER_ID_INVALID
messages.saveDraft,user,ENTITY_BOUNDS_INVALID PEER_ID_INVALID
messages.saveGif,user,GIF_ID_INVALID
messages.saveRecentSticker,user,STICKER_ID_INVALID
messages.search,user,CHAT_ADMIN_REQUIRED FROM_PEER_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FILTER_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID
@ -279,10 +281,10 @@ messages.searchStickerSets,user,
messages.sendEncrypted,user,CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED
messages.sendEncryptedFile,user,MSG_WAIT_FAILED
messages.sendEncryptedService,user,DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED
messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.sendMultiMedia,both,MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH
messages.sendInlineBotResult,user,CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMedia,both,BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID ENTITY_BOUNDS_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID POSTPONED_TIMEOUT QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
messages.sendMessage,both,AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_BOUNDS_INVALID ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
messages.sendMultiMedia,both,ENTITY_BOUNDS_INVALID MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED
messages.sendScheduledMessages,user,
messages.sendVote,user,MESSAGE_POLL_CLOSED OPTION_INVALID
messages.setBotCallbackAnswer,both,QUERY_ID_INVALID URL_INVALID
@ -297,14 +299,14 @@ messages.setInlineGameScore,bot,MESSAGE_ID_INVALID USER_BOT_REQUIRED
messages.setTyping,both,CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT
messages.startBot,user,BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID
messages.startHistoryImport,user,IMPORT_ID_INVALID
messages.toggleDialogPin,user,PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH
messages.toggleDialogPin,user,PEER_HISTORY_EMPTY PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH
messages.toggleStickerSets,user,
messages.uninstallStickerSet,user,STICKERSET_INVALID
messages.updateDialogFilter,user,
messages.updateDialogFiltersOrder,user,
messages.updatePinnedMessage,both,BOT_ONESIDE_NOT_AVAIL
messages.uploadEncryptedFile,user,
messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID
messages.uploadMedia,both,BOT_MISSING MEDIA_INVALID PEER_ID_INVALID POSTPONED_TIMEOUT
payments.clearSavedInfo,user,
payments.getBankCardData,user,BANK_CARD_NUMBER_INVALID
payments.getPaymentForm,user,MESSAGE_ID_INVALID
@ -319,9 +321,9 @@ phone.discardCall,user,CALL_ALREADY_ACCEPTED CALL_PEER_INVALID
phone.discardGroupCallRequest,user,GROUPCALL_ALREADY_DISCARDED
phone.editGroupCallParticipant,user,USER_VOLUME_INVALID
phone.getCallConfig,user,
phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN USER_ALREADY_INVITED INVITE_FORBIDDEN_WITH_JOINAS
phone.inviteToGroupCall,user,GROUPCALL_FORBIDDEN INVITE_FORBIDDEN_WITH_JOINAS USER_ALREADY_INVITED
phone.joinGroupCall,user,GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH
phone.joinGroupCallPresentation,user, PARTICIPANT_JOIN_MISSING
phone.joinGroupCallPresentation,user,PARTICIPANT_JOIN_MISSING
phone.receivedCall,user,CALL_ALREADY_DECLINED CALL_PEER_INVALID
phone.requestCall,user,CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED
phone.saveCallDebug,user,CALL_PEER_INVALID DATA_JSON_INVALID
@ -330,7 +332,7 @@ phone.toggleGroupCallSettings,user,GROUPCALL_NOT_MODIFIED
photos.deletePhotos,user,
photos.getUserPhotos,both,MAX_ID_INVALID USER_ID_INVALID
photos.updateProfilePhoto,user,PHOTO_ID_INVALID
photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID VIDEO_FILE_INVALID
photos.uploadProfilePhoto,user,ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID STICKER_MIME_INVALID VIDEO_FILE_INVALID
ping,both,
reqDHParams,both,
reqPq,both,
@ -340,10 +342,10 @@ setClientDHParams,both,
stats.getBroadcastStats,user,BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X
stats.getMegagroupStats,user,CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X
stats.loadAsyncGraph,user,GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD
stickers.addStickerToSet,bot,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS
stickers.addStickerToSet,both,BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS
stickers.changeStickerPosition,bot,BOT_MISSING STICKER_INVALID
stickers.checkShortName,user,SHORT_NAME_INVALID SHORT_NAME_OCCUPIED
stickers.createStickerSet,bot,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID
stickers.createStickerSet,both,BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID
stickers.removeStickerFromSet,bot,BOT_MISSING STICKER_INVALID
stickers.setStickerSetThumb,bot,STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS
stickers.suggestShortName,user,TITLE_INVALID

1 method usability errors
80 auth.logOut both
81 auth.recoverPassword user CODE_EMPTY NEW_SETTINGS_INVALID
82 auth.requestPasswordRecovery user PASSWORD_EMPTY PASSWORD_RECOVERY_NA
83 auth.resendCode user PHONE_NUMBER_INVALID PHONE_NUMBER_INVALID SEND_CODE_UNAVAILABLE
84 auth.resetAuthorizations user TIMEOUT
85 auth.sendCode user API_ID_INVALID API_ID_PUBLISHED_FLOOD AUTH_RESTART INPUT_REQUEST_TOO_LONG PHONE_NUMBER_APP_SIGNUP_FORBIDDEN PHONE_NUMBER_BANNED PHONE_NUMBER_FLOOD PHONE_NUMBER_INVALID PHONE_PASSWORD_FLOOD PHONE_PASSWORD_PROTECTED
86 auth.signIn user PHONE_CODE_EMPTY PHONE_CODE_EXPIRED PHONE_CODE_INVALID PHONE_NUMBER_INVALID PHONE_NUMBER_UNOCCUPIED SESSION_PASSWORD_NEEDED
92 channels.convertToGigagroup user PARTICIPANTS_TOO_FEW
93 channels.createChannel user CHAT_TITLE_EMPTY USER_RESTRICTED
94 channels.deleteChannel user CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_TOO_LARGE
95 channels.deleteHistory user CHANNEL_TOO_BIG
96 channels.deleteMessages both CHANNEL_INVALID CHANNEL_PRIVATE MESSAGE_DELETE_FORBIDDEN
97 channels.deleteUserHistory user CHANNEL_INVALID CHAT_ADMIN_REQUIRED
98 channels.editAdmin both ADMINS_TOO_MUCH ADMIN_RANK_EMOJI_NOT_ALLOWED ADMIN_RANK_INVALID BOT_CHANNELS_NA CHANNEL_INVALID CHAT_ADMIN_INVITE_REQUIRED CHAT_ADMIN_REQUIRED FRESH_CHANGE_ADMINS_FORBIDDEN RIGHT_FORBIDDEN USER_CREATOR USER_ID_INVALID USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
113 channels.getParticipant both CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED USER_ID_INVALID USER_NOT_PARTICIPANT
114 channels.getParticipants both CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED INPUT_CONSTRUCTOR_INVALID TIMEOUT
115 channels.inviteToChannel user BOTS_TOO_MUCH BOT_GROUPS_BLOCKED CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_INVALID CHAT_WRITE_FORBIDDEN INPUT_USER_DEACTIVATED USERS_TOO_MUCH USER_BANNED_IN_CHANNEL USER_BLOCKED USER_BOT USER_CHANNELS_TOO_MUCH USER_ID_INVALID USER_KICKED USER_NOT_MUTUAL_CONTACT USER_PRIVACY_RESTRICTED
116 channels.joinChannel user CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE CHANNELS_TOO_MUCH CHANNEL_INVALID CHANNEL_PRIVATE INVITE_REQUEST_SENT
117 channels.leaveChannel both CHANNEL_INVALID CHANNEL_PRIVATE CHANNEL_PUBLIC_GROUP_NA USER_CREATOR USER_NOT_PARTICIPANT
118 channels.readHistory user CHANNEL_INVALID CHANNEL_PRIVATE
119 channels.readMessageContents user CHANNEL_INVALID CHANNEL_PRIVATE
120 channels.reportSpam user CHANNEL_INVALID INPUT_USER_DEACTIVATED
121 channels.setDiscussionGroup user BROADCAST_ID_INVALID LINK_NOT_MODIFIED MEGAGROUP_ID_INVALID MEGAGROUP_PREHISTORY_HIDDEN
122 channels.setStickers both CHANNEL_INVALID PARTICIPANTS_TOO_FEW STICKERSET_OWNER_ANONYMOUS
123 channels.toggleForum user CHAT_DISCUSSION_UNALLOWED
124 channels.togglePreHistoryHidden user CHAT_LINK_EXISTS
125 channels.toggleSignatures user CHANNEL_INVALID
126 channels.toggleSlowMode user SECONDS_INVALID
127 channels.updateUsername user CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED CHANNELS_ADMIN_PUBLIC_TOO_MUCH CHANNEL_INVALID CHAT_ADMIN_REQUIRED USERNAME_INVALID USERNAME_OCCUPIED USERNAME_PURCHASE_AVAILABLE
128 channels.viewSponsoredMessage user UNKNOWN_ERROR
129 contacts.acceptContact user
130 contacts.addContact user CONTACT_NAME_EMPTY
149 folders.editPeerFolders user FOLDER_ID_INVALID
150 getFutureSalts both
151 help.acceptTermsOfService user
152 help.editUserInfo user USER_INVALID ENTITY_BOUNDS_INVALID USER_INVALID
153 help.getAppChangelog user
154 help.getAppConfig user
155 help.getAppUpdate user
196 messages.editChatDefaultBannedRights both BANNED_RIGHTS_INVALID
197 messages.editChatPhoto both CHAT_ID_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FETCH_FAIL PEER_ID_INVALID PHOTO_EXT_INVALID
198 messages.editChatTitle both CHAT_ID_INVALID NEED_CHAT_INVALID
199 messages.editInlineBotMessage both MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED ENTITY_BOUNDS_INVALID MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED
200 messages.editMessage both CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_BOT_REQUIRED INPUT_USER_DEACTIVATED MEDIA_GROUPED_INVALID MEDIA_NEW_INVALID MEDIA_PREV_INVALID MESSAGE_AUTHOR_REQUIRED MESSAGE_EDIT_TIME_EXPIRED MESSAGE_EMPTY MESSAGE_ID_INVALID MESSAGE_NOT_MODIFIED PEER_ID_INVALID
201 messages.exportChatInvite both CHAT_ID_INVALID CHAT_ID_INVALID EXPIRE_DATE_INVALID
202 messages.faveSticker user STICKER_ID_INVALID
203 messages.forwardMessages both BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_SEND_GIFS_FORBIDDEN CHAT_SEND_MEDIA_FORBIDDEN CHAT_SEND_STICKERS_FORBIDDEN CHAT_WRITE_FORBIDDEN GROUPED_MEDIA_INVALID INPUT_USER_DEACTIVATED MEDIA_EMPTY MESSAGE_IDS_EMPTY MESSAGE_ID_INVALID PEER_ID_INVALID PTS_CHANGE_EMPTY QUIZ_ANSWER_MISSING RANDOM_ID_DUPLICATE RANDOM_ID_INVALID SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
204 messages.getAllChats user
205 messages.getAllDrafts user
206 messages.getAllStickers user
249 messages.getSuggestedDialogFilters user
250 messages.getUnreadMentions user PEER_ID_INVALID
251 messages.getWebPage user WC_CONVERT_URL_INVALID
252 messages.getWebPagePreview user ENTITY_BOUNDS_INVALID
253 messages.hideAllChatJoinRequests user HIDE_REQUESTER_MISSING
254 messages.hidePeerSettingsBar user
255 messages.importChatInvite user CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT CHANNELS_TOO_MUCH INVITE_HASH_EMPTY INVITE_HASH_EXPIRED INVITE_HASH_INVALID INVITE_REQUEST_SENT SESSION_PASSWORD_NEEDED USERS_TOO_MUCH USER_ALREADY_PARTICIPANT
256 messages.initHistoryImport user IMPORT_FILE_INVALID IMPORT_FORMAT_UNRECOGNIZED PREVIOUS_CHAT_IMPORT_ACTIVE_WAIT_XMIN TIMEOUT
257 messages.installStickerSet user STICKERSET_INVALID
258 messages.markDialogUnread user
263 messages.readMentions user
264 messages.readMessageContents user
265 messages.receivedMessages user
266 messages.receivedQueue user MSG_WAIT_FAILED MAX_QTS_INVALID MAX_QTS_INVALID MSG_WAIT_FAILED
267 messages.reorderPinnedDialogs user PEER_ID_INVALID
268 messages.reorderStickerSets user
269 messages.report user
271 messages.reportSpam user PEER_ID_INVALID
272 messages.requestEncryption user DH_G_A_INVALID USER_ID_INVALID
273 messages.requestUrlAuth user
274 messages.saveDraft user PEER_ID_INVALID ENTITY_BOUNDS_INVALID PEER_ID_INVALID
275 messages.saveGif user GIF_ID_INVALID
276 messages.saveRecentSticker user STICKER_ID_INVALID
277 messages.search user CHAT_ADMIN_REQUIRED FROM_PEER_INVALID INPUT_CONSTRUCTOR_INVALID INPUT_FILTER_INVALID INPUT_USER_DEACTIVATED PEER_ID_INVALID PEER_ID_NOT_SUPPORTED SEARCH_QUERY_EMPTY USER_ID_INVALID
281 messages.sendEncrypted user CHAT_ID_INVALID DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED
282 messages.sendEncryptedFile user MSG_WAIT_FAILED
283 messages.sendEncryptedService user DATA_INVALID ENCRYPTION_DECLINED MSG_WAIT_FAILED USER_IS_BLOCKED
284 messages.sendInlineBotResult user CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY CHAT_SEND_INLINE_FORBIDDEN CHAT_WRITE_FORBIDDEN ENTITY_BOUNDS_INVALID INLINE_RESULT_EXPIRED PEER_ID_INVALID QUERY_ID_EMPTY SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
285 messages.sendMedia both BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY BOT_PAYMENTS_DISABLED BOT_POLLS_DISABLED BROADCAST_PUBLIC_VOTERS_FORBIDDEN CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_SEND_MEDIA_FORBIDDEN CHAT_WRITE_FORBIDDEN CURRENCY_TOTAL_AMOUNT_INVALID EMOTICON_INVALID ENTITY_BOUNDS_INVALID EXTERNAL_URL_INVALID FILE_PARTS_INVALID FILE_PART_LENGTH_INVALID FILE_REFERENCE_EMPTY FILE_REFERENCE_EXPIRED GAME_BOT_INVALID INPUT_USER_DEACTIVATED MEDIA_CAPTION_TOO_LONG MEDIA_EMPTY PAYMENT_PROVIDER_INVALID PEER_ID_INVALID PHOTO_EXT_INVALID PHOTO_INVALID_DIMENSIONS PHOTO_SAVE_FILE_INVALID POLL_ANSWERS_INVALID POLL_OPTION_DUPLICATE POLL_QUESTION_INVALID POSTPONED_TIMEOUT QUIZ_CORRECT_ANSWERS_EMPTY QUIZ_CORRECT_ANSWERS_TOO_MUCH QUIZ_CORRECT_ANSWER_INVALID QUIZ_MULTIPLE_INVALID RANDOM_ID_DUPLICATE SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH STORAGE_CHECK_FAILED TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT VIDEO_CONTENT_TYPE_INVALID WEBPAGE_CURL_FAILED WEBPAGE_MEDIA_EMPTY
286 messages.sendMessage both AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER AUTH_KEY_DUPLICATED BOT_DOMAIN_INVALID BUTTON_DATA_INVALID BUTTON_TYPE_INVALID BUTTON_URL_INVALID CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ADMIN_REQUIRED CHAT_ID_INVALID CHAT_RESTRICTED CHAT_WRITE_FORBIDDEN ENTITIES_TOO_LONG ENTITY_BOUNDS_INVALID ENTITY_MENTION_USER_INVALID INPUT_USER_DEACTIVATED MESSAGE_EMPTY MESSAGE_TOO_LONG MSG_ID_INVALID PEER_ID_INVALID POLL_OPTION_INVALID RANDOM_ID_DUPLICATE REPLY_MARKUP_INVALID REPLY_MARKUP_TOO_LONG SCHEDULE_BOT_NOT_ALLOWED SCHEDULE_DATE_TOO_LATE SCHEDULE_STATUS_PRIVATE SCHEDULE_TOO_MUCH TIMEOUT TOPIC_DELETED USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT YOU_BLOCKED_USER
287 messages.sendMultiMedia both MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH ENTITY_BOUNDS_INVALID MULTI_MEDIA_TOO_LONG SCHEDULE_DATE_TOO_LATE SCHEDULE_TOO_MUCH TOPIC_DELETED
288 messages.sendScheduledMessages user
289 messages.sendVote user MESSAGE_POLL_CLOSED OPTION_INVALID
290 messages.setBotCallbackAnswer both QUERY_ID_INVALID URL_INVALID
299 messages.setTyping both CHANNEL_INVALID CHANNEL_PRIVATE CHAT_ID_INVALID CHAT_WRITE_FORBIDDEN PEER_ID_INVALID USER_BANNED_IN_CHANNEL USER_IS_BLOCKED USER_IS_BOT
300 messages.startBot user BOT_INVALID PEER_ID_INVALID START_PARAM_EMPTY START_PARAM_INVALID
301 messages.startHistoryImport user IMPORT_ID_INVALID
302 messages.toggleDialogPin user PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH PEER_HISTORY_EMPTY PEER_ID_INVALID PINNED_DIALOGS_TOO_MUCH
303 messages.toggleStickerSets user
304 messages.uninstallStickerSet user STICKERSET_INVALID
305 messages.updateDialogFilter user
306 messages.updateDialogFiltersOrder user
307 messages.updatePinnedMessage both BOT_ONESIDE_NOT_AVAIL
308 messages.uploadEncryptedFile user
309 messages.uploadMedia both BOT_MISSING MEDIA_INVALID PEER_ID_INVALID BOT_MISSING MEDIA_INVALID PEER_ID_INVALID POSTPONED_TIMEOUT
310 payments.clearSavedInfo user
311 payments.getBankCardData user BANK_CARD_NUMBER_INVALID
312 payments.getPaymentForm user MESSAGE_ID_INVALID
321 phone.discardGroupCallRequest user GROUPCALL_ALREADY_DISCARDED
322 phone.editGroupCallParticipant user USER_VOLUME_INVALID
323 phone.getCallConfig user
324 phone.inviteToGroupCall user GROUPCALL_FORBIDDEN USER_ALREADY_INVITED INVITE_FORBIDDEN_WITH_JOINAS GROUPCALL_FORBIDDEN INVITE_FORBIDDEN_WITH_JOINAS USER_ALREADY_INVITED
325 phone.joinGroupCall user GROUPCALL_ADD_PARTICIPANTS_FAILED GROUPCALL_SSRC_DUPLICATE_MUCH
326 phone.joinGroupCallPresentation user PARTICIPANT_JOIN_MISSING
327 phone.receivedCall user CALL_ALREADY_DECLINED CALL_PEER_INVALID
328 phone.requestCall user CALL_PROTOCOL_FLAGS_INVALID PARTICIPANT_CALL_FAILED PARTICIPANT_VERSION_OUTDATED USER_ID_INVALID USER_IS_BLOCKED USER_PRIVACY_RESTRICTED
329 phone.saveCallDebug user CALL_PEER_INVALID DATA_JSON_INVALID
332 photos.deletePhotos user
333 photos.getUserPhotos both MAX_ID_INVALID USER_ID_INVALID
334 photos.updateProfilePhoto user PHOTO_ID_INVALID
335 photos.uploadProfilePhoto user ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID VIDEO_FILE_INVALID ALBUM_PHOTOS_TOO_MANY FILE_PARTS_INVALID IMAGE_PROCESS_FAILED PHOTO_CROP_SIZE_SMALL PHOTO_EXT_INVALID STICKER_MIME_INVALID VIDEO_FILE_INVALID
336 ping both
337 reqDHParams both
338 reqPq both
342 stats.getBroadcastStats user BROADCAST_REQUIRED CHAT_ADMIN_REQUIRED CHP_CALL_FAIL STATS_MIGRATE_X
343 stats.getMegagroupStats user CHAT_ADMIN_REQUIRED MEGAGROUP_REQUIRED STATS_MIGRATE_X
344 stats.loadAsyncGraph user GRAPH_INVALID_RELOAD GRAPH_OUTDATED_RELOAD
345 stickers.addStickerToSet bot both BOT_MISSING STICKERSET_INVALID STICKER_PNG_NOPNG STICKER_TGS_NOTGS
346 stickers.changeStickerPosition bot BOT_MISSING STICKER_INVALID
347 stickers.checkShortName user SHORT_NAME_INVALID SHORT_NAME_OCCUPIED
348 stickers.createStickerSet bot both BOT_MISSING PACK_SHORT_NAME_INVALID PACK_SHORT_NAME_OCCUPIED PEER_ID_INVALID SHORTNAME_OCCUPY_FAILED STICKERS_EMPTY STICKER_EMOJI_INVALID STICKER_FILE_INVALID STICKER_PNG_DIMENSIONS STICKER_PNG_NOPNG STICKER_TGS_NOTGS STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS USER_ID_INVALID
349 stickers.removeStickerFromSet bot BOT_MISSING STICKER_INVALID
350 stickers.setStickerSetThumb bot STICKER_THUMB_PNG_NOPNG STICKER_THUMB_TGS_NOTGS
351 stickers.suggestShortName user TITLE_INVALID

View File

@ -142,8 +142,8 @@ class DocsWriter:
self.write(':')
# "Opening" modifiers
if arg.is_flag:
self.write('flags.{}?', arg.flag_index)
if arg.flag:
self.write('{}.{}?', arg.flag, arg.flag_index)
if arg.is_generic:
self.write('!')

View File

@ -117,11 +117,11 @@ def _generate_index(folder, paths,
.replace(os.path.sep, '/').title())
if bots_index:
docs.write_text('These are the methods that you may be able to '
docs.write_text('These are the requests that you may be able to '
'use as a bot. Click <a href="{}">here</a> to '
'view them all.'.format(INDEX))
else:
docs.write_text('Click <a href="{}">here</a> to view the methods '
docs.write_text('Click <a href="{}">here</a> to view the requests '
'that you can use as a bot.'.format(BOT_INDEX))
if namespaces:
docs.write_title('Namespaces', level=3)
@ -164,7 +164,7 @@ def _get_description(arg):
if arg.can_be_inferred:
desc.append('If left unspecified, it will be inferred automatically.')
otherwise = True
elif arg.is_flag:
elif arg.flag:
desc.append('This argument defaults to '
'<code>None</code> and can be omitted.')
otherwise = True
@ -268,7 +268,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
start = \
'Both users and bots <strong>may</strong> be able to'
docs.write_text('{} use this method. <a href="#examples">'
docs.write_text('{} use this request. <a href="#examples">'
'See code examples.</a>'.format(start))
# Write the code definition for this TLObject
@ -287,7 +287,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
# We assume it's a function returning a generic type
generic_arg = next((arg.name for arg in tlobject.args
if arg.is_generic))
docs.write_text('This function returns the result of whatever '
docs.write_text('This request returns the result of whatever '
'the result from invoking the request passed '
'through <i>{}</i> is.'.format(generic_arg))
else:
@ -381,7 +381,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
ns, friendly = tlobject.friendly
docs.write_text(
'Please refer to the documentation of <a href="'
'https://docs.telethon.dev/en/latest/modules/client.html'
'https://docs.telethon.dev/en/stable/modules/client.html'
'#telethon.client.{0}.{1}"><code>client.{1}()</code></a> '
'to learn about the parameters and see several code '
'examples on how to use it.'
@ -465,15 +465,15 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
docs.end_table()
# List all the methods which return this type
docs.write_title('Methods returning this type', level=3)
docs.write_title('Requests returning this type', level=3)
functions = type_to_functions.get(t, [])
if not functions:
docs.write_text('No method returns this type.')
docs.write_text('No request returns this type.')
elif len(functions) == 1:
docs.write_text('Only the following method returns this type.')
docs.write_text('Only the following request returns this type.')
else:
docs.write_text(
'The following %d methods return this type as a result.' %
'The following %d requests return this type as a result.' %
len(functions)
)
@ -484,7 +484,7 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
docs.end_table()
# List all the methods which take this type as input
docs.write_title('Methods accepting this type as input', level=3)
docs.write_title('Requests accepting this type as input', level=3)
other_methods = sorted(
(u for u in tlobjects
if any(a.type == t for a in u.args) and u.is_function),
@ -492,13 +492,13 @@ def _write_html_pages(tlobjects, methods, layer, input_res):
)
if not other_methods:
docs.write_text(
'No methods accept this type as an input parameter.')
'No request accepts this type as an input parameter.')
elif len(other_methods) == 1:
docs.write_text(
'Only this method has a parameter with this type.')
'Only this request has a parameter with this type.')
else:
docs.write_text(
'The following %d methods accept this type as an input '
'The following %d requests accept this type as an input '
'parameter.' % len(other_methods))
docs.begin_table(2)

Some files were not shown because too many files have changed in this diff Show More