mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-03-13 16:05:49 +03:00
Compare commits
1374 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
67765f84a5 | ||
|
4bfe7849f6 | ||
|
859f7423f2 | ||
|
6f5556373f | ||
|
0fc9b14674 | ||
|
0c2a3c144b | ||
|
a03a8673e1 | ||
|
045df418df | ||
|
592a899aab | ||
|
1cb5ff1dd5 | ||
|
9762a83541 | ||
|
141b620437 | ||
|
551c24f3e4 | ||
|
38d024312e | ||
|
a2926b548f | ||
|
455acc43f6 | ||
|
792adb78b3 | ||
|
5a0e69693b | ||
|
b9aafa3441 | ||
|
494b20db2d | ||
|
0a6b649ead | ||
|
cfce68e9ad | ||
|
b09c8c83f7 | ||
|
225ea9c3ab | ||
|
9ca3b599fc | ||
|
63d55bbe3d | ||
|
c9cce8aa81 | ||
|
70098c58a5 | ||
|
769b65efb1 | ||
|
f03e4b1137 | ||
|
a77835a7d9 | ||
|
85c4a91317 | ||
|
3f589b287d | ||
|
8138be2503 | ||
|
a0e42c1eb7 | ||
|
4553f04e49 | ||
|
f652f3f01a | ||
|
693c73ec1d | ||
|
a9442ef1be | ||
|
d37b0f812f | ||
|
b01d3d7a2f | ||
|
aec957d62d | ||
|
46854a7660 | ||
|
90f1e5b073 | ||
|
75408483ad | ||
|
946f803de7 | ||
|
087191e9c5 | ||
|
a5c98aec50 | ||
|
cfebb9df05 | ||
|
04aea46fe4 | ||
|
3def9433b8 | ||
|
b3e210a1fb | ||
|
47673680f4 | ||
|
1974b663a2 | ||
|
881bfaac5c | ||
|
0f6dd5987e | ||
|
d77ac18695 | ||
|
8137b12bec | ||
|
10a6d16af6 | ||
|
3ac11e15ec | ||
|
3625bf849d | ||
|
d3a201a277 | ||
|
49a8f111d3 | ||
|
723fbd570f | ||
|
26aa178cf6 | ||
|
9f3e7e4aa8 | ||
|
75d609ab2a | ||
|
4d34243b98 | ||
|
7ceb2e0b25 | ||
|
47178dfaef | ||
|
d90d0dc00f | ||
|
d1518f002a | ||
|
319db57ccb | ||
|
22bf0b4310 | ||
|
2b99ff65c5 | ||
|
39fc5c5fef | ||
|
65c27c5ced | ||
|
d76f3b7556 | ||
|
41eb665c9d | ||
|
70201a9ff1 | ||
|
63d9b267f4 | ||
|
6187ff7dcb | ||
|
6ee2fffce8 | ||
|
32a4cb82ce | ||
|
a97a7a5400 | ||
|
9dbe9a7669 | ||
|
c445684be8 | ||
|
1241671e72 | ||
|
b882348a2b | ||
|
2082a0e4de | ||
|
6cf1be93ae | ||
|
3d58dc355e | ||
|
3b428f97a9 | ||
|
abeb8c4d8d | ||
|
985d12e169 | ||
|
1ef66896bd | ||
|
584735afe1 | ||
|
cf3bc71e1d | ||
|
ddc9bef503 | ||
|
308f8e8bf8 | ||
|
6ccd6b0a41 | ||
|
b17e10af1d | ||
|
046dbb58b8 | ||
|
fda6840449 | ||
|
eb67ef1b15 | ||
|
7d7dbdf47f | ||
|
6a36066d19 | ||
|
bd11564579 | ||
|
ad19987cd6 | ||
|
7325718f0e | ||
|
7ce0b2f940 | ||
|
5ba312555a | ||
|
2cef715921 | ||
|
ba99b8b466 | ||
|
72faa89361 | ||
|
e928fbdac0 | ||
|
9b1d9aa672 | ||
|
72f16ef73e | ||
|
33f3e27e7d | ||
|
ac483e6812 | ||
|
d40aae75f3 | ||
|
574e8876ec | ||
|
2011a329b0 | ||
|
0cc9ca9bd9 | ||
|
e617b59d48 | ||
|
b0f9fd1f25 | ||
|
128b707488 | ||
|
6ded164b85 | ||
|
211238fcd2 | ||
|
694c78c8e9 | ||
|
ce010e9bfb | ||
|
413a2bb9f3 | ||
|
9cf4cd70d1 | ||
|
131f021d51 | ||
|
438aff3545 | ||
|
4eef9b52c9 | ||
|
a0cda0c37c | ||
|
816b0bdf9f | ||
|
164d35681e | ||
|
75ed58ad89 | ||
|
16ed9614f9 | ||
|
9267917031 | ||
|
1e63de9b68 | ||
|
2826c942c0 | ||
|
65407fc899 | ||
|
c3bddf9440 | ||
|
4ff7ac6b75 | ||
|
c3ec775607 | ||
|
aab8009a5a | ||
|
0f0ca6b0d9 | ||
|
c89644eec4 | ||
|
ed825a2c7d | ||
|
9751b356fe | ||
|
6acc39ac04 | ||
|
9fe5937ae1 | ||
|
16122545ec | ||
|
6a7a981b7a | ||
|
980f8b32fc | ||
|
c4a41adae5 | ||
|
2889bd5bf3 | ||
|
9c7ac3b210 | ||
|
ce29f13606 | ||
|
d7bd554ba0 | ||
|
ccf67d0f4f | ||
|
03ff996ace | ||
|
9aad453e1a | ||
|
6e7423e894 | ||
|
7b1b33f805 | ||
|
d419979406 | ||
|
acec8a776f | ||
|
ced36adb03 | ||
|
5b1135734b | ||
|
10c74f8bab | ||
|
af18538722 | ||
|
fd09284598 | ||
|
a657ae0134 | ||
|
88bc6a46a6 | ||
|
97b0ba6707 | ||
|
cb04e269c0 | ||
|
d1e3237c41 | ||
|
f7e38ee6f0 | ||
|
3e64ea35ff | ||
|
f9001bc8e0 | ||
|
68ea208b43 | ||
|
0f7756ac68 | ||
|
33c5ee9be4 | ||
|
a942b021bc | ||
|
516a2e7435 | ||
|
be59c36ed3 | ||
|
acd3407418 | ||
|
f3414d134a | ||
|
177386e755 | ||
|
1f79f063a2 | ||
|
b87a8d0c1f | ||
|
b68c1f4f03 | ||
|
6bc7245106 | ||
|
373601500f | ||
|
f334d5b8fe | ||
|
4de1609d4e | ||
|
07a7a8b404 | ||
|
0563430314 | ||
|
acfde7132b | ||
|
610b8c34dd | ||
|
daf21f12d9 | ||
|
6dece6e8a1 | ||
|
9f077e356b | ||
|
1f8b59043b | ||
|
cc3d25eeb8 | ||
|
d81eb0b2e8 | ||
|
83bafa25e3 | ||
|
fb97a8aa87 | ||
|
c932d79ab3 | ||
|
94cc897019 | ||
|
7a74dedc48 | ||
|
6332690a51 | ||
|
2007c83c9e | ||
|
7288c9933c | ||
|
979e38152d | ||
|
6d2a5dada5 | ||
|
061a84bef2 | ||
|
e750eb7ab5 | ||
|
59ffad0090 | ||
|
a8ce308b7a | ||
|
c72c7b160a | ||
|
5080715565 | ||
|
b2925f8279 | ||
|
4a6ef97910 | ||
|
83f13da420 | ||
|
4f51604def | ||
|
ba7fc245ab | ||
|
bd1ba3bf1e | ||
|
8ae75db862 | ||
|
2c85ffea12 | ||
|
fb43f638ff | ||
|
073b87ba1f | ||
|
0c868065c7 | ||
|
2ffac2dcdb | ||
|
f902c9293a | ||
|
a7db08d020 | ||
|
0980d55c34 | ||
|
b3266fabd8 | ||
|
ef4f9a962c | ||
|
f819593cbf | ||
|
2d237c41fe | ||
|
7f5a1ec5e1 | ||
|
949b54fdb0 | ||
|
b6d8311a55 | ||
|
db29e9b7ef | ||
|
299b090cde | ||
|
04cf2953f6 | ||
|
ad2238e788 | ||
|
908375ac42 | ||
|
7f472ee72c | ||
|
d2b1c3ec5f | ||
|
1cf6cf46bd | ||
|
bb98f4e68c | ||
|
105a7a7c56 | ||
|
fd70b5a428 | ||
|
6fcd7dff38 | ||
|
346a3f0ef5 | ||
|
c975b566a1 | ||
|
49bdb762c9 | ||
|
a83fe46baf | ||
|
17516318e6 | ||
|
6d02a1c6ff | ||
|
1f42e6e32f | ||
|
ff0f9b0e8f | ||
|
2d4305db76 | ||
|
5a17397fc7 | ||
|
d7424ccb90 | ||
|
e6ebe6b334 | ||
|
18da855dd4 | ||
|
75fe90005f | ||
|
363c2604df | ||
|
7cac3668d6 | ||
|
2f2a9901e2 | ||
|
64bc73c41e | ||
|
243f58c331 | ||
|
06536cfb91 | ||
|
299eceb6eb | ||
|
50aa92ebde | ||
|
7d4424ac2b | ||
|
a66df977f7 | ||
|
935be9dd6e | ||
|
6e8bc0d5b9 | ||
|
8b1bfcdf9c | ||
|
48d7dbe90b | ||
|
e87e6738b5 | ||
|
7d21b40401 | ||
|
88b2b9372d | ||
|
44e3651adf | ||
|
d5c864597c | ||
|
df96ead0ab | ||
|
809a07edac | ||
|
4b151fbce9 | ||
|
396594060b | ||
|
dd55e7c748 | ||
|
362d06654f | ||
|
db3faedbfc | ||
|
046e2cb605 | ||
|
066820900d | ||
|
f90cdf2ffb | ||
|
1af6d9a873 | ||
|
0f5eeb29e7 | ||
|
441fe9d076 | ||
|
7e0639ac57 | ||
|
898e279218 | ||
|
a38170d26a | ||
|
6f6b207866 | ||
|
876af8f27c | ||
|
8190a92aae | ||
|
378ccd17bf | ||
|
aa7a083444 | ||
|
b180b53619 | ||
|
6005585764 | ||
|
06b0ae56d4 | ||
|
c5bf83eb86 | ||
|
5a1b9daf4c | ||
|
2bcedb9820 | ||
|
9dbf3443d0 | ||
|
f50b2f5d61 | ||
|
dfce1f53a8 | ||
|
5e46b6365c | ||
|
d5bfb71e10 | ||
|
af56429e78 | ||
|
dfc6d448ed | ||
|
3a44f56f64 | ||
|
80685191ab | ||
|
184984ac51 | ||
|
09b9cd8193 | ||
|
c16fb0dae6 | ||
|
898eb5b82f | ||
|
3c7f53802f | ||
|
0dff21a80f | ||
|
7963af1d17 | ||
|
001df933a5 | ||
|
db7b7fde3f | ||
|
a5c3df2743 | ||
|
053a0052c8 | ||
|
db09a92bc5 | ||
|
b5bfe5d9a1 | ||
|
f4b2fe9540 | ||
|
fdb0720fe9 | ||
|
f913ea6b75 | ||
|
ecc036c7f4 | ||
|
dda696cce4 | ||
|
f351d5dcfd | ||
|
d2de0f3aca | ||
|
43f629f665 | ||
|
5feb210442 | ||
|
f9643bf737 | ||
|
bda4259815 | ||
|
c9ecd61f7e | ||
|
9c796e8d73 | ||
|
2e1be01ad4 | ||
|
3f5f5dbe48 | ||
|
28d3d4b122 | ||
|
391fbab674 | ||
|
2182e7f6f1 | ||
|
022c1db33f | ||
|
8c56f95252 | ||
|
2cb6cd5dad | ||
|
befba11657 | ||
|
828cf2dcad | ||
|
9830c4e02b | ||
|
0a4b827d8e | ||
|
2ea3153cd5 | ||
|
1e6be28e4b | ||
|
49713b2784 | ||
|
9285e50c63 | ||
|
bc6bcd31ad | ||
|
6a1f29d953 | ||
|
45ed6658fe | ||
|
e546ae2f85 | ||
|
e5599c178b | ||
|
ad55b945c1 | ||
|
196cef66fd | ||
|
e2d97b44c5 | ||
|
79866750d2 | ||
|
3570953d14 | ||
|
06afd04b07 | ||
|
2df1dd7215 | ||
|
1e09e133e3 | ||
|
ecfc6ae87d | ||
|
7763939e7d | ||
|
249670827c | ||
|
42bfc7bb3f | ||
|
417bfcd36e | ||
|
2052b502c8 | ||
|
7c1ad0cadb | ||
|
9d899e3dab | ||
|
3f185aada2 | ||
|
37b81c6418 | ||
|
7c5efee1de | ||
|
6b53d45ce2 | ||
|
63f24d2282 | ||
|
3d350c6087 | ||
|
85381713b2 | ||
|
f6a0f5f979 | ||
|
d44928c27b | ||
|
08a11eeacf | ||
|
b2c26a53ef | ||
|
319b6283a9 | ||
|
5f16434346 | ||
|
3001b620ec | ||
|
a376faa3a8 | ||
|
4b16183d2b | ||
|
5b91adf62d | ||
|
2fbf850841 | ||
|
e5a5ac5943 | ||
|
f326769fa8 | ||
|
4d3ff0e175 | ||
|
1cef9173a0 | ||
|
b06f496a27 | ||
|
58013f4f44 | ||
|
ad0307fda6 | ||
|
3d6a2bb945 | ||
|
bdc324760d | ||
|
eba95ebd07 | ||
|
6f2f8ae69f | ||
|
8f46f704b1 | ||
|
0ad9b1375e | ||
|
52ae9f09ce | ||
|
a1f91d6eb8 | ||
|
bfa7e4ca37 | ||
|
3ee94bdc5e | ||
|
8724949b54 | ||
|
42cc9e61fb | ||
|
d9691c9342 | ||
|
4c771bf2af | ||
|
292a36f760 | ||
|
a955138021 | ||
|
b475a2ecc6 | ||
|
175b30faf8 | ||
|
0d05d0d8f5 | ||
|
2c2a07d02f | ||
|
0e8bd8248c | ||
|
ff3c21c805 | ||
|
b102f1f345 | ||
|
73b9de2085 | ||
|
b0158b3f65 | ||
|
75db9f70df | ||
|
8f0de3d285 | ||
|
845fe88451 | ||
|
9a47fdc1ee | ||
|
23041f398b | ||
|
acb066ad2e | ||
|
b85f50e314 | ||
|
79f6da2dac | ||
|
abe4b8d5b0 | ||
|
0997e3fa9f | ||
|
9a0e030db8 | ||
|
b88ec4b814 | ||
|
4cc9645d76 | ||
|
8c38d7fb0e | ||
|
a12b49fd40 | ||
|
584e2b3743 | ||
|
ea57db7aad | ||
|
6f7640af18 | ||
|
055643bd01 | ||
|
4e73577d59 | ||
|
2117f8f54b | ||
|
320ab75818 | ||
|
9a6bc5ae72 | ||
|
ad4c49aa18 | ||
|
a886d609d9 | ||
|
65bf0e4c45 | ||
|
fa99f6a1af | ||
|
b1d6bd564e | ||
|
de7cf03ba7 | ||
|
3ddb0a3903 | ||
|
00aa0a4bf1 | ||
|
cd51c9e47c | ||
|
c0738a7ae1 | ||
|
4bf1d67eba | ||
|
c0ed709adf | ||
|
82d25a7e52 | ||
|
3150726f32 | ||
|
b192c3e6a3 | ||
|
3df4807fb9 | ||
|
d0ee3c3a56 | ||
|
acd4c8648e | ||
|
4b6c69ac1e | ||
|
dd00829f1e | ||
|
5011747f1f | ||
|
ee0fc5cc29 | ||
|
becfe2ce7a | ||
|
0a4d54fca4 | ||
|
1cd11391c4 | ||
|
44aca29057 | ||
|
0b0a1dc6a1 | ||
|
12380207ba | ||
|
2439404ad1 | ||
|
23fc38f7c9 | ||
|
e3a194acb4 | ||
|
66a508a174 | ||
|
f2f43336c6 | ||
|
ab3c5acf9a | ||
|
9c87598950 | ||
|
c924365e24 | ||
|
46ee8e86c6 | ||
|
ab9035acd2 | ||
|
96a535fe4a | ||
|
59da53ec48 | ||
|
a68800b3f0 | ||
|
38d8a54cc1 | ||
|
633986cfa6 | ||
|
c4cbead25b | ||
|
ba3a090a80 | ||
|
e1d2c81dca | ||
|
0d8497bf3b | ||
|
a6781c8e34 | ||
|
08d5bfcbd0 | ||
|
b02a22eaa3 | ||
|
e4a6ec40cd | ||
|
78514110de | ||
|
b6fe4b8fec | ||
|
39e899294f | ||
|
64d751a397 | ||
|
935ee2242d | ||
|
9e3cb8180b | ||
|
d83c154f8d | ||
|
353b88ea5a | ||
|
4ce2c0017a | ||
|
e7f174cdc8 | ||
|
aac4d03a70 | ||
|
7790307595 | ||
|
62467b6318 | ||
|
60c5d0d8f4 | ||
|
1a2e09487c | ||
|
44e2ef6c79 | ||
|
e5476e6fef | ||
|
d9ddf8858e | ||
|
f450682a22 | ||
|
7ed5b4dfbe | ||
|
d56b27e570 | ||
|
4db51dff8a | ||
|
94ce3b06eb | ||
|
1311b9393c | ||
|
5952a40c6d | ||
|
4e1f582b17 | ||
|
3ff09f7b91 | ||
|
312dac90a3 | ||
|
9c5b9abb93 | ||
|
7c3bbaca2a | ||
|
15f7c27bce | ||
|
7de1c0e237 | ||
|
adf52a1b74 | ||
|
d0faaa2ead | ||
|
61b0f09e1d | ||
|
e28fbc6678 | ||
|
026c992395 | ||
|
5722ba8306 | ||
|
d2756cf68f | ||
|
ce71b3293b | ||
|
05af5d0d74 | ||
|
cf1645b598 | ||
|
7f61b92f81 | ||
|
ce120cba13 | ||
|
09f4c5c708 | ||
|
185a93a105 | ||
|
20606b3a71 | ||
|
cb92a40156 | ||
|
52a247c156 | ||
|
bb3ccca333 | ||
|
180105a965 | ||
|
3e188d0344 | ||
|
fc765f6014 | ||
|
bf29cddbc9 | ||
|
4321153b06 | ||
|
e24c49f5be | ||
|
53920a1568 | ||
|
18f70b3bac | ||
|
5c93ea8019 | ||
|
572229e536 | ||
|
522681f463 | ||
|
5c5cee16d9 | ||
|
67b87a0ea0 | ||
|
233daafd96 | ||
|
4683e83287 | ||
|
668dcd52ca | ||
|
8ce7e776c1 | ||
|
d5e4398ace | ||
|
62737c1caf | ||
|
10b2b60415 | ||
|
c864ef7e16 | ||
|
75fbd28d3e | ||
|
2c9d43d600 | ||
|
219b4ecb77 | ||
|
9ec5707c32 | ||
|
1d6fd7898a | ||
|
2a114917f1 | ||
|
1afb5b95e3 | ||
|
8cbaacabdb | ||
|
1ed0f75c49 | ||
|
02b8f1d007 | ||
|
daec282cdf | ||
|
0c9d0db730 | ||
|
06f3dc3053 | ||
|
1a9accbe5d | ||
|
bde38fb748 | ||
|
bc799fd82c | ||
|
accb2142e7 | ||
|
26ff92caa9 | ||
|
73109eb819 | ||
|
e00496aa63 | ||
|
e19aa44d5c | ||
|
0cefc73448 | ||
|
3c56a6db4d | ||
|
9a0d6b9931 | ||
|
ddeefff431 | ||
|
958698bba7 | ||
|
1d71cdc9e0 | ||
|
241c6c4ac8 | ||
|
cb6ffeaabd | ||
|
34861ad1bc | ||
|
012cae051b | ||
|
f18ab08334 | ||
|
b1ea7572dd | ||
|
e12f6c747f | ||
|
95ea2fb40c | ||
|
57b38b24dd | ||
|
ec8bb8a06a | ||
|
1c3e7dda01 | ||
|
de17a19168 | ||
|
bfb8de2736 | ||
|
e44926114a | ||
|
326f70b678 | ||
|
7b852206f1 | ||
|
ab594ed0cb | ||
|
0f8119c400 | ||
|
ba4f4c1f78 | ||
|
e0c3143763 | ||
|
fc07e6bba7 | ||
|
3e511484c7 | ||
|
4b933069f1 | ||
|
faf7263d8f | ||
|
db3e7656e0 | ||
|
20a6d7b26b | ||
|
3f74f83964 | ||
|
bc03419902 | ||
|
8557effe13 | ||
|
c904b7ccd8 | ||
|
bfa995d52b | ||
|
493f69f195 | ||
|
6c7cfd79b9 | ||
|
8330635a72 | ||
|
a46ce053f1 | ||
|
02d0cbcfab | ||
|
88e7f0da65 | ||
|
165950169f | ||
|
29eb90e503 | ||
|
856538635d | ||
|
634bc3a8bd | ||
|
c45f2e7c39 | ||
|
393da7e57a | ||
|
4393ec0b83 | ||
|
db16cf5548 | ||
|
0f1f655e5d | ||
|
c43e2a0a3a | ||
|
74bced75b4 | ||
|
7ea4686d6c | ||
|
7f3aa43ad4 | ||
|
71ed1564cb | ||
|
202a8a171b | ||
|
bfa46f47ed | ||
|
eb58e60dd1 | ||
|
a16c60c886 | ||
|
c487340f8e | ||
|
dcc450267f | ||
|
a353ae3b65 | ||
|
c37dc69592 | ||
|
0c8a90f2a3 | ||
|
67a9718f9e | ||
|
01cf4967a5 | ||
|
79fb1a54cb | ||
|
c0e523508b | ||
|
8ea5fae61b | ||
|
d0f937bcb6 | ||
|
3729fde572 | ||
|
15f30ed942 | ||
|
0ec612d71a | ||
|
1669d80082 | ||
|
65d8205eef | ||
|
3ab9986fc7 | ||
|
ccfd7a1015 | ||
|
68438f4621 | ||
|
e3d8109110 | ||
|
0e0052888f | ||
|
1ec38aa5b2 | ||
|
e451abbf20 | ||
|
673a2ecd5d | ||
|
e9c5e719f1 | ||
|
9a86447b6e | ||
|
0814a20ec4 | ||
|
8aa15174ab | ||
|
64752d89fc | ||
|
f21abcd529 | ||
|
1e94fe25fa | ||
|
7ffb87170b | ||
|
3d32e16235 | ||
|
3a6c955c90 | ||
|
9f73c35621 | ||
|
7c6fe5c4e9 | ||
|
95dc775344 | ||
|
c6bd620555 | ||
|
8bd60f7cde | ||
|
ac8009af4a | ||
|
5f8032584b | ||
|
22e645e22f | ||
|
dd4c22d02d | ||
|
acb8518911 | ||
|
82943bd464 | ||
|
02bdf7d27c | ||
|
a2fc7dca79 | ||
|
54c8771885 | ||
|
da9505fa3c | ||
|
72dc8052b3 | ||
|
76cc076d61 | ||
|
78ee787310 | ||
|
d09f6a50b0 | ||
|
76cf208619 | ||
|
76fa7918a5 | ||
|
3c253734ac | ||
|
d68d70362b | ||
|
582a61192a | ||
|
364afd61e1 | ||
|
0683d9771a | ||
|
d196c89825 | ||
|
be8838b5f8 | ||
|
a142b7de5e | ||
|
bdb74ac235 | ||
|
5d7e9f3879 | ||
|
bea4225d28 | ||
|
1bd02d64c5 | ||
|
94ff5a8641 | ||
|
0af823e86c | ||
|
29ff3708c4 | ||
|
86bb4b4e6c | ||
|
bdff61653a | ||
|
c3188ff0fa | ||
|
fa736f81af | ||
|
627e176f8e | ||
|
ecb27f33f7 | ||
|
4499f3b95e | ||
|
f3111f93b2 | ||
|
ccbc1c669c | ||
|
7e6f12daa6 | ||
|
9121478a2e | ||
|
8b535473ce | ||
|
30fdf17902 | ||
|
acd14d7bf3 | ||
|
a4876c1ac5 | ||
|
b8aa639f3c | ||
|
03f0533139 | ||
|
99d4001db6 | ||
|
b985dcd248 | ||
|
cd37478e31 | ||
|
149b26fb51 | ||
|
3a56c8b0f4 | ||
|
6817e19923 | ||
|
57dd0827f4 | ||
|
38b929b973 | ||
|
4a1310dc21 | ||
|
4839d8bf59 | ||
|
3d1ce845be | ||
|
a1aaa96120 | ||
|
a67c94787b | ||
|
7e346180d7 | ||
|
6850903d17 | ||
|
0a3d164806 | ||
|
4a8b19b0be | ||
|
baacecadc5 | ||
|
0a8103b6e8 | ||
|
5dcc30dcc6 | ||
|
08b78f0c47 | ||
|
3039915ce9 | ||
|
ca2537941c | ||
|
6206a1a524 | ||
|
b862f215c5 | ||
|
09f27f0dd7 | ||
|
72dd36bc17 | ||
|
07b0583069 | ||
|
88d8424474 | ||
|
5e6ff67d01 | ||
|
a360d74a4c | ||
|
7de01a5f94 | ||
|
6da8d1a0ec | ||
|
d1ddfd09b6 | ||
|
40aa46e72a | ||
|
4f6e5c5f5a | ||
|
8d5a7c6ffb | ||
|
b76bed3a40 | ||
|
75ca28df49 | ||
|
c1774276c2 | ||
|
9c06f29aaf | ||
|
5c72e1286e | ||
|
0bf4c4ae75 | ||
|
95ba02a9d3 | ||
|
47956ddbca | ||
|
b4046017a7 | ||
|
8ded667a6b | ||
|
6e9d799103 | ||
|
67183ff9e8 | ||
|
dab237e758 | ||
|
9dd73cd494 | ||
|
57049de23a | ||
|
d5faf5e8aa | ||
|
61bc8f7fa3 | ||
|
bd7ab23a8f | ||
|
42874de2b2 | ||
|
00b0319397 | ||
|
f2a236eb57 | ||
|
e1905d0d7a | ||
|
61c0e63bbe | ||
|
e24dd3ad75 | ||
|
48a70308b5 | ||
|
969a36c2a8 | ||
|
f5de2cd9a0 | ||
|
c0e4d6c8b6 | ||
|
45d82f2a85 | ||
|
b1eed82b7f | ||
|
b719a2a432 | ||
|
8a933afc5d | ||
|
d3221a508a | ||
|
e1355ae5d8 | ||
|
2b277dd558 | ||
|
2ace4fde41 | ||
|
13e9119573 | ||
|
de85c34462 | ||
|
5a225d1668 | ||
|
eb44c6634b | ||
|
5498d14e54 | ||
|
48d6f15850 | ||
|
ae620db0c5 | ||
|
cbcbda5276 | ||
|
4bf85d9e8e | ||
|
649e9a7b0c | ||
|
cca50ef842 | ||
|
2ffd1e8e7c | ||
|
eb02eadb9f | ||
|
ec093f90e7 | ||
|
de46745926 | ||
|
944fb10733 | ||
|
ae1c1b3912 | ||
|
2adc746143 | ||
|
c4c263a85b | ||
|
0ced884aa3 | ||
|
7a78aebb12 | ||
|
97e4d83593 | ||
|
05b770a93f | ||
|
8e36bb4c4d | ||
|
42d5c0fe6d | ||
|
2d0fc8356f | ||
|
be65c63f16 | ||
|
8a0a18255a | ||
|
61270e0ea6 | ||
|
2d2afc5280 | ||
|
0d9e639f4f | ||
|
3f19f6fd50 | ||
|
99b15b916c | ||
|
7285b156f4 | ||
|
e8327da189 | ||
|
a7a7c4add2 | ||
|
71979e7b23 | ||
|
aa2b3daccc | ||
|
8c771a842f | ||
|
7249d01709 | ||
|
9322c37a94 | ||
|
84c4fcdec6 | ||
|
81e628b9f7 | ||
|
2a7d4317bd | ||
|
4f1edeb750 | ||
|
80c9c5dad3 | ||
|
b6b4ea669d | ||
|
4e80e21ba1 | ||
|
8b28f4ffbf | ||
|
962949008f | ||
|
3c68208c41 | ||
|
83789aaa42 | ||
|
35ba9848d9 | ||
|
86cdb7c1f8 | ||
|
bdedd2dc26 | ||
|
d61bb2e87f | ||
|
8d28d1145a | ||
|
40d32cee95 | ||
|
5877459907 | ||
|
1a056899d7 | ||
|
fd37e44854 | ||
|
634d8a7898 | ||
|
93f4de8792 | ||
|
51de0bd2da | ||
|
31a26c0a0a | ||
|
b8a38baaf6 | ||
|
9752f66eab | ||
|
770c2c504d | ||
|
065719c8d8 | ||
|
4c3e467d25 | ||
|
690a40be77 | ||
|
9d6150da37 | ||
|
e47f3ec1d6 | ||
|
27360242b0 | ||
|
30a0e39060 | ||
|
5832ab2f31 | ||
|
9ea686ab14 | ||
|
0d64fd98f7 | ||
|
e4158acd08 | ||
|
9f72bd8ca3 | ||
|
1354bf68a8 | ||
|
0b41454b01 | ||
|
e5485f3d54 | ||
|
4ebf825c43 | ||
|
6e5f90730e | ||
|
80f19bd1f0 | ||
|
c3d1d7a64c | ||
|
cf152403ee | ||
|
0b0f8f4285 | ||
|
a43830d403 | ||
|
58f3225fa6 | ||
|
a9ff328e38 | ||
|
465f38c1c6 | ||
|
383ab9b0b2 | ||
|
7c1c040d50 | ||
|
4b74d16438 | ||
|
da5c801b4d | ||
|
e5f1b2afa3 | ||
|
a4c2e45d6d | ||
|
a71b095c1d | ||
|
bcfc3e7550 | ||
|
0946a7902f | ||
|
9730894a07 | ||
|
fefd6f0e6d | ||
|
5754ad589f | ||
|
5d41246e73 | ||
|
560d4bed09 | ||
|
4ca3517e22 | ||
|
c1be0bd2e8 | ||
|
b31b239088 | ||
|
564baffa17 | ||
|
278f0e9e98 | ||
|
313caf440e | ||
|
1828dca0b9 | ||
|
e408550553 | ||
|
a7443612f6 | ||
|
aa1eec93be | ||
|
fbce902cf8 | ||
|
7f88238d8f | ||
|
0a3d6106f0 | ||
|
10251f9782 | ||
|
cfd6d3ce04 | ||
|
d92d989569 | ||
|
c6691dc6a8 | ||
|
8bd9dd66ab | ||
|
61613ab6ac | ||
|
744f5f50fe | ||
|
7d0efcf50f | ||
|
5ed7bf7815 | ||
|
b20dc3b804 | ||
|
19398d75be | ||
|
945d438696 | ||
|
532bd1c916 | ||
|
f5e611e4d2 | ||
|
716ab2f96d | ||
|
adc9b4c9f1 | ||
|
84c197be60 | ||
|
05fcbfd7b7 | ||
|
89c993f567 | ||
|
cd4b915522 | ||
|
c0e506e568 | ||
|
1e17ef1c98 | ||
|
52be689926 | ||
|
63ef7284f7 | ||
|
cb56c54351 | ||
|
1a00de6494 | ||
|
b58c0d3071 | ||
|
fce5cfea0e | ||
|
3a1496c205 | ||
|
6d004601d0 | ||
|
e84c9847c5 | ||
|
9a400748f7 | ||
|
22124b5ced | ||
|
c12c65f728 | ||
|
1dc6d226b7 | ||
|
1ead9757d3 | ||
|
599a5ac3ff | ||
|
1805dc48ec | ||
|
3ea1c9f04b | ||
|
1666976646 | ||
|
b0e96b2821 | ||
|
19664cd9cf | ||
|
01eb49d3a4 | ||
|
9fc719cb24 | ||
|
21aec00e46 | ||
|
08f8aa3c52 | ||
|
1b6b4a57d9 | ||
|
56595e4a9c | ||
|
665c844c9d | ||
|
bb23bc0fd2 | ||
|
013525797a | ||
|
4e783728f9 | ||
|
8868ce14e8 | ||
|
a151d24951 | ||
|
fee0923dd1 | ||
|
c1880c9191 | ||
|
8edbfbdced | ||
|
a7b4794585 | ||
|
68bf9f76f6 | ||
|
4c1555cc5f | ||
|
0b6d766f0c | ||
|
6d83b16503 | ||
|
bd6c03e5f9 | ||
|
20b8250037 | ||
|
9090ede5db | ||
|
badefcec48 | ||
|
fadc343821 | ||
|
73742633bd | ||
|
cf3a4bc658 | ||
|
9965cda968 | ||
|
bec0fa414e | ||
|
b32d8307ec | ||
|
9598e1877c | ||
|
2fb560624d | ||
|
68291c7b3d | ||
|
29b21209bf | ||
|
21a8a50ab9 | ||
|
5e5fe5876a | ||
|
ff8349ff3f | ||
|
22fcdeef7f | ||
|
f95933c246 | ||
|
41e4d0f788 | ||
|
a9ab3e1211 | ||
|
c0828f590f | ||
|
85be48f42b | ||
|
38900c5079 | ||
|
3398bee77a | ||
|
34a8140ff0 | ||
|
c95467ea3e | ||
|
7225b7a40f | ||
|
5377169db2 | ||
|
ad963fd23e | ||
|
a59f53d592 | ||
|
39d9531483 | ||
|
5554b414e1 | ||
|
7523869875 | ||
|
c902428af1 | ||
|
8abc7ade22 | ||
|
4d35e8c80f | ||
|
facf3ae582 | ||
|
0239852cc7 | ||
|
9db9db1ade | ||
|
e71638abf1 | ||
|
3126533a33 | ||
|
0b4d64947b | ||
|
f6fe580eb7 | ||
|
9eabca6987 | ||
|
436fb64289 | ||
|
0662ead33b | ||
|
347db79979 | ||
|
04ba2e1fc7 | ||
|
8f302bcdb0 | ||
|
2e4476a754 | ||
|
f6c4ab6f41 | ||
|
7c48857d0c | ||
|
a5f5d6ef23 | ||
|
05e5becd78 | ||
|
598b9f25e1 | ||
|
9d5344e90d | ||
|
43505e0aad | ||
|
fef580c24b | ||
|
4696dfc25e | ||
|
916b379c03 | ||
|
1b703e905c | ||
|
8884015dae | ||
|
b873aa67cc | ||
|
baa8970bb6 | ||
|
3d72c10ea5 | ||
|
758556cd30 | ||
|
fcfebf75a3 | ||
|
ae8f1fed05 | ||
|
0f69455dc7 | ||
|
6799295115 | ||
|
4cc2a17765 | ||
|
dd1ca16ded | ||
|
c4d65f8bf4 | ||
|
df534585e9 | ||
|
2681dc09bb | ||
|
70e0d865a8 | ||
|
8429f9bd3c | ||
|
934c733ccb | ||
|
05d174d4ce | ||
|
f66d65d409 | ||
|
6b50152bb3 | ||
|
d02d0e2d5e | ||
|
d508e58d49 | ||
|
bf71e49fcc | ||
|
b9133567af | ||
|
c99157ade2 | ||
|
c73b8eda26 | ||
|
202ce1f494 | ||
|
40ded93c7c | ||
|
4f647847e7 | ||
|
968da5f72d | ||
|
49d8a3fb33 | ||
|
5b8e6531fa | ||
|
6d6c1917bc | ||
|
60606b9994 | ||
|
35dc46ffb0 | ||
|
e3991fadd5 | ||
|
f765f73fa3 | ||
|
e2f44ddbea | ||
|
19f38d6733 | ||
|
36eb1b1009 | ||
|
1e4a12d2f7 | ||
|
5b098a909a | ||
|
8e36c0002b | ||
|
70b08c4952 | ||
|
0934f71c02 | ||
|
abadf3c789 | ||
|
6de7329ce7 | ||
|
96270bdc18 | ||
|
6db60627e6 | ||
|
f540c4e089 | ||
|
628a16f287 | ||
|
41bfb8ae52 | ||
|
d1c755809d | ||
|
2e544270cd | ||
|
5bb2d9adf3 | ||
|
559a40c7ea | ||
|
99ad26bfa4 | ||
|
5c85f830bd | ||
|
fd24f7087e | ||
|
5772a5483c | ||
|
47d9de98ed | ||
|
d25442345e | ||
|
4899788d92 | ||
|
bf11bbd8a6 | ||
|
8f8ae9aee5 | ||
|
1007e19172 | ||
|
3b4d00564d | ||
|
45d0ba9e2f | ||
|
45fdd098cc | ||
|
8c428e8566 | ||
|
c39cc06908 | ||
|
de84bf08bf | ||
|
e23308c0f9 | ||
|
9a98d41a2c | ||
|
274fa72a8c | ||
|
abd26af0ad | ||
|
fc2977fc0d | ||
|
c8f16a4e89 | ||
|
c9e9b82eac | ||
|
b79c4510a6 | ||
|
c2c8a3caeb | ||
|
8492300780 | ||
|
cf867954c3 | ||
|
c11d71c3cd | ||
|
c1ae7d67a9 | ||
|
10cd61d2f9 | ||
|
86a8928278 | ||
|
d1fee27814 | ||
|
33742c809a | ||
|
747caf1652 | ||
|
bb5c1f24c6 | ||
|
8f44364c6c | ||
|
b0883148f5 | ||
|
09e58c4e53 | ||
|
61593279c9 | ||
|
51f0bf5d84 | ||
|
21545c56bb | ||
|
be4f89740a | ||
|
5ac88f764d | ||
|
fbf7f75b00 | ||
|
d31aaa5d0d | ||
|
ae4d4ba3ef | ||
|
f271316d7d | ||
|
eda4178333 | ||
|
ca2c8687c8 | ||
|
d892a537a7 | ||
|
00c8aa847d | ||
|
b57e3e3e0a | ||
|
e565552ae9 | ||
|
a5b107e6c9 | ||
|
0a147d5b95 | ||
|
1544577757 | ||
|
f99b4874c8 | ||
|
4b9b77614f | ||
|
8c0250f775 | ||
|
c51a17bf9a | ||
|
77be6a2755 | ||
|
72927a4ec3 | ||
|
1b424b3fe7 | ||
|
eda8d0dbc8 | ||
|
2631144702 | ||
|
4ccabaf422 | ||
|
95cf873bad | ||
|
229969192a | ||
|
619e4dc2d6 | ||
|
4ad9c9bf31 | ||
|
f1157b8fd1 | ||
|
342a4dd502 | ||
|
fc46e1ec20 | ||
|
b66c1e6084 | ||
|
100cd807f6 | ||
|
a6c3c382b4 | ||
|
f73ae42a03 | ||
|
382106aafb | ||
|
18f457a41d | ||
|
b0e0bc3701 | ||
|
deb6ca0da0 | ||
|
fb9796a293 | ||
|
ffc0a8808b | ||
|
de74711e82 | ||
|
f16ed8235c | ||
|
ab557a8cef | ||
|
46fea3fc93 | ||
|
6823b6c691 | ||
|
d2ac7e5b0a | ||
|
a623006ea0 | ||
|
24986bbea0 | ||
|
aefa429236 | ||
|
8224e5aabf | ||
|
b9d4eb5449 | ||
|
e852dccebf | ||
|
2aa089f29c | ||
|
d3feaeedb2 | ||
|
7100b75598 | ||
|
09f994c105 | ||
|
6839bfcc2d | ||
|
e71c556ca7 | ||
|
da5c171346 | ||
|
5018879f84 | ||
|
7ee7b43547 | ||
|
932d3bd033 | ||
|
34879a4b35 | ||
|
027d08cec7 | ||
|
52b179dba8 | ||
|
f3013c6817 | ||
|
c48d41d99d | ||
|
f9fc433c0f | ||
|
40730e7862 | ||
|
424079aa12 | ||
|
0b89d9d3f9 | ||
|
50b77d881d | ||
|
e4cfd964d5 | ||
|
4901447ad6 | ||
|
01848de7e2 | ||
|
4d8f078ad9 | ||
|
b8912ea0aa | ||
|
8eca29be25 | ||
|
f05109f186 | ||
|
f646863149 | ||
|
ed1bcb509f | ||
|
9a6f4d35f2 | ||
|
10b9b4b969 | ||
|
4a8a85d7a6 | ||
|
16dd47f5ec | ||
|
d2e995ef95 | ||
|
09b16f96fc | ||
|
7e168cb4fb | ||
|
5b2cfffedc | ||
|
9362f03fd5 | ||
|
d214c78bd0 | ||
|
aaee092a46 | ||
|
d854babf22 | ||
|
d6ec883cd9 | ||
|
8ed3ddba31 | ||
|
207d5ebdcb | ||
|
f90dd76f4c | ||
|
0f7c2b891a | ||
|
924b59d735 | ||
|
dfab8f5e20 | ||
|
f046d1f0a3 | ||
|
f6bc80bc6b | ||
|
cdbd1f6193 | ||
|
0fc97a3e8c | ||
|
8e6b98669a | ||
|
c70943bb0e | ||
|
20a8081e8e | ||
|
67c5572d7b | ||
|
45999001be | ||
|
15546fd957 | ||
|
24970a875a | ||
|
2863eb8cc9 | ||
|
5beaf46e09 | ||
|
b0e587c03d | ||
|
0d8b15a109 | ||
|
091180b32d | ||
|
eacfa226fd | ||
|
2c61c50671 | ||
|
4562ba9ccf | ||
|
7dece209a0 | ||
|
f2e77f4030 | ||
|
740a715acd | ||
|
0755421fc2 | ||
|
c248a102dc | ||
|
396ec908dd | ||
|
f51b56558d | ||
|
39929db2b8 | ||
|
a2bae8374e | ||
|
0686ec4440 | ||
|
9dc4009152 | ||
|
b93b01cb02 | ||
|
6b280dc3bd | ||
|
945b34b103 | ||
|
8563b9560d | ||
|
939854a0dd | ||
|
f5bc952309 | ||
|
e2fe3eb503 | ||
|
83f60deef9 | ||
|
6d30a38316 | ||
|
542d0f539b | ||
|
e5fc9d8674 | ||
|
a9a2401e44 | ||
|
419fe6dca3 | ||
|
9cbc088b76 | ||
|
477fbd8dc7 | ||
|
8f04ec820f | ||
|
bb180a1db8 | ||
|
e3c4bd46fb | ||
|
b219da6b87 | ||
|
34cefed9ff | ||
|
99129daeee | ||
|
ee4c952290 | ||
|
0094eb391e | ||
|
cf6686ff42 | ||
|
e677a6bb05 | ||
|
6e77f583f1 | ||
|
cca7055fcf | ||
|
74f7ae525f | ||
|
9ee415749d | ||
|
32c884d543 | ||
|
27345d5749 | ||
|
26c773392e | ||
|
2d94a104d1 | ||
|
ded24db3dd | ||
|
8c14259728 | ||
|
fb9660afe0 | ||
|
67be6418b6 | ||
|
3dd8b7c6d1 | ||
|
653f3c043d | ||
|
730cf31921 | ||
|
d392939018 | ||
|
0fcc2e5e52 | ||
|
aa3f26263c | ||
|
dedbf29ca4 | ||
|
dc77136453 | ||
|
ef60ade647 | ||
|
7e7bbcf4c0 | ||
|
a5d4e97922 | ||
|
ebde3be820 | ||
|
db83709c6b | ||
|
0cc8bca098 | ||
|
1b9d6aac06 | ||
|
37b9922f64 | ||
|
e319fa3aa9 | ||
|
3fd7c33127 | ||
|
bc1fd9039d | ||
|
cf7e5d5592 | ||
|
21ffa2f26b | ||
|
ac567ebf1d | ||
|
3b1142aaca | ||
|
5edc2216c7 | ||
|
2d275989cb | ||
|
105bd52eee | ||
|
b02ebcb69b | ||
|
470fb9f5df | ||
|
9402b4a26d | ||
|
5daad2aaab | ||
|
daf94e416b | ||
|
2fd51b8582 | ||
|
096424ea78 | ||
|
340f5614b5 | ||
|
2468b32fc5 | ||
|
c2966297f1 |
8
.coveragerc
Normal file
8
.coveragerc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[run]
|
||||||
|
branch = true
|
||||||
|
parallel = true
|
||||||
|
source =
|
||||||
|
telethon
|
||||||
|
|
||||||
|
[report]
|
||||||
|
precision = 2
|
15
.github/ISSUE_TEMPLATE.md
vendored
15
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,15 +0,0 @@
|
||||||
DON'T ASK QUESTIONS HERE.
|
|
||||||
|
|
||||||
This place is for issues with the library, so please make sure that:
|
|
||||||
1. The error is in the library's code, and not in your own.
|
|
||||||
2. You have already searched for your error on existing issues.
|
|
||||||
3. You are testing with upstream code (pip install -U https://github.com/LonamiWebs/Telethon/archive/master.zip).
|
|
||||||
|
|
||||||
If you have a question, ask in the official Telegram group https://t.me/TelethonChat or https://stackoverflow.com. Enhancement ideas are currently welcome, too.
|
|
||||||
|
|
||||||
If you paste code, please put it between three backticks (`):
|
|
||||||
```python
|
|
||||||
code here
|
|
||||||
```
|
|
||||||
|
|
||||||
Once you have read and understood this, delete all this text and detail whatever issue you are posting.
|
|
96
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
96
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal 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
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Ask questions in StackOverflow
|
||||||
|
url: https://stackoverflow.com/questions/ask?tags=telethon
|
||||||
|
about: Questions are not bugs. Please ask them in StackOverflow instead. Questions in the bug tracker will be closed
|
||||||
|
- name: Find about updates and our Telegram groups
|
||||||
|
url: https://t.me/s/TelethonUpdates
|
||||||
|
about: Be notified of updates, chat with other people about the library or ask questions in these groups
|
22
.github/ISSUE_TEMPLATE/documentation-issue.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/documentation-issue.yml
vendored
Normal 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
|
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
5
.github/pull_request_template.md
vendored
Normal 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!
|
||||||
|
-->
|
28
.github/workflows.disabled/python.yml
vendored
Normal file
28
.github/workflows.disabled/python.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: Python Library
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.5", "3.6", "3.7", "3.8"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Set up env
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install tox
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
tox -e flake
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
# use "py", which is the default python version
|
||||||
|
tox -e py
|
115
.gitignore
vendored
Executable file → Normal file
115
.gitignore
vendored
Executable file → Normal file
|
@ -1,108 +1,23 @@
|
||||||
# Docs
|
|
||||||
_build/
|
|
||||||
docs/
|
|
||||||
|
|
||||||
# Generated code
|
# Generated code
|
||||||
telethon/tl/functions/
|
/telethon/tl/functions/
|
||||||
telethon/tl/types/
|
/telethon/tl/types/
|
||||||
telethon/tl/patched/
|
/telethon/tl/alltlobjects.py
|
||||||
telethon/tl/alltlobjects.py
|
/telethon/errors/rpcerrorlist.py
|
||||||
telethon/errors/rpcerrorlist.py
|
|
||||||
|
|
||||||
# User session
|
# User session
|
||||||
*.session
|
*.session
|
||||||
usermedia/
|
/usermedia/
|
||||||
|
|
||||||
# Quick tests should live in this file
|
# Builds and testing
|
||||||
example.py
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
/dist/
|
||||||
*$py.class
|
/build/
|
||||||
|
/*.egg-info/
|
||||||
|
/readthedocs/_build/
|
||||||
|
/.tox/
|
||||||
|
|
||||||
# C extensions
|
# API reference docs
|
||||||
*.so
|
/docs/
|
||||||
|
|
||||||
# Distribution / packaging
|
# File used to manually test new changes, contains sensitive data
|
||||||
.Python
|
/example.py
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*,cover
|
|
||||||
.hypothesis/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# IPython Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
.python-version
|
|
||||||
|
|
||||||
# celery beat schedule file
|
|
||||||
celerybeat-schedule
|
|
||||||
|
|
||||||
# dotenv
|
|
||||||
.env
|
|
||||||
|
|
||||||
# virtualenv
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
18
.readthedocs.yaml
Normal file
18
.readthedocs.yaml
Normal 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
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016 LonamiWebs
|
Copyright (c) 2016-Present LonamiWebs
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
include LICENSE
|
|
||||||
include README.rst
|
|
||||||
|
|
||||||
recursive-include telethon *
|
|
43
README.rst
43
README.rst
|
@ -4,16 +4,16 @@ Telethon
|
||||||
|
|
||||||
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
⭐️ Thanks **everyone** who has starred the project, it means a lot!
|
||||||
|
|
||||||
|logo| **Telethon** is an `asyncio
|
|logo| **Telethon** is an asyncio_ **Python 3**
|
||||||
<https://docs.python.org/3/library/asyncio.html>`_ **Python 3** library
|
MTProto_ library to interact with Telegram_'s API
|
||||||
to interact with Telegram's API.
|
as a user or through a bot account (bot API alternative).
|
||||||
|
|
||||||
**If you're upgrading from Telethon pre-1.0 to 1.0, please make sure to read**
|
.. important::
|
||||||
`this section of the documentation
|
|
||||||
<https://telethon.readthedocs.io/en/latest/extra/basic/asyncio-magic.html>`_,
|
If you have code using Telethon before its 1.0 version, you must
|
||||||
or ``pip install telethon-sync`` which is compatible with `synchronous code
|
read `Compatibility and Convenience`_ to learn how to migrate.
|
||||||
<https://github.com/LonamiWebs/Telethon/tree/sync>`_. Don't forget to remove
|
As with any third-party library for Telegram, be careful not to
|
||||||
the asynchronous version (``pip uninstall telethon``) if you do install sync.
|
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
||||||
|
|
||||||
What is this?
|
What is this?
|
||||||
-------------
|
-------------
|
||||||
|
@ -27,7 +27,7 @@ heavy job for you, so you can focus on developing an application.
|
||||||
Installing
|
Installing
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. code:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
pip3 install telethon
|
pip3 install telethon
|
||||||
|
|
||||||
|
@ -35,9 +35,9 @@ Installing
|
||||||
Creating a client
|
Creating a client
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
from telethon import TelegramClient, events, sync
|
||||||
|
|
||||||
# These example values won't work. You must get your own api_id and
|
# These example values won't work. You must get your own api_id and
|
||||||
# api_hash from https://my.telegram.org, under API Development.
|
# api_hash from https://my.telegram.org, under API Development.
|
||||||
|
@ -51,7 +51,7 @@ Creating a client
|
||||||
Doing stuff
|
Doing stuff
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
print(client.get_me().stringify())
|
print(client.get_me().stringify())
|
||||||
|
|
||||||
|
@ -62,14 +62,25 @@ Doing stuff
|
||||||
messages = client.get_messages('username')
|
messages = client.get_messages('username')
|
||||||
messages[0].download_media()
|
messages[0].download_media()
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||||
|
async def handler(event):
|
||||||
|
await event.respond('Hey!')
|
||||||
|
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Do you like how Telethon looks? Check out `Read The Docs
|
Do you like how Telethon looks? Check out `Read The Docs`_ for a more
|
||||||
<http://telethon.rtfd.io/>`_ for a more in-depth explanation,
|
in-depth explanation, with examples, troubleshooting issues, and more
|
||||||
with examples, troubleshooting issues, and more useful information.
|
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/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
|
.. |logo| image:: logo.svg
|
||||||
:width: 24pt
|
:width: 24pt
|
||||||
|
|
3
dev-requirements.txt
Normal file
3
dev-requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-asyncio
|
|
@ -1,3 +1,5 @@
|
||||||
cryptg
|
cryptg
|
||||||
pysocks
|
pysocks
|
||||||
hachoir3
|
python-socks[asyncio]
|
||||||
|
hachoir
|
||||||
|
pillow
|
||||||
|
|
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# https://snarky.ca/what-the-heck-is-pyproject-toml/
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
# Need to use legacy format for the time being
|
||||||
|
# https://tox.readthedocs.io/en/3.20.0/example/basic.html#pyproject-toml-tox-legacy-ini
|
||||||
|
[tool.tox]
|
||||||
|
legacy_tox_ini = """
|
||||||
|
[tox]
|
||||||
|
envlist = py35,py36,py37,py38
|
||||||
|
|
||||||
|
# run with tox -e py
|
||||||
|
[testenv]
|
||||||
|
deps =
|
||||||
|
-rrequirements.txt
|
||||||
|
-roptional-requirements.txt
|
||||||
|
-rdev-requirements.txt
|
||||||
|
commands =
|
||||||
|
# NOTE: you can run any command line tool here - not just tests
|
||||||
|
pytest {posargs}
|
||||||
|
|
||||||
|
# run with tox -e flake
|
||||||
|
[testenv:flake]
|
||||||
|
deps =
|
||||||
|
-rrequirements.txt
|
||||||
|
-roptional-requirements.txt
|
||||||
|
-rdev-requirements.txt
|
||||||
|
flake8
|
||||||
|
commands =
|
||||||
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
|
flake8 telethon/ telethon_generator/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 telethon/ telethon_generator/ tests/ --count --exit-zero --exclude telethon/tl/,telethon/errors/rpcerrorlist.py --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
|
"""
|
96
readthedocs/basic/installation.rst
Normal file
96
readthedocs/basic/installation.rst
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
.. _installation:
|
||||||
|
|
||||||
|
============
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
Telethon is a Python library, which means you need to download and install
|
||||||
|
Python from https://www.python.org/downloads/ if you haven't already. Once
|
||||||
|
you have Python installed, `upgrade pip`__ and run:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install --upgrade telethon
|
||||||
|
|
||||||
|
…to install or upgrade the library to the latest version.
|
||||||
|
|
||||||
|
.. __: https://pythonspeed.com/articles/upgrade-pip/
|
||||||
|
|
||||||
|
Installing Development Versions
|
||||||
|
===============================
|
||||||
|
|
||||||
|
If you want the *latest* unreleased changes,
|
||||||
|
you can run the following command instead:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
python3 -m pip install --upgrade https://github.com/LonamiWebs/Telethon/archive/v1.zip
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The development version may have bugs and is not recommended for production
|
||||||
|
use. However, when you are `reporting a library bug`__, you should try if the
|
||||||
|
bug still occurs in this version.
|
||||||
|
|
||||||
|
.. __: https://github.com/LonamiWebs/Telethon/issues/
|
||||||
|
|
||||||
|
|
||||||
|
Verification
|
||||||
|
============
|
||||||
|
|
||||||
|
To verify that the library is installed correctly, run the following command:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
python3 -c "import telethon; print(telethon.__version__)"
|
||||||
|
|
||||||
|
The version number of the library should show in the output.
|
||||||
|
|
||||||
|
|
||||||
|
Optional Dependencies
|
||||||
|
=====================
|
||||||
|
|
||||||
|
If cryptg_ is installed, **the library will work a lot faster**, since
|
||||||
|
encryption and decryption will be made in C instead of Python. If your
|
||||||
|
code deals with a lot of updates or you are downloading/uploading a lot
|
||||||
|
of files, you will notice a considerable speed-up (from a hundred kilobytes
|
||||||
|
per second to several megabytes per second, if your connection allows it).
|
||||||
|
If it's not installed, pyaes_ will be used (which is pure Python, so it's
|
||||||
|
much slower).
|
||||||
|
|
||||||
|
If pillow_ is installed, large images will be automatically resized when
|
||||||
|
sending photos to prevent Telegram from failing with "invalid image".
|
||||||
|
Official clients also do this.
|
||||||
|
|
||||||
|
If aiohttp_ is installed, the library will be able to download
|
||||||
|
:tl:`WebDocument` media files (otherwise you will get an error).
|
||||||
|
|
||||||
|
If hachoir_ is installed, it will be used to extract metadata from files
|
||||||
|
when sending documents. Telegram uses this information to show the song's
|
||||||
|
performer, artist, title, duration, and for videos too (including size).
|
||||||
|
Otherwise, they will default to empty values, and you can set the attributes
|
||||||
|
manually.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Some of the modules may require additional dependencies before being
|
||||||
|
installed through ``pip``. If you have an ``apt``-based system, consider
|
||||||
|
installing the most commonly missing dependencies (with the right ``pip``):
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
apt update
|
||||||
|
apt install clang lib{jpeg-turbo,webp}-dev python{,-dev} zlib-dev
|
||||||
|
pip install -U --user setuptools
|
||||||
|
pip install -U --user telethon cryptg pillow
|
||||||
|
|
||||||
|
Thanks to `@bb010g`_ for writing down this nice list.
|
||||||
|
|
||||||
|
|
||||||
|
.. _cryptg: https://github.com/cher-nov/cryptg
|
||||||
|
.. _pyaes: https://github.com/ricmoo/pyaes
|
||||||
|
.. _pillow: https://python-pillow.org
|
||||||
|
.. _aiohttp: https://docs.aiohttp.org
|
||||||
|
.. _hachoir: https://hachoir.readthedocs.io
|
||||||
|
.. _@bb010g: https://static.bb010g.com
|
46
readthedocs/basic/next-steps.rst
Normal file
46
readthedocs/basic/next-steps.rst
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
==========
|
||||||
|
Next Steps
|
||||||
|
==========
|
||||||
|
|
||||||
|
These basic first steps should have gotten you started with the library.
|
||||||
|
|
||||||
|
By now, you should know how to call friendly methods and how to work with
|
||||||
|
the returned objects, how things work inside event handlers, etc.
|
||||||
|
|
||||||
|
Next, we will see a quick reference summary of *all* the methods and
|
||||||
|
properties that you will need when using the library. If you follow
|
||||||
|
the links there, you will expand the documentation for the method
|
||||||
|
and property, with more examples on how to use them.
|
||||||
|
|
||||||
|
Therefore, **you can find an example on every method** of the client
|
||||||
|
to learn how to use it, as well as a description of all the arguments.
|
||||||
|
|
||||||
|
After that, we will go in-depth with some other important concepts
|
||||||
|
that are worth learning and understanding.
|
||||||
|
|
||||||
|
From now on, you can keep pressing the "Next" button if you want,
|
||||||
|
or use the menu on the left, since some pages are quite lengthy.
|
||||||
|
|
||||||
|
A note on developing applications
|
||||||
|
=================================
|
||||||
|
|
||||||
|
If you're using the library to make an actual application (and not just
|
||||||
|
automate things), you should make sure to `comply with the ToS`__:
|
||||||
|
|
||||||
|
[…] when logging in as an existing user, apps are supposed to call
|
||||||
|
[:tl:`GetTermsOfServiceUpdate`] to check for any updates to the Terms of
|
||||||
|
Service; this call should be repeated after ``expires`` seconds have
|
||||||
|
elapsed. If an update to the Terms Of Service is available, clients are
|
||||||
|
supposed to show a consent popup; if accepted, clients should call
|
||||||
|
[:tl:`AcceptTermsOfService`], providing the ``termsOfService id`` JSON
|
||||||
|
object; in case of denial, clients are to delete the account using
|
||||||
|
[:tl:`DeleteAccount`], providing Decline ToS update as deletion reason.
|
||||||
|
|
||||||
|
.. __: https://core.telegram.org/api/config#terms-of-service
|
||||||
|
|
||||||
|
However, if you use the library to automate or enhance your Telegram
|
||||||
|
experience, it's very likely that you are using other applications doing this
|
||||||
|
check for you (so you wouldn't run the risk of violating the ToS).
|
||||||
|
|
||||||
|
The library itself will not automatically perform this check or accept the ToS
|
||||||
|
because it should require user action (the only exception is during sign-up).
|
111
readthedocs/basic/quick-start.rst
Normal file
111
readthedocs/basic/quick-start.rst
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
===========
|
||||||
|
Quick-Start
|
||||||
|
===========
|
||||||
|
|
||||||
|
Let's see a longer example to learn some of the methods that the library
|
||||||
|
has to offer. These are known as "friendly methods", and you should always
|
||||||
|
use these if possible.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
# Remember to use your own values from my.telegram.org!
|
||||||
|
api_id = 12345
|
||||||
|
api_hash = '0123456789abcdef0123456789abcdef'
|
||||||
|
client = TelegramClient('anon', api_id, api_hash)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Getting information about yourself
|
||||||
|
me = await client.get_me()
|
||||||
|
|
||||||
|
# "me" is a user object. You can pretty-print
|
||||||
|
# any Telegram object with the "stringify" method:
|
||||||
|
print(me.stringify())
|
||||||
|
|
||||||
|
# When you print something, you see a representation of it.
|
||||||
|
# You can access all attributes of Telegram objects with
|
||||||
|
# the dot operator. For example, to get the username:
|
||||||
|
username = me.username
|
||||||
|
print(username)
|
||||||
|
print(me.phone)
|
||||||
|
|
||||||
|
# You can print all the dialogs/conversations that you are part of:
|
||||||
|
async for dialog in client.iter_dialogs():
|
||||||
|
print(dialog.name, 'has ID', dialog.id)
|
||||||
|
|
||||||
|
# You can send messages to yourself...
|
||||||
|
await client.send_message('me', 'Hello, myself!')
|
||||||
|
# ...to some chat ID
|
||||||
|
await client.send_message(-100123456, 'Hello, group!')
|
||||||
|
# ...to your contacts
|
||||||
|
await client.send_message('+34600123123', 'Hello, friend!')
|
||||||
|
# ...or even to any username
|
||||||
|
await client.send_message('username', 'Testing Telethon!')
|
||||||
|
|
||||||
|
# You can, of course, use markdown in your messages:
|
||||||
|
message = await client.send_message(
|
||||||
|
'me',
|
||||||
|
'This message has **bold**, `code`, __italics__ and '
|
||||||
|
'a [nice website](https://example.com)!',
|
||||||
|
link_preview=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sending a message returns the sent message object, which you can use
|
||||||
|
print(message.raw_text)
|
||||||
|
|
||||||
|
# You can reply to messages directly if you have a message object
|
||||||
|
await message.reply('Cool!')
|
||||||
|
|
||||||
|
# Or send files, songs, documents, albums...
|
||||||
|
await client.send_file('me', '/home/me/Pictures/holidays.jpg')
|
||||||
|
|
||||||
|
# You can print the message history of any chat:
|
||||||
|
async for message in client.iter_messages('me'):
|
||||||
|
print(message.id, message.text)
|
||||||
|
|
||||||
|
# You can download media from messages, too!
|
||||||
|
# The method will return the path where the file was saved.
|
||||||
|
if message.photo:
|
||||||
|
path = await message.download_media()
|
||||||
|
print('File saved to', path) # printed after download is done
|
||||||
|
|
||||||
|
with client:
|
||||||
|
client.loop.run_until_complete(main())
|
||||||
|
|
||||||
|
|
||||||
|
Here, we show how to sign in, get information about yourself, send
|
||||||
|
messages, files, getting chats, printing messages, and downloading
|
||||||
|
files.
|
||||||
|
|
||||||
|
You should make sure that you understand what the code shown here
|
||||||
|
does, take note on how methods are called and used and so on before
|
||||||
|
proceeding. We will see all the available methods later on.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
Note that Telethon is an asynchronous library, and as such, you should
|
||||||
|
get used to it and learn a bit of basic `asyncio`. This will help a lot.
|
||||||
|
As a quick start, this means you generally want to write all your code
|
||||||
|
inside some ``async def`` like so:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
client = ...
|
||||||
|
|
||||||
|
async def do_something(me):
|
||||||
|
...
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Most of your code should go here.
|
||||||
|
# You can of course make and use your own async def (do_something).
|
||||||
|
# They only need to be async if they need to await things.
|
||||||
|
me = await client.get_me()
|
||||||
|
await do_something(me)
|
||||||
|
|
||||||
|
with client:
|
||||||
|
client.loop.run_until_complete(main())
|
||||||
|
|
||||||
|
After you understand this, you may use the ``telethon.sync`` hack if you
|
||||||
|
want do so (see :ref:`compatibility-and-convenience`), but note you may
|
||||||
|
run into other issues (iPython, Anaconda, etc. have some issues with it).
|
229
readthedocs/basic/signing-in.rst
Normal file
229
readthedocs/basic/signing-in.rst
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
.. _signing-in:
|
||||||
|
|
||||||
|
==========
|
||||||
|
Signing In
|
||||||
|
==========
|
||||||
|
|
||||||
|
Before working with Telegram's API, you need to get your own API ID and hash:
|
||||||
|
|
||||||
|
1. `Login to your Telegram account <https://my.telegram.org/>`_ with the
|
||||||
|
phone number of the developer account to use.
|
||||||
|
|
||||||
|
2. Click under API Development tools.
|
||||||
|
|
||||||
|
3. A *Create new application* window will appear. Fill in your application
|
||||||
|
details. There is no need to enter any *URL*, and only the first two
|
||||||
|
fields (*App title* and *Short name*) can currently be changed later.
|
||||||
|
|
||||||
|
4. Click on *Create application* at the end. Remember that your
|
||||||
|
**API hash is secret** and Telegram won't let you revoke it.
|
||||||
|
Don't post it anywhere!
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This API ID and hash is the one used by *your application*, not your
|
||||||
|
phone number. You can use this API ID and hash with *any* phone number
|
||||||
|
or even for bot accounts.
|
||||||
|
|
||||||
|
|
||||||
|
Editing the Code
|
||||||
|
================
|
||||||
|
|
||||||
|
This is a little introduction for those new to Python programming in general.
|
||||||
|
|
||||||
|
We will write our code inside ``hello.py``, so you can use any text
|
||||||
|
editor that you like. To run the code, use ``python3 hello.py`` from
|
||||||
|
the terminal.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
Don't call your script ``telethon.py``! Python will try to import
|
||||||
|
the client from there and it will fail with an error such as
|
||||||
|
"ImportError: cannot import name 'TelegramClient' ...".
|
||||||
|
|
||||||
|
|
||||||
|
Signing In
|
||||||
|
==========
|
||||||
|
|
||||||
|
We can finally write some code to log into our account!
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
# Use your own values from my.telegram.org
|
||||||
|
api_id = 12345
|
||||||
|
api_hash = '0123456789abcdef0123456789abcdef'
|
||||||
|
|
||||||
|
# The first parameter is the .session file name (absolute paths allowed)
|
||||||
|
with TelegramClient('anon', api_id, api_hash) as client:
|
||||||
|
client.loop.run_until_complete(client.send_message('me', 'Hello, myself!'))
|
||||||
|
|
||||||
|
|
||||||
|
In the first line, we import the class name so we can create an instance
|
||||||
|
of the client. Then, we define variables to store our API ID and hash
|
||||||
|
conveniently.
|
||||||
|
|
||||||
|
At last, we create a new `TelegramClient <telethon.client.telegramclient.TelegramClient>`
|
||||||
|
instance and call it ``client``. We can now use the client variable
|
||||||
|
for anything that we want, such as sending a message to ourselves.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Since Telethon is an asynchronous library, you need to ``await``
|
||||||
|
coroutine functions to have them run (or otherwise, run the loop
|
||||||
|
until they are complete). In this tiny example, we don't bother
|
||||||
|
making an ``async def main()``.
|
||||||
|
|
||||||
|
See :ref:`mastering-asyncio` to find out more.
|
||||||
|
|
||||||
|
|
||||||
|
Using a ``with`` block is the preferred way to use the library. It will
|
||||||
|
automatically `start() <telethon.client.auth.AuthMethods.start>` the client,
|
||||||
|
logging or signing up if necessary.
|
||||||
|
|
||||||
|
If the ``.session`` file already existed, it will not login
|
||||||
|
again, so be aware of this if you move or rename the file!
|
||||||
|
|
||||||
|
|
||||||
|
Signing In as a Bot Account
|
||||||
|
===========================
|
||||||
|
|
||||||
|
You can also use Telethon for your bots (normal bot accounts, not users).
|
||||||
|
You will still need an API ID and hash, but the process is very similar:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
|
||||||
|
api_id = 12345
|
||||||
|
api_hash = '0123456789abcdef0123456789abcdef'
|
||||||
|
bot_token = '12345:0123456789abcdef0123456789abcdef'
|
||||||
|
|
||||||
|
# We have to manually call "start" if we want an explicit bot token
|
||||||
|
bot = TelegramClient('bot', api_id, api_hash).start(bot_token=bot_token)
|
||||||
|
|
||||||
|
# But then we can use the client instance as usual
|
||||||
|
with bot:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
To get a bot account, you need to talk
|
||||||
|
with `@BotFather <https://t.me/BotFather>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Signing In behind a Proxy
|
||||||
|
=========================
|
||||||
|
|
||||||
|
If you need to use a proxy to access Telegram,
|
||||||
|
you will need to either:
|
||||||
|
|
||||||
|
* For Python >= 3.6 : `install python-socks[asyncio]`__
|
||||||
|
* For Python <= 3.5 : `install PySocks`__
|
||||||
|
|
||||||
|
and then change
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
TelegramClient('anon', api_id, api_hash)
|
||||||
|
|
||||||
|
with
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
TelegramClient('anon', api_id, api_hash, proxy=("socks5", '127.0.0.1', 4444))
|
||||||
|
|
||||||
|
(of course, replacing the protocol, IP and port with the protocol, IP and port of the proxy).
|
||||||
|
|
||||||
|
The ``proxy=`` argument should be a dict (or tuple, for backwards compatibility),
|
||||||
|
consisting of parameters described `in PySocks usage`__.
|
||||||
|
|
||||||
|
The allowed values for the argument ``proxy_type`` are:
|
||||||
|
|
||||||
|
* For Python <= 3.5:
|
||||||
|
* ``socks.SOCKS5`` or ``'socks5'``
|
||||||
|
* ``socks.SOCKS4`` or ``'socks4'``
|
||||||
|
* ``socks.HTTP`` or ``'http'``
|
||||||
|
|
||||||
|
* For Python >= 3.6:
|
||||||
|
* All of the above
|
||||||
|
* ``python_socks.ProxyType.SOCKS5``
|
||||||
|
* ``python_socks.ProxyType.SOCKS4``
|
||||||
|
* ``python_socks.ProxyType.HTTP``
|
||||||
|
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
proxy = {
|
||||||
|
'proxy_type': 'socks5', # (mandatory) protocol to use (see above)
|
||||||
|
'addr': '1.1.1.1', # (mandatory) proxy IP address
|
||||||
|
'port': 5555, # (mandatory) proxy port number
|
||||||
|
'username': 'foo', # (optional) username if the proxy requires auth
|
||||||
|
'password': 'bar', # (optional) password if the proxy requires auth
|
||||||
|
'rdns': True # (optional) whether to use remote or local resolve, default remote
|
||||||
|
}
|
||||||
|
|
||||||
|
For backwards compatibility with ``PySocks`` the following format
|
||||||
|
is possible (but discouraged):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
proxy = (socks.SOCKS5, '1.1.1.1', 5555, True, 'foo', 'bar')
|
||||||
|
|
||||||
|
.. __: https://github.com/romis2012/python-socks#installation
|
||||||
|
.. __: https://github.com/Anorov/PySocks#installation
|
||||||
|
.. __: https://github.com/Anorov/PySocks#usage-1
|
||||||
|
|
||||||
|
|
||||||
|
Using MTProto Proxies
|
||||||
|
=====================
|
||||||
|
|
||||||
|
MTProto Proxies are Telegram's alternative to normal proxies,
|
||||||
|
and work a bit differently. The following protocols are available:
|
||||||
|
|
||||||
|
* ``ConnectionTcpMTProxyAbridged``
|
||||||
|
* ``ConnectionTcpMTProxyIntermediate``
|
||||||
|
* ``ConnectionTcpMTProxyRandomizedIntermediate`` (preferred)
|
||||||
|
|
||||||
|
For now, you need to manually specify these special connection modes
|
||||||
|
if you want to use a MTProto Proxy. Your code would look like this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, connection
|
||||||
|
# we need to change the connection ^^^^^^^^^^
|
||||||
|
|
||||||
|
client = TelegramClient(
|
||||||
|
'anon',
|
||||||
|
api_id,
|
||||||
|
api_hash,
|
||||||
|
|
||||||
|
# Use one of the available connection modes.
|
||||||
|
# Normally, this one works with most proxies.
|
||||||
|
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
|
||||||
|
|
||||||
|
# Then, pass the proxy details as a tuple:
|
||||||
|
# (host name, port, proxy secret)
|
||||||
|
#
|
||||||
|
# If the proxy has no secret, the secret must be:
|
||||||
|
# '00000000000000000000000000000000'
|
||||||
|
proxy=('mtproxy.example.com', 2002, 'secret')
|
||||||
|
)
|
||||||
|
|
||||||
|
In future updates, we may make it easier to use MTProto Proxies
|
||||||
|
(such as avoiding the need to manually pass ``connection=``).
|
||||||
|
|
||||||
|
In short, the same code above but without comments to make it clearer:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, connection
|
||||||
|
|
||||||
|
client = TelegramClient(
|
||||||
|
'anon', api_id, api_hash,
|
||||||
|
connection=connection.ConnectionTcpMTProxyRandomizedIntermediate,
|
||||||
|
proxy=('mtproxy.example.com', 2002, 'secret')
|
||||||
|
)
|
159
readthedocs/basic/updates.rst
Normal file
159
readthedocs/basic/updates.rst
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
=======
|
||||||
|
Updates
|
||||||
|
=======
|
||||||
|
|
||||||
|
Updates are an important topic in a messaging platform like Telegram.
|
||||||
|
After all, you want to be notified when a new message arrives, when
|
||||||
|
a member joins, when someone starts typing, etc.
|
||||||
|
For that, you can use **events**.
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
It is strongly advised to enable logging when working with events,
|
||||||
|
since exceptions in event handlers are hidden by default. Please
|
||||||
|
add the following snippet to the very top of your file:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logging.basicConfig(format='[%(levelname) %(asctime)s] %(name)s: %(message)s',
|
||||||
|
level=logging.WARNING)
|
||||||
|
|
||||||
|
|
||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
Let's start things with an example to automate replies:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
client = TelegramClient('anon', api_id, api_hash)
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
async def my_event_handler(event):
|
||||||
|
if 'hello' in event.raw_text:
|
||||||
|
await event.reply('hi!')
|
||||||
|
|
||||||
|
client.start()
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
This code isn't much, but there might be some things unclear.
|
||||||
|
Let's break it down:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
client = TelegramClient('anon', api_id, api_hash)
|
||||||
|
|
||||||
|
|
||||||
|
This is normal creation (of course, pass session name, API ID and hash).
|
||||||
|
Nothing we don't know already.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
|
||||||
|
|
||||||
|
This Python decorator will attach itself to the ``my_event_handler``
|
||||||
|
definition, and basically means that *on* a `NewMessage
|
||||||
|
<telethon.events.newmessage.NewMessage>` *event*,
|
||||||
|
the callback function you're about to define will be called:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def my_event_handler(event):
|
||||||
|
if 'hello' in event.raw_text:
|
||||||
|
await event.reply('hi!')
|
||||||
|
|
||||||
|
|
||||||
|
If a `NewMessage
|
||||||
|
<telethon.events.newmessage.NewMessage>` event occurs,
|
||||||
|
and ``'hello'`` is in the text of the message, we `reply()
|
||||||
|
<telethon.tl.custom.message.Message.reply>` to the event
|
||||||
|
with a ``'hi!'`` message.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Event handlers **must** be ``async def``. After all,
|
||||||
|
Telethon is an asynchronous library based on `asyncio`,
|
||||||
|
which is a safer and often faster approach to threads.
|
||||||
|
|
||||||
|
You **must** ``await`` all method calls that use
|
||||||
|
network requests, which is most of them.
|
||||||
|
|
||||||
|
|
||||||
|
More Examples
|
||||||
|
=============
|
||||||
|
|
||||||
|
Replying to messages with hello is fun, but, can we do more?
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(outgoing=True, pattern=r'\.save'))
|
||||||
|
async def handler(event):
|
||||||
|
if event.is_reply:
|
||||||
|
replied = await event.get_reply_message()
|
||||||
|
sender = replied.sender
|
||||||
|
await client.download_profile_photo(sender)
|
||||||
|
await event.respond('Saved your photo {}'.format(sender.username))
|
||||||
|
|
||||||
|
We could also get replies. This event filters outgoing messages
|
||||||
|
(only those that we send will trigger the method), then we filter
|
||||||
|
by the regex ``r'\.save'``, which will match messages starting
|
||||||
|
with ``".save"``.
|
||||||
|
|
||||||
|
Inside the method, we check whether the event is replying to another message
|
||||||
|
or not. If it is, we get the reply message and the sender of that message,
|
||||||
|
and download their profile photo.
|
||||||
|
|
||||||
|
Let's delete messages which contain "heck". We don't allow swearing here.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(pattern=r'(?i).*heck'))
|
||||||
|
async def handler(event):
|
||||||
|
await event.delete()
|
||||||
|
|
||||||
|
|
||||||
|
With the ``r'(?i).*heck'`` regex, we match case-insensitive
|
||||||
|
"heck" anywhere in the message. Regex is very powerful and you
|
||||||
|
can learn more at https://regexone.com/.
|
||||||
|
|
||||||
|
So far, we have only seen the `NewMessage
|
||||||
|
<telethon.events.newmessage.NewMessage>`, but there are many more
|
||||||
|
which will be covered later. This is only a small introduction to updates.
|
||||||
|
|
||||||
|
Entities
|
||||||
|
========
|
||||||
|
|
||||||
|
When you need the user or chat where an event occurred, you **must** use
|
||||||
|
the following methods:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
# Good
|
||||||
|
chat = await event.get_chat()
|
||||||
|
sender = await event.get_sender()
|
||||||
|
chat_id = event.chat_id
|
||||||
|
sender_id = event.sender_id
|
||||||
|
|
||||||
|
# BAD. Don't do this
|
||||||
|
chat = event.chat
|
||||||
|
sender = event.sender
|
||||||
|
chat_id = event.chat.id
|
||||||
|
sender_id = event.sender.id
|
||||||
|
|
||||||
|
Events are like messages, but don't have all the information a message has!
|
||||||
|
When you manually get a message, it will have all the information it needs.
|
||||||
|
When you receive an update about a message, it **won't** have all the
|
||||||
|
information, so you have to **use the methods**, not the properties.
|
||||||
|
|
||||||
|
Make sure you understand the code seen here before continuing!
|
||||||
|
As a rule of thumb, remember that new message events behave just
|
||||||
|
like message objects, so you can do with them everything you can
|
||||||
|
do with a message object.
|
368
readthedocs/concepts/asyncio.rst
Normal file
368
readthedocs/concepts/asyncio.rst
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
.. _mastering-asyncio:
|
||||||
|
|
||||||
|
=================
|
||||||
|
Mastering asyncio
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
What's asyncio?
|
||||||
|
===============
|
||||||
|
|
||||||
|
`asyncio` is a Python 3's built-in library. This means it's already installed if
|
||||||
|
you have Python 3. Since Python 3.5, it is convenient to work with asynchronous
|
||||||
|
code. Before (Python 3.4) we didn't have ``async`` or ``await``, but now we do.
|
||||||
|
|
||||||
|
`asyncio` stands for *Asynchronous Input Output*. This is a very powerful
|
||||||
|
concept to use whenever you work IO. Interacting with the web or external
|
||||||
|
APIs such as Telegram's makes a lot of sense this way.
|
||||||
|
|
||||||
|
|
||||||
|
Why asyncio?
|
||||||
|
============
|
||||||
|
|
||||||
|
Asynchronous IO makes a lot of sense in a library like Telethon.
|
||||||
|
You send a request to the server (such as "get some message"), and
|
||||||
|
thanks to `asyncio`, your code won't block while a response arrives.
|
||||||
|
|
||||||
|
The alternative would be to spawn a thread for each update so that
|
||||||
|
other code can run while the response arrives. That is *a lot* more
|
||||||
|
expensive.
|
||||||
|
|
||||||
|
The code will also run faster, because instead of switching back and
|
||||||
|
forth between the OS and your script, your script can handle it all.
|
||||||
|
Avoiding switching saves quite a bit of time, in Python or any other
|
||||||
|
language that supports asynchronous IO. It will also be cheaper,
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 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?
|
||||||
|
===========================
|
||||||
|
|
||||||
|
The moment you import any of these:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import sync, ...
|
||||||
|
# or
|
||||||
|
from telethon.sync import ...
|
||||||
|
# or
|
||||||
|
import telethon.sync
|
||||||
|
|
||||||
|
The ``sync`` module rewrites most ``async def``
|
||||||
|
methods in Telethon to something similar to this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def new_method():
|
||||||
|
result = original_method()
|
||||||
|
if loop.is_running():
|
||||||
|
# the loop is already running, return the await-able to the user
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# the loop is not running yet, so we can run it for the user
|
||||||
|
return loop.run_until_complete(result)
|
||||||
|
|
||||||
|
|
||||||
|
That means you can do this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(client.get_me().username)
|
||||||
|
|
||||||
|
Instead of this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
me = client.loop.run_until_complete(client.get_me())
|
||||||
|
print(me.username)
|
||||||
|
|
||||||
|
# or, using asyncio's default loop (it's the same)
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.get_running_loop() # == client.loop
|
||||||
|
me = loop.run_until_complete(client.get_me())
|
||||||
|
print(me.username)
|
||||||
|
|
||||||
|
|
||||||
|
As you can see, it's a lot of boilerplate and noise having to type
|
||||||
|
``run_until_complete`` all the time, so you can let the magic module
|
||||||
|
to rewrite it for you. But notice the comment above: it won't run
|
||||||
|
the loop if it's already running, because it can't. That means this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# 3. the loop is running here
|
||||||
|
print(
|
||||||
|
client.get_me() # 4. this will return a coroutine!
|
||||||
|
.username # 5. this fails, coroutines don't have usernames
|
||||||
|
)
|
||||||
|
|
||||||
|
loop.run_until_complete( # 2. run the loop and the ``main()`` coroutine
|
||||||
|
main() # 1. calling ``async def`` "returns" a coroutine
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Will fail. So if you're inside an ``async def``, then the loop is
|
||||||
|
running, and if the loop is running, you must ``await`` things yourself:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print((await client.get_me()).username)
|
||||||
|
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
|
||||||
|
|
||||||
|
What are async, await and coroutines?
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
The ``async`` keyword lets you define asynchronous functions,
|
||||||
|
also known as coroutines, and also iterate over asynchronous
|
||||||
|
loops or use ``async with``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# ^ this declares the main() coroutine function
|
||||||
|
|
||||||
|
async with client:
|
||||||
|
# ^ this is an asynchronous with block
|
||||||
|
|
||||||
|
async for message in client.iter_messages(chat):
|
||||||
|
# ^ this is a for loop over an asynchronous generator
|
||||||
|
|
||||||
|
print(message.sender.username)
|
||||||
|
|
||||||
|
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
|
||||||
|
other tasks. Tasks can be thought of as "threads", since many can run
|
||||||
|
concurrently:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def hello(delay):
|
||||||
|
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||||
|
print('hello') # eventually the loop resumes the code here
|
||||||
|
|
||||||
|
async def world(delay):
|
||||||
|
# the loop decides this method should run first
|
||||||
|
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||||
|
print('world') # eventually the loop finishes all tasks
|
||||||
|
|
||||||
|
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:
|
||||||
|
# create a new temporary asyncio loop and use it to run main
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
The same example, but without the comment noise:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def hello(delay):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
print('hello')
|
||||||
|
|
||||||
|
async def world(delay):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
print('world')
|
||||||
|
|
||||||
|
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?
|
||||||
|
==================
|
||||||
|
|
||||||
|
Yes, you can, but you must understand that the loops themselves are
|
||||||
|
not thread safe. and you must be sure to know what is happening. The
|
||||||
|
easiest and cleanest option is to use `asyncio.run` to create and manage
|
||||||
|
the new event loop for you:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
|
||||||
|
async def actual_work():
|
||||||
|
client = TelegramClient(..., loop=loop)
|
||||||
|
... # can use `await` here
|
||||||
|
|
||||||
|
def go():
|
||||||
|
asyncio.run(actual_work())
|
||||||
|
|
||||||
|
threading.Thread(target=go).start()
|
||||||
|
|
||||||
|
|
||||||
|
Generally, **you don't need threads** unless you know what you're doing.
|
||||||
|
Just create another task, as shown above. If you're using the Telethon
|
||||||
|
with a library that uses threads, you must be careful to use `threading.Lock`
|
||||||
|
whenever you use the client, or enable the compatible mode. For that, see
|
||||||
|
:ref:`compatibility-and-convenience`.
|
||||||
|
|
||||||
|
You may have seen this error:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
||||||
|
|
||||||
|
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!
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
All of what `client.run_until_disconnected()
|
||||||
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
|
||||||
|
run the `asyncio`'s event loop until the client is disconnected. That means
|
||||||
|
*the loop is running*. And if the loop is running, it will run all the tasks
|
||||||
|
in it. So if you want to run *other* code, create tasks for it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
async def clock():
|
||||||
|
while True:
|
||||||
|
print('The time:', datetime.now())
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
loop.create_task(clock())
|
||||||
|
...
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
This creates a task for a clock that prints the time every second.
|
||||||
|
You don't need to use `client.run_until_disconnected()
|
||||||
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
|
||||||
|
You just need to make the loop is running, somehow. `loop.run_forever()
|
||||||
|
<asyncio.loop.run_forever()>` and `loop.run_until_complete()
|
||||||
|
<asyncio.loop.run_until_complete>` can also be used to run
|
||||||
|
the loop, and Telethon will be happy with any approach.
|
||||||
|
|
||||||
|
Of course, there are better tools to run code hourly or daily, see below.
|
||||||
|
|
||||||
|
|
||||||
|
What else can asyncio do?
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Asynchronous IO is a really powerful tool, as we've seen. There are plenty
|
||||||
|
of other useful libraries that also use `asyncio` and that you can integrate
|
||||||
|
with Telethon.
|
||||||
|
|
||||||
|
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
|
||||||
|
`requests <https://github.com/requests/requests/>`_ but asynchronous.
|
||||||
|
* `quart <https://gitlab.com/pgjones/quart>`_ is an asynchronous alternative
|
||||||
|
to `Flask <http://flask.pocoo.org/>`_.
|
||||||
|
* `aiocron <https://github.com/gawel/aiocron>`_ lets you schedule things
|
||||||
|
to run things at a desired time, or run some tasks hourly, daily, etc.
|
||||||
|
|
||||||
|
And of course, `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
||||||
|
itself! It has a lot of methods that let you do nice things. For example,
|
||||||
|
you can run requests in parallel:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
last, sent, download_path = await asyncio.gather(
|
||||||
|
client.get_messages('telegram', 10),
|
||||||
|
client.send_message('me', 'Using asyncio!'),
|
||||||
|
client.download_profile_photo('telegram')
|
||||||
|
)
|
||||||
|
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
|
||||||
|
|
||||||
|
This code will get the 10 last messages from `@telegram
|
||||||
|
<https://t.me/telegram>`_, send one to the chat with yourself, and also
|
||||||
|
download the profile photo of the channel. `asyncio` will run all these
|
||||||
|
three tasks at the same time. You can run all the tasks you want this way.
|
||||||
|
|
||||||
|
A different way would be:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
loop.create_task(client.get_messages('telegram', 10))
|
||||||
|
loop.create_task(client.send_message('me', 'Using asyncio!'))
|
||||||
|
loop.create_task(client.download_profile_photo('telegram'))
|
||||||
|
|
||||||
|
They will run in the background as long as the loop is running too.
|
||||||
|
|
||||||
|
You can also `start an asyncio server
|
||||||
|
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.start_server>`_
|
||||||
|
in the main script, and from another script, `connect to it
|
||||||
|
<https://docs.python.org/3/library/asyncio-stream.html#asyncio.open_connection>`_
|
||||||
|
to achieve `Inter-Process Communication
|
||||||
|
<https://en.wikipedia.org/wiki/Inter-process_communication>`_.
|
||||||
|
You can get as creative as you want. You can program anything you want.
|
||||||
|
When you use a library, you're not limited to use only its methods. You can
|
||||||
|
combine all the libraries you want. People seem to forget this simple fact!
|
||||||
|
|
||||||
|
|
||||||
|
Why does client.start() work outside async?
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Because it's so common that it's really convenient to offer said
|
||||||
|
functionality by default. This means you can set up all your event
|
||||||
|
handlers and start the client without worrying about loops at all.
|
||||||
|
|
||||||
|
Using the client in a ``with`` block, `start
|
||||||
|
<telethon.client.auth.AuthMethods.start>`, `run_until_disconnected
|
||||||
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>`, and
|
||||||
|
`disconnect <telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`
|
||||||
|
all support this.
|
||||||
|
|
||||||
|
Where can I read more?
|
||||||
|
======================
|
||||||
|
|
||||||
|
`Check out my blog post
|
||||||
|
<https://lonami.dev/blog/asyncio/>`_ about `asyncio`, which
|
||||||
|
has some more examples and pictures to help you understand what happens
|
||||||
|
when the loop runs.
|
336
readthedocs/concepts/botapi-vs-mtproto.rst
Normal file
336
readthedocs/concepts/botapi-vs-mtproto.rst
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
.. _botapi:
|
||||||
|
|
||||||
|
=======================
|
||||||
|
HTTP Bot API vs MTProto
|
||||||
|
=======================
|
||||||
|
|
||||||
|
|
||||||
|
Telethon is more than just another viable alternative when developing bots
|
||||||
|
for Telegram. If you haven't decided which wrapper library for bots to use
|
||||||
|
yet, using Telethon from the beginning may save you some headaches later.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
What is Bot API?
|
||||||
|
================
|
||||||
|
|
||||||
|
The `Telegram Bot API`_, also known as HTTP Bot API and from now on referred
|
||||||
|
to as simply "Bot API" is Telegram's official way for developers to control
|
||||||
|
their own Telegram bots. Quoting their main page:
|
||||||
|
|
||||||
|
The Bot API is an HTTP-based interface created for developers keen on
|
||||||
|
building bots for Telegram.
|
||||||
|
|
||||||
|
To learn how to create and set up a bot, please consult our
|
||||||
|
`Introduction to Bots`_ and `Bot FAQ`_.
|
||||||
|
|
||||||
|
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?
|
||||||
|
================
|
||||||
|
|
||||||
|
MTProto_ is Telegram's own protocol to communicate with their API when you
|
||||||
|
connect to their servers.
|
||||||
|
|
||||||
|
Telethon is an alternative MTProto-based backend written entirely in Python
|
||||||
|
and much easier to setup and use.
|
||||||
|
|
||||||
|
Both official applications and third-party clients (like your own
|
||||||
|
applications) logged in as either user or bots **can use MTProto** to
|
||||||
|
communicate directly with Telegram's API (which is not the HTTP bot API).
|
||||||
|
|
||||||
|
When we talk about MTProto, we often mean "MTProto-based clients".
|
||||||
|
|
||||||
|
|
||||||
|
Advantages of MTProto over Bot API
|
||||||
|
==================================
|
||||||
|
|
||||||
|
MTProto clients (like Telethon) connect directly to Telegram's servers,
|
||||||
|
which means there is no HTTP connection, no "polling" or "web hooks". This
|
||||||
|
means **less overhead**, since the protocol used between you and the server
|
||||||
|
is much more compact than HTTP requests with responses in wasteful JSON.
|
||||||
|
|
||||||
|
Since there is a direct connection to Telegram's servers, even if their
|
||||||
|
Bot API endpoint is down, you can still have connection to Telegram directly.
|
||||||
|
|
||||||
|
Using a MTProto client, you are also not limited to the public API that
|
||||||
|
they expose, and instead, **you have full control** of what your bot can do.
|
||||||
|
Telethon offers you all the power with often **much easier usage** than any
|
||||||
|
of the available Python Bot API wrappers.
|
||||||
|
|
||||||
|
If your application ever needs user features because bots cannot do certain
|
||||||
|
things, you will be able to easily login as a user and even keep your bot
|
||||||
|
without having to learn a new library.
|
||||||
|
|
||||||
|
If less overhead and full control didn't convince you to use Telethon yet,
|
||||||
|
check out the wiki page `MTProto vs HTTP Bot API`_ with a more exhaustive
|
||||||
|
and up-to-date list of differences.
|
||||||
|
|
||||||
|
|
||||||
|
Migrating from Bot API to Telethon
|
||||||
|
==================================
|
||||||
|
|
||||||
|
It doesn't matter if you wrote your bot with requests_ and you were
|
||||||
|
making API requests manually, or if you used a wrapper library like
|
||||||
|
python-telegram-bot_ or pyTelegramBotAPI_. It's never too late to
|
||||||
|
migrate to Telethon!
|
||||||
|
|
||||||
|
If you were using an asynchronous library like aiohttp_ or a wrapper like
|
||||||
|
aiogram_ or dumbot_, it will be even easier, because Telethon is also an
|
||||||
|
asynchronous library.
|
||||||
|
|
||||||
|
Next, we will see some examples from the most popular libraries.
|
||||||
|
|
||||||
|
|
||||||
|
Migrating from python-telegram-bot
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Let's take their `echobot.py`_ example and shorten it a bit:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
|
||||||
|
|
||||||
|
def start(update, context):
|
||||||
|
"""Send a message when the command /start is issued."""
|
||||||
|
update.message.reply_text('Hi!')
|
||||||
|
|
||||||
|
def echo(update, context):
|
||||||
|
"""Echo the user message."""
|
||||||
|
update.message.reply_text(update.message.text)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Start the bot."""
|
||||||
|
updater = Updater("TOKEN")
|
||||||
|
dp = updater.dispatcher
|
||||||
|
dp.add_handler(CommandHandler("start", start))
|
||||||
|
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))
|
||||||
|
|
||||||
|
updater.start_polling()
|
||||||
|
|
||||||
|
updater.idle()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
After using Telethon:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage(pattern='/start'))
|
||||||
|
async def start(event):
|
||||||
|
"""Send a message when the command /start is issued."""
|
||||||
|
await event.respond('Hi!')
|
||||||
|
raise events.StopPropagation
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage)
|
||||||
|
async def echo(event):
|
||||||
|
"""Echo the user message."""
|
||||||
|
await event.respond(event.text)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Start the bot."""
|
||||||
|
bot.run_until_disconnected()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
|
||||||
|
* The recommended way to do it imports fewer things.
|
||||||
|
* All handlers trigger by default, so we need ``events.StopPropagation``.
|
||||||
|
* Adding handlers, responding and running is a lot less verbose.
|
||||||
|
* Telethon needs ``async def`` and ``await``.
|
||||||
|
* The ``bot`` isn't hidden away by ``Updater`` or ``Dispatcher``.
|
||||||
|
|
||||||
|
|
||||||
|
Migrating from pyTelegramBotAPI
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Let's show another echobot from their README:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import telebot
|
||||||
|
|
||||||
|
bot = telebot.TeleBot("TOKEN")
|
||||||
|
|
||||||
|
@bot.message_handler(commands=['start'])
|
||||||
|
def send_welcome(message):
|
||||||
|
bot.reply_to(message, "Howdy, how are you doing?")
|
||||||
|
|
||||||
|
@bot.message_handler(func=lambda m: True)
|
||||||
|
def echo_all(message):
|
||||||
|
bot.reply_to(message, message.text)
|
||||||
|
|
||||||
|
bot.polling()
|
||||||
|
|
||||||
|
Now we rewrite it to use Telethon:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage(pattern='/start'))
|
||||||
|
async def send_welcome(event):
|
||||||
|
await event.reply('Howdy, how are you doing?')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage)
|
||||||
|
async def echo_all(event):
|
||||||
|
await event.reply(event.text)
|
||||||
|
|
||||||
|
bot.run_until_disconnected()
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
|
||||||
|
* Instead of doing ``bot.reply_to(message)``, we can do ``event.reply``.
|
||||||
|
Note that the ``event`` behaves just like their ``message``.
|
||||||
|
* Telethon also supports ``func=lambda m: True``, but it's not necessary.
|
||||||
|
|
||||||
|
|
||||||
|
Migrating from aiogram
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
From their GitHub:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher, executor, types
|
||||||
|
|
||||||
|
API_TOKEN = 'BOT TOKEN HERE'
|
||||||
|
|
||||||
|
# Initialize bot and dispatcher
|
||||||
|
bot = Bot(token=API_TOKEN)
|
||||||
|
dp = Dispatcher(bot)
|
||||||
|
|
||||||
|
@dp.message_handler(commands=['start'])
|
||||||
|
async def send_welcome(message: types.Message):
|
||||||
|
"""
|
||||||
|
This handler will be called when client send `/start` command.
|
||||||
|
"""
|
||||||
|
await message.reply("Hi!\nI'm EchoBot!\nPowered by aiogram.")
|
||||||
|
|
||||||
|
@dp.message_handler(regexp='(^cat[s]?$|puss)')
|
||||||
|
async def cats(message: types.Message):
|
||||||
|
with open('data/cats.jpg', 'rb') as photo:
|
||||||
|
await bot.send_photo(message.chat.id, photo, caption='Cats is here 😺',
|
||||||
|
reply_to_message_id=message.message_id)
|
||||||
|
|
||||||
|
@dp.message_handler()
|
||||||
|
async def echo(message: types.Message):
|
||||||
|
await bot.send_message(message.chat.id, message.text)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
executor.start_polling(dp, skip_updates=True)
|
||||||
|
|
||||||
|
|
||||||
|
After rewrite:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
# Initialize bot and... just the bot!
|
||||||
|
bot = TelegramClient('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage(pattern='/start'))
|
||||||
|
async def send_welcome(event):
|
||||||
|
await event.reply('Howdy, how are you doing?')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage(pattern='(^cat[s]?$|puss)'))
|
||||||
|
async def cats(event):
|
||||||
|
await event.reply('Cats is here 😺', file='data/cats.jpg')
|
||||||
|
|
||||||
|
@bot.on(events.NewMessage)
|
||||||
|
async def echo_all(event):
|
||||||
|
await event.reply(event.text)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
bot.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
|
||||||
|
* Telethon offers convenience methods to avoid retyping
|
||||||
|
``bot.send_photo(message.chat.id, ...)`` all the time,
|
||||||
|
and instead let you type ``event.reply``.
|
||||||
|
* Sending files is **a lot** easier. The methods for sending
|
||||||
|
photos, documents, audios, etc. are all the same!
|
||||||
|
|
||||||
|
Migrating from dumbot
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Showcasing their subclassing example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from dumbot import Bot
|
||||||
|
|
||||||
|
class Subbot(Bot):
|
||||||
|
async def init(self):
|
||||||
|
self.me = await self.getMe()
|
||||||
|
|
||||||
|
async def on_update(self, update):
|
||||||
|
await self.sendMessage(
|
||||||
|
chat_id=update.message.chat.id,
|
||||||
|
text='i am {}'.format(self.me.username)
|
||||||
|
)
|
||||||
|
|
||||||
|
Subbot(token).run()
|
||||||
|
|
||||||
|
After rewriting:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
class Subbot(TelegramClient):
|
||||||
|
def __init__(self, *a, **kw):
|
||||||
|
super().__init__(*a, **kw)
|
||||||
|
self.add_event_handler(self.on_update, events.NewMessage)
|
||||||
|
|
||||||
|
async def connect():
|
||||||
|
await super().connect()
|
||||||
|
self.me = await self.get_me()
|
||||||
|
|
||||||
|
async def on_update(event):
|
||||||
|
await event.reply('i am {}'.format(self.me.username))
|
||||||
|
|
||||||
|
bot = Subbot('bot', 11111, 'a1b2c3d4').start(bot_token='TOKEN')
|
||||||
|
bot.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
Key differences:
|
||||||
|
|
||||||
|
* Telethon method names are ``snake_case``.
|
||||||
|
* dumbot does not offer friendly methods like ``update.reply``.
|
||||||
|
* Telethon does not have an implicit ``on_update`` handler, so
|
||||||
|
we need to manually register one.
|
||||||
|
|
||||||
|
|
||||||
|
.. _Telegram Bot API: https://core.telegram.org/bots/api
|
||||||
|
.. _Introduction to Bots: https://core.telegram.org/bots
|
||||||
|
.. _Bot FAQ: https://core.telegram.org/bots/faq
|
||||||
|
.. _tdlib: https://core.telegram.org/tdlib
|
||||||
|
.. _MTProto: https://core.telegram.org/mtproto
|
||||||
|
.. _MTProto vs HTTP Bot API: https://github.com/LonamiWebs/Telethon/wiki/MTProto-vs-HTTP-Bot-API
|
||||||
|
.. _requests: https://pypi.org/project/requests/
|
||||||
|
.. _python-telegram-bot: https://python-telegram-bot.readthedocs.io
|
||||||
|
.. _pyTelegramBotAPI: https://github.com/eternnoir/pyTelegramBotAPI
|
||||||
|
.. _aiohttp: https://docs.aiohttp.org/en/stable
|
||||||
|
.. _aiogram: https://aiogram.readthedocs.io
|
||||||
|
.. _dumbot: https://github.com/Lonami/dumbot
|
||||||
|
.. _echobot.py: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py
|
169
readthedocs/concepts/chats-vs-channels.rst
Normal file
169
readthedocs/concepts/chats-vs-channels.rst
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
.. _chats-channels:
|
||||||
|
|
||||||
|
=================
|
||||||
|
Chats vs Channels
|
||||||
|
=================
|
||||||
|
|
||||||
|
Telegram's raw API can get very confusing sometimes, in particular when it
|
||||||
|
comes to talking about "chats", "channels", "groups", "megagroups", and all
|
||||||
|
those concepts.
|
||||||
|
|
||||||
|
This section will try to explain what each of these concepts are.
|
||||||
|
|
||||||
|
|
||||||
|
Chats
|
||||||
|
=====
|
||||||
|
|
||||||
|
A ``Chat`` can be used to talk about either the common "subclass" that both
|
||||||
|
chats and channels share, or the concrete :tl:`Chat` type.
|
||||||
|
|
||||||
|
Technically, both :tl:`Chat` and :tl:`Channel` are a form of the `Chat type`_.
|
||||||
|
|
||||||
|
**Most of the time**, the term :tl:`Chat` is used to talk about *small group
|
||||||
|
chats*. When you create a group through an official application, this is the
|
||||||
|
type that you get. Official applications refer to these as "Group".
|
||||||
|
|
||||||
|
Both the bot API and Telethon will add a minus sign (negate) the real chat ID
|
||||||
|
so that you can tell at a glance, with just a number, the entity type.
|
||||||
|
|
||||||
|
For example, if you create a chat with :tl:`CreateChatRequest`, the real chat
|
||||||
|
ID might be something like `123`. If you try printing it from a
|
||||||
|
`message.chat_id` you will see `-123`. This ID helps Telethon know you're
|
||||||
|
talking about a :tl:`Chat`.
|
||||||
|
|
||||||
|
|
||||||
|
Channels
|
||||||
|
========
|
||||||
|
|
||||||
|
Official applications create a *broadcast* channel when you create a new
|
||||||
|
channel (used to broadcast messages, only administrators can post messages).
|
||||||
|
|
||||||
|
Official applications implicitly *migrate* an *existing* :tl:`Chat` to a
|
||||||
|
*megagroup* :tl:`Channel` when you perform certain actions (exceed user limit,
|
||||||
|
add a public username, set certain permissions, etc.).
|
||||||
|
|
||||||
|
A ``Channel`` can be created directly with :tl:`CreateChannelRequest`, as
|
||||||
|
either a ``megagroup`` or ``broadcast``.
|
||||||
|
|
||||||
|
Official applications use the term "channel" **only** for broadcast channels.
|
||||||
|
|
||||||
|
The API refers to the different types of :tl:`Channel` with certain attributes:
|
||||||
|
|
||||||
|
* A **broadcast channel** is a :tl:`Channel` with the ``channel.broadcast``
|
||||||
|
attribute set to `True`.
|
||||||
|
|
||||||
|
* A **megagroup channel** is a :tl:`Channel` with the ``channel.megagroup``
|
||||||
|
attribute set to `True`. Official applications refer to this as "supergroup".
|
||||||
|
|
||||||
|
* A **gigagroup channel** is a :tl:`Channel` with the ``channel.gigagroup``
|
||||||
|
attribute set to `True`. Official applications refer to this as "broadcast
|
||||||
|
groups", and is used when a megagroup becomes very large and administrators
|
||||||
|
want to transform it into something where only they can post messages.
|
||||||
|
|
||||||
|
|
||||||
|
Both the bot API and Telethon will "concatenate" ``-100`` to the real chat ID
|
||||||
|
so that you can tell at a glance, with just a number, the entity type.
|
||||||
|
|
||||||
|
For example, if you create a new broadcast channel, the real channel ID might
|
||||||
|
be something like `456`. If you try printing it from a `message.chat_id` you
|
||||||
|
will see `-1000000000456`. This ID helps Telethon know you're talking about a
|
||||||
|
:tl:`Channel`.
|
||||||
|
|
||||||
|
|
||||||
|
Converting IDs
|
||||||
|
==============
|
||||||
|
|
||||||
|
You can convert between the "marked" identifiers (prefixed with a minus sign)
|
||||||
|
and the real ones with ``utils.resolve_id``. It will return a tuple with the
|
||||||
|
real ID, and the peer type (the class):
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import utils
|
||||||
|
real_id, peer_type = utils.resolve_id(-1000000000456)
|
||||||
|
|
||||||
|
print(real_id) # 456
|
||||||
|
print(peer_type) # <class 'telethon.tl.types.PeerChannel'>
|
||||||
|
|
||||||
|
peer = peer_type(real_id)
|
||||||
|
print(peer) # PeerChannel(channel_id=456)
|
||||||
|
|
||||||
|
|
||||||
|
The reverse operation can be done with ``utils.get_peer_id``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(utils.get_peer_id(types.PeerChannel(456))) # -1000000000456
|
||||||
|
|
||||||
|
|
||||||
|
Note that this function can also work with other types, like :tl:`Chat` or
|
||||||
|
:tl:`Channel` instances.
|
||||||
|
|
||||||
|
If you need to convert other types like usernames which might need to perform
|
||||||
|
API calls to find out the identifier, you can use ``client.get_peer_id``:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(await client.get_peer_id('me')) # your id
|
||||||
|
|
||||||
|
|
||||||
|
If there is no "mark" (no minus sign), Telethon will assume your identifier
|
||||||
|
refers to a :tl:`User`. If this is **not** the case, you can manually fix it:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import types
|
||||||
|
await client.send_message(types.PeerChannel(456), 'hello')
|
||||||
|
# ^^^^^^^^^^^^^^^^^ explicit peer type
|
||||||
|
|
||||||
|
|
||||||
|
A note on raw API
|
||||||
|
=================
|
||||||
|
|
||||||
|
Certain methods only work on a :tl:`Chat`, and some others only work on a
|
||||||
|
:tl:`Channel` (and these may only work in broadcast, or megagroup). Your code
|
||||||
|
likely knows what it's working with, so it shouldn't be too much of an issue.
|
||||||
|
|
||||||
|
If you need to find the :tl:`Channel` from a :tl:`Chat` that migrated to it,
|
||||||
|
access the `migrated_to` property:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# chat is a Chat
|
||||||
|
channel = await client.get_entity(chat.migrated_to)
|
||||||
|
# channel is now a Channel
|
||||||
|
|
||||||
|
Channels do not have a "migrated_from", but a :tl:`ChannelFull` does. You can
|
||||||
|
use :tl:`GetFullChannelRequest` to obtain this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import functions
|
||||||
|
full = await client(functions.channels.GetFullChannelRequest(your_channel))
|
||||||
|
full_channel = full.full_chat
|
||||||
|
# full_channel is a ChannelFull
|
||||||
|
print(full_channel.migrated_from_chat_id)
|
||||||
|
|
||||||
|
This way, you can also access the linked discussion megagroup of a broadcast channel:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(full_channel.linked_chat_id) # prints ID of linked discussion group or None
|
||||||
|
|
||||||
|
You do not need to use ``client.get_entity`` to access the
|
||||||
|
``migrated_from_chat_id`` :tl:`Chat` or the ``linked_chat_id`` :tl:`Channel`.
|
||||||
|
They are in the ``full.chats`` attribute:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if full_channel.migrated_from_chat_id:
|
||||||
|
migrated_from_chat = next(c for c in full.chats if c.id == full_channel.migrated_from_chat_id)
|
||||||
|
print(migrated_from_chat.title)
|
||||||
|
|
||||||
|
if full_channel.linked_chat_id:
|
||||||
|
linked_group = next(c for c in full.chats if c.id == full_channel.linked_chat_id)
|
||||||
|
print(linked_group.username)
|
||||||
|
|
||||||
|
.. _Chat type: https://tl.telethon.dev/types/chat.html
|
313
readthedocs/concepts/entities.rst
Normal file
313
readthedocs/concepts/entities.rst
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
.. _entities:
|
||||||
|
|
||||||
|
========
|
||||||
|
Entities
|
||||||
|
========
|
||||||
|
|
||||||
|
The library widely uses the concept of "entities". An entity will refer
|
||||||
|
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
|
||||||
|
in response to certain methods, such as :tl:`GetUsersRequest`.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
When something "entity-like" is required, it means that you need to
|
||||||
|
provide something that can be turned into an entity. These things include,
|
||||||
|
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
|
||||||
|
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
|
||||||
|
phone numbers **from people you have in your contact list**.
|
||||||
|
|
||||||
|
To "encounter" an ID, you would have to "find it" like you would in the
|
||||||
|
normal app. If the peer is in your dialogs, you would need to
|
||||||
|
`client.get_dialogs() <telethon.client.dialogs.DialogMethods.get_dialogs>`.
|
||||||
|
If the peer is someone in a group, you would similarly
|
||||||
|
`client.get_participants(group) <telethon.client.chats.ChatMethods.get_participants>`.
|
||||||
|
|
||||||
|
Once you have encountered an ID, the library will (by default) have saved
|
||||||
|
their ``access_hash`` for you, which is needed to invoke most methods.
|
||||||
|
This is why sometimes you might encounter this error when working with
|
||||||
|
the library. You should ``except ValueError`` and run code that you know
|
||||||
|
should work to find the entity.
|
||||||
|
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
What is an Entity?
|
||||||
|
==================
|
||||||
|
|
||||||
|
A lot of methods and requests require *entities* to work. For example,
|
||||||
|
you send a message to an *entity*, get the username of an *entity*, and
|
||||||
|
so on.
|
||||||
|
|
||||||
|
There are a lot of things that work as entities: usernames, phone numbers,
|
||||||
|
chat links, invite links, IDs, and the types themselves. That is, you can
|
||||||
|
use any of those when you see an "entity" is needed.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Remember that the phone number must be in your contact list before you
|
||||||
|
can use it.
|
||||||
|
|
||||||
|
You should use, **from better to worse**:
|
||||||
|
|
||||||
|
1. Input entities. For example, `event.input_chat
|
||||||
|
<telethon.tl.custom.chatgetter.ChatGetter.input_chat>`,
|
||||||
|
`message.input_sender
|
||||||
|
<telethon.tl.custom.sendergetter.SenderGetter.input_sender>`,
|
||||||
|
or caching an entity you will use a lot with
|
||||||
|
``entity = await client.get_input_entity(...)``.
|
||||||
|
|
||||||
|
2. Entities. For example, if you had to get someone's
|
||||||
|
username, you can just use ``user`` or ``channel``.
|
||||||
|
It will work. Only use this option if you already have the entity!
|
||||||
|
|
||||||
|
3. IDs. This will always look the entity up from the
|
||||||
|
cache (the ``*.session`` file caches seen entities).
|
||||||
|
|
||||||
|
4. Usernames, phone numbers and links. The cache will be
|
||||||
|
used too (unless you force a `client.get_entity()
|
||||||
|
<telethon.client.users.UserMethods.get_entity>`),
|
||||||
|
but may make a request if the username, phone or link
|
||||||
|
has not been found yet.
|
||||||
|
|
||||||
|
In recent versions of the library, the following two are equivalent:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
await client.send_message(event.sender_id, 'Hi')
|
||||||
|
await client.send_message(event.input_sender, 'Hi')
|
||||||
|
|
||||||
|
|
||||||
|
If you need to be 99% sure that the code will work (sometimes it's
|
||||||
|
simply impossible for the library to find the input entity), or if
|
||||||
|
you will reuse the chat a lot, consider using the following instead:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
# This method may make a network request to find the input sender.
|
||||||
|
# Properties can't make network requests, so we need a method.
|
||||||
|
sender = await event.get_input_sender()
|
||||||
|
await client.send_message(sender, 'Hi')
|
||||||
|
await client.send_message(sender, 'Hi')
|
||||||
|
|
||||||
|
|
||||||
|
Getting Entities
|
||||||
|
================
|
||||||
|
|
||||||
|
Through the use of the :ref:`sessions`, the library will automatically
|
||||||
|
remember the ID and hash pair, along with some extra information, so
|
||||||
|
you're able to just do this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# (These examples assume you are inside an "async def")
|
||||||
|
#
|
||||||
|
# Dialogs are the "conversations you have open".
|
||||||
|
# This method returns a list of Dialog, which
|
||||||
|
# has the .entity attribute and other information.
|
||||||
|
#
|
||||||
|
# This part is IMPORTANT, because it fills the entity cache.
|
||||||
|
dialogs = await client.get_dialogs()
|
||||||
|
|
||||||
|
# All of these work and do the same.
|
||||||
|
username = await client.get_entity('username')
|
||||||
|
username = await client.get_entity('t.me/username')
|
||||||
|
username = await client.get_entity('https://telegram.dog/username')
|
||||||
|
|
||||||
|
# Other kind of entities.
|
||||||
|
channel = await client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
|
||||||
|
contact = await client.get_entity('+34xxxxxxxxx')
|
||||||
|
friend = await client.get_entity(friend_id)
|
||||||
|
|
||||||
|
# Getting entities through their ID (User, Chat or Channel)
|
||||||
|
entity = await client.get_entity(some_id)
|
||||||
|
|
||||||
|
# You can be more explicit about the type for said ID by wrapping
|
||||||
|
# it inside a Peer instance. This is recommended but not necessary.
|
||||||
|
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
|
||||||
|
|
||||||
|
my_user = await client.get_entity(PeerUser(some_id))
|
||||||
|
my_chat = await client.get_entity(PeerChat(some_id))
|
||||||
|
my_channel = await client.get_entity(PeerChannel(some_id))
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You **don't** need to get the entity before using it! Just let the
|
||||||
|
library do its job. Use a phone from your contacts, username, ID or
|
||||||
|
input entity (preferred but not necessary), whatever you already have.
|
||||||
|
|
||||||
|
All methods in the :ref:`telethon-client` call `.get_input_entity()
|
||||||
|
<telethon.client.users.UserMethods.get_input_entity>` prior
|
||||||
|
to sending the request to save you from the hassle of doing so manually.
|
||||||
|
That way, convenience calls such as `client.send_message('username', 'hi!')
|
||||||
|
<telethon.client.messages.MessageMethods.send_message>`
|
||||||
|
become possible.
|
||||||
|
|
||||||
|
Every entity the library encounters (in any response to any call) will by
|
||||||
|
default be cached in the ``.session`` file (an SQLite database), to avoid
|
||||||
|
performing unnecessary API calls. If the entity cannot be found, additonal
|
||||||
|
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
|
||||||
|
made to obtain the required information.
|
||||||
|
|
||||||
|
|
||||||
|
Entities vs. Input Entities
|
||||||
|
===========================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This section is informative, but worth reading. The library
|
||||||
|
will transparently handle all of these details for you.
|
||||||
|
|
||||||
|
On top of the normal types, the API also make use of what they call their
|
||||||
|
``Input*`` versions of objects. The input version of an entity (e.g.
|
||||||
|
:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum
|
||||||
|
information that's required from Telegram to be able to identify
|
||||||
|
who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They
|
||||||
|
are named like this because they are input parameters in the requests.
|
||||||
|
|
||||||
|
Entities' ID are the same for all user and bot accounts, however, the access
|
||||||
|
hash is **different for each account**, so trying to reuse the access hash
|
||||||
|
from one account in another will **not** work.
|
||||||
|
|
||||||
|
Sometimes, Telegram only needs to indicate the type of the entity along
|
||||||
|
with their ID. For this purpose, :tl:`Peer` versions of the entities also
|
||||||
|
exist, which just have the ID. You cannot get the hash out of them since
|
||||||
|
you should not be needing it. The library probably has cached it before.
|
||||||
|
|
||||||
|
Peers are enough to identify an entity, but they are not enough to make
|
||||||
|
a request with them. You need to know their hash before you can
|
||||||
|
"use them", and to know the hash you need to "encounter" them, let it
|
||||||
|
be in your dialogs, participants, message forwards, etc.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
You *can* use peers with the library. Behind the scenes, they are
|
||||||
|
replaced with the input variant. Peers "aren't enough" on their own
|
||||||
|
but the library will do some more work to use the right type.
|
||||||
|
|
||||||
|
As we just mentioned, API calls don't need to know the whole information
|
||||||
|
about the entities, only their ID and hash. For this reason, another method,
|
||||||
|
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
||||||
|
is available. This will always use the cache while possible, making zero API
|
||||||
|
calls most of the time. When a request is made, if you provided the full
|
||||||
|
entity, e.g. an :tl:`User`, the library will convert it to the required
|
||||||
|
:tl:`InputPeer` automatically for you.
|
||||||
|
|
||||||
|
**You should always favour**
|
||||||
|
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
||||||
|
**over**
|
||||||
|
`client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
||||||
|
for this reason! Calling the latter will always make an API call to get
|
||||||
|
the most recent information about said entity, but invoking requests don't
|
||||||
|
need this information, just the :tl:`InputPeer`. Only use
|
||||||
|
`client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
||||||
|
if you need to get actual information, like the username, name, title, etc.
|
||||||
|
of the entity.
|
||||||
|
|
||||||
|
To further simplify the workflow, since the version ``0.16.2`` of the
|
||||||
|
library, the raw requests you make to the API are also able to call
|
||||||
|
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
||||||
|
wherever needed, so you can even do things like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client(SendMessageRequest('username', 'hello'))
|
||||||
|
|
||||||
|
The library will call the ``.resolve()`` method of the request, which will
|
||||||
|
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if
|
||||||
|
you don't get this yet, but remember some of the details here are important.
|
||||||
|
|
||||||
|
|
||||||
|
Full Entities
|
||||||
|
=============
|
||||||
|
|
||||||
|
In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its
|
||||||
|
variants for chats and channels), there is also the concept of :tl:`UserFull`.
|
||||||
|
|
||||||
|
This full variant has additional information such as whether the user is
|
||||||
|
blocked, its notification settings, the bio or about of the user, etc.
|
||||||
|
|
||||||
|
There is also :tl:`messages.ChatFull` which is the equivalent of full entities
|
||||||
|
for chats and channels, with also the about section of the channel. Note that
|
||||||
|
the ``users`` field only contains bots for the channel (so that clients can
|
||||||
|
suggest commands to use).
|
||||||
|
|
||||||
|
You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat`
|
||||||
|
and :tl:`GetFullChannel` respectively.
|
||||||
|
|
||||||
|
|
||||||
|
Accessing Entities
|
||||||
|
==================
|
||||||
|
|
||||||
|
Although it's explicitly noted in the documentation that messages
|
||||||
|
*subclass* `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
||||||
|
and `SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`,
|
||||||
|
some people still don't get inheritance.
|
||||||
|
|
||||||
|
When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`"
|
||||||
|
it means that the class you're looking at, *also* can act as the class it
|
||||||
|
bases. In this case, `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
||||||
|
knows how to get the *chat* where a thing belongs to.
|
||||||
|
|
||||||
|
So, a `Message <telethon.tl.custom.message.Message>` is a
|
||||||
|
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||||
|
That means you can do this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
message.is_private
|
||||||
|
message.chat_id
|
||||||
|
await message.get_chat()
|
||||||
|
# ...etc
|
||||||
|
|
||||||
|
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is similar:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
message.user_id
|
||||||
|
await message.get_input_sender()
|
||||||
|
message.user
|
||||||
|
# ...etc
|
||||||
|
|
||||||
|
Quite a few things implement them, so it makes sense to reuse the code.
|
||||||
|
For example, all events (except raw updates) implement `ChatGetter
|
||||||
|
<telethon.tl.custom.chatgetter.ChatGetter>` since all events occur
|
||||||
|
in some chat.
|
||||||
|
|
||||||
|
|
||||||
|
Summary
|
||||||
|
=======
|
||||||
|
|
||||||
|
TL;DR; If you're here because of *"Could not find the input entity for"*,
|
||||||
|
you must ask yourself "how did I find this entity through official
|
||||||
|
applications"? Now do the same with the library. Use what applies:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# (These examples assume you are inside an "async def")
|
||||||
|
async with client:
|
||||||
|
# Does it have a username? Use it!
|
||||||
|
entity = await client.get_entity(username)
|
||||||
|
|
||||||
|
# Do you have a conversation open with them? Get dialogs.
|
||||||
|
await client.get_dialogs()
|
||||||
|
|
||||||
|
# Are they participant of some group? Get them.
|
||||||
|
await client.get_participants('username')
|
||||||
|
|
||||||
|
# Is the entity the original sender of a forwarded message? Get it.
|
||||||
|
await client.get_messages('username', 100)
|
||||||
|
|
||||||
|
# NOW you can use the ID, anywhere!
|
||||||
|
await client.send_message(123456, 'Hi!')
|
||||||
|
|
||||||
|
entity = await client.get_entity(123456)
|
||||||
|
print(entity)
|
||||||
|
|
||||||
|
Once the library has "seen" the entity, you can use their **integer** ID.
|
||||||
|
You can't use entities from IDs the library hasn't seen. You must make the
|
||||||
|
library see them *at least once* and disconnect properly. You know where
|
||||||
|
the entities are and you must tell the library. It won't guess for you.
|
155
readthedocs/concepts/errors.rst
Normal file
155
readthedocs/concepts/errors.rst
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
.. _rpc-errors:
|
||||||
|
|
||||||
|
==========
|
||||||
|
RPC Errors
|
||||||
|
==========
|
||||||
|
|
||||||
|
RPC stands for Remote Procedure Call, and when the library raises
|
||||||
|
a ``RPCError``, it's because you have invoked some of the API
|
||||||
|
methods incorrectly (wrong parameters, wrong permissions, or even
|
||||||
|
something went wrong on Telegram's server).
|
||||||
|
|
||||||
|
You should import the errors from ``telethon.errors`` like so:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import errors
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with client.takeout() as takeout:
|
||||||
|
...
|
||||||
|
|
||||||
|
except errors.TakeoutInitDelayError as e:
|
||||||
|
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ here we except TAKEOUT_INIT_DELAY
|
||||||
|
print('Must wait', e.seconds, 'before takeout')
|
||||||
|
|
||||||
|
|
||||||
|
There isn't any official list of all possible RPC errors, so the
|
||||||
|
`list of known errors`_ is provided on a best-effort basis. When new methods
|
||||||
|
are available, the list may be lacking since we simply don't know what errors
|
||||||
|
can raise from them.
|
||||||
|
|
||||||
|
Once we do find out about a new error and what causes it, the list is
|
||||||
|
updated, so if you see an error without a specific class, do report it
|
||||||
|
(and what method caused it)!.
|
||||||
|
|
||||||
|
This list is used to generate documentation for the `raw API page`_.
|
||||||
|
For example, if we want to know what errors can occur from
|
||||||
|
`messages.sendMessage`_ we can simply navigate to its raw API page
|
||||||
|
and find it has 24 known RPC errors at the time of writing.
|
||||||
|
|
||||||
|
|
||||||
|
Base Errors
|
||||||
|
===========
|
||||||
|
|
||||||
|
All the "base" errors are listed in :ref:`telethon-errors`.
|
||||||
|
Any other more specific error will be a subclass of these.
|
||||||
|
|
||||||
|
If the library isn't aware of a specific error just yet, it will instead
|
||||||
|
raise one of these superclasses. This means you may find stuff like this:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
telethon.errors.rpcbaseerrors.BadRequestError: RPCError 400: MESSAGE_POLL_CLOSED (caused by SendVoteRequest)
|
||||||
|
|
||||||
|
If you do, make sure to open an issue or send a pull request to update the
|
||||||
|
`list of known errors`_.
|
||||||
|
|
||||||
|
|
||||||
|
Common Errors
|
||||||
|
=============
|
||||||
|
|
||||||
|
These are some of the errors you may normally need to deal with:
|
||||||
|
|
||||||
|
- ``FloodWaitError`` (420), the same request was repeated many times.
|
||||||
|
Must wait ``.seconds`` (you can access this attribute). For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
...
|
||||||
|
from telethon import errors
|
||||||
|
|
||||||
|
try:
|
||||||
|
messages = await client.get_messages(chat)
|
||||||
|
print(messages[0].text)
|
||||||
|
except errors.FloodWaitError as e:
|
||||||
|
print('Have to sleep', e.seconds, 'seconds')
|
||||||
|
time.sleep(e.seconds)
|
||||||
|
|
||||||
|
- ``SessionPasswordNeededError``, if you have setup two-steps
|
||||||
|
verification on Telegram and are trying to sign in.
|
||||||
|
- ``FilePartMissingError``, if you have tried to upload an empty file.
|
||||||
|
- ``ChatAdminRequiredError``, you don't have permissions to perform
|
||||||
|
said operation on a chat or channel. Try avoiding filters, i.e. when
|
||||||
|
searching messages.
|
||||||
|
|
||||||
|
The generic classes for different error codes are:
|
||||||
|
|
||||||
|
- ``InvalidDCError`` (303), the request must be repeated on another DC.
|
||||||
|
- ``BadRequestError`` (400), the request contained errors.
|
||||||
|
- ``UnauthorizedError`` (401), the user is not authorized yet.
|
||||||
|
- ``ForbiddenError`` (403), privacy violation error.
|
||||||
|
- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's!
|
||||||
|
|
||||||
|
If the error is not recognised, it will only be an ``RPCError``.
|
||||||
|
|
||||||
|
You can refer to all errors from Python through the ``telethon.errors``
|
||||||
|
module. If you don't know what attributes they have, try printing their
|
||||||
|
dir (like ``print(dir(e))``).
|
||||||
|
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
==========
|
||||||
|
|
||||||
|
Some of the errors carry additional data in them. When they look like
|
||||||
|
``EMAIL_UNCONFIRMED_X``, the ``_X`` value will be accessible from the
|
||||||
|
error instance. The current list of errors that do this is the following:
|
||||||
|
|
||||||
|
- ``EmailUnconfirmedError`` has ``.code_length``.
|
||||||
|
- ``FileMigrateError`` has ``.new_dc``.
|
||||||
|
- ``FilePartMissingError`` has ``.which``.
|
||||||
|
- ``FloodTestPhoneWaitError`` has ``.seconds``.
|
||||||
|
- ``FloodWaitError`` has ``.seconds``.
|
||||||
|
- ``InterdcCallErrorError`` has ``.dc``.
|
||||||
|
- ``InterdcCallRichErrorError`` has ``.dc``.
|
||||||
|
- ``NetworkMigrateError`` has ``.new_dc``.
|
||||||
|
- ``PhoneMigrateError`` has ``.new_dc``.
|
||||||
|
- ``SlowModeWaitError`` has ``.seconds``.
|
||||||
|
- ``TakeoutInitDelayError`` has ``.seconds``.
|
||||||
|
- ``UserMigrateError`` has ``.new_dc``.
|
||||||
|
|
||||||
|
|
||||||
|
Avoiding Limits
|
||||||
|
===============
|
||||||
|
|
||||||
|
Don't spam. You won't get ``FloodWaitError`` or your account banned or
|
||||||
|
deleted if you use the library *for legit use cases*. Make cool tools.
|
||||||
|
Don't spam! Nobody knows the exact limits for all requests since they
|
||||||
|
depend on a lot of factors, so don't bother asking.
|
||||||
|
|
||||||
|
Still, if you do have a legit use case and still get those errors, the
|
||||||
|
library will automatically sleep when they are smaller than 60 seconds
|
||||||
|
by default. You can set different "auto-sleep" thresholds:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
client.flood_sleep_threshold = 0 # Don't auto-sleep
|
||||||
|
client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always
|
||||||
|
|
||||||
|
You can also except it and act as you prefer:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.errors import FloodWaitError
|
||||||
|
try:
|
||||||
|
...
|
||||||
|
except FloodWaitError as e:
|
||||||
|
print('Flood waited for', e.seconds)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
VoIP numbers are very limited, and some countries are more limited too.
|
||||||
|
|
||||||
|
|
||||||
|
.. _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
|
420
readthedocs/concepts/full-api.rst
Normal file
420
readthedocs/concepts/full-api.rst
Normal file
|
@ -0,0 +1,420 @@
|
||||||
|
.. _full-api:
|
||||||
|
|
||||||
|
============
|
||||||
|
The Full API
|
||||||
|
============
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
While you have access to this, you should always use the friendly
|
||||||
|
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 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::
|
||||||
|
|
||||||
|
The reason to keep both https://tl.telethon.dev and this
|
||||||
|
documentation alive is that the former allows instant search results
|
||||||
|
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).
|
||||||
|
|
||||||
|
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,
|
||||||
|
we could `use the search`_ to look for "message". There we would find
|
||||||
|
:tl:`SendMessageRequest`, which we can work with.
|
||||||
|
|
||||||
|
Every request is a Python class, and has the parameters needed for you
|
||||||
|
to invoke it. You can also call ``help(request)`` for information on
|
||||||
|
what input parameters it takes. Remember to "Copy import to the
|
||||||
|
clipboard", or your script won't be aware of this class! Now we have:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.tl.functions.messages import SendMessageRequest
|
||||||
|
|
||||||
|
If you're going to use a lot of these, you may do:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.tl import types, functions
|
||||||
|
# We now have access to 'functions.messages.SendMessageRequest'
|
||||||
|
|
||||||
|
We see that this request must take at least two parameters, a ``peer``
|
||||||
|
of type :tl:`InputPeer`, and a ``message`` which is just a Python
|
||||||
|
`str`\ ing.
|
||||||
|
|
||||||
|
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
|
||||||
|
construct one, for instance:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.tl.types import InputPeerUser
|
||||||
|
|
||||||
|
peer = InputPeerUser(user_id, user_hash)
|
||||||
|
|
||||||
|
Or we call `client.get_input_entity()
|
||||||
|
<telethon.client.users.UserMethods.get_input_entity>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import telethon
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
peer = await client.get_input_entity('someone')
|
||||||
|
|
||||||
|
client.loop.run_until_complete(main())
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Remember that ``await`` must occur inside an ``async def``.
|
||||||
|
Every full API example assumes you already know and do this.
|
||||||
|
|
||||||
|
|
||||||
|
When you're going to invoke an API method, most require you to pass an
|
||||||
|
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
|
||||||
|
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
||||||
|
is more straightforward (and often immediate, if you've seen the user before,
|
||||||
|
know their ID, etc.). If you also **need** to have information about the whole
|
||||||
|
user, use `client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
||||||
|
instead:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
entity = await client.get_entity('someone')
|
||||||
|
|
||||||
|
In the later case, when you use the entity, the library will cast it to
|
||||||
|
its "input" version for you. If you already have the complete user and
|
||||||
|
want to cache its input version so the library doesn't have to do this
|
||||||
|
every time its used, simply call `telethon.utils.get_input_peer`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import utils
|
||||||
|
peer = utils.get_input_peer(entity)
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Since ``v0.16.2`` this is further simplified. The ``Request`` itself
|
||||||
|
will call `client.get_input_entity
|
||||||
|
<telethon.client.users.UserMethods.get_input_entity>` for you when
|
||||||
|
required, but it's good to remember what's happening.
|
||||||
|
|
||||||
|
After this small parenthesis about `client.get_entity
|
||||||
|
<telethon.client.users.UserMethods.get_entity>` versus
|
||||||
|
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`,
|
||||||
|
we have everything we need. To invoke our
|
||||||
|
request we do:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
result = await client(SendMessageRequest(peer, 'Hello there!'))
|
||||||
|
|
||||||
|
Message sent! Of course, this is only an example. There are over 250
|
||||||
|
methods available as of layer 80, and you can use every single of them
|
||||||
|
as you wish. Remember to use the right types! To sum up:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
result = await client(SendMessageRequest(
|
||||||
|
await client.get_input_entity('username'), 'Hello there!'
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
This can further be simplified to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
result = await client(SendMessageRequest('username', 'Hello there!'))
|
||||||
|
# Or even
|
||||||
|
result = await client(SendMessageRequest(PeerChannel(id), 'Hello there!'))
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Note that some requests have a "hash" parameter. This is **not**
|
||||||
|
your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
|
||||||
|
|
||||||
|
It's a special hash used by Telegram to only send a difference of new data
|
||||||
|
that you don't already have with that request, so you can leave it to 0,
|
||||||
|
and it should work (which means no hash is known yet).
|
||||||
|
|
||||||
|
For those requests having a "limit" parameter, you can often set it to
|
||||||
|
zero to signify "return default amount". This won't work for all of them
|
||||||
|
though, for instance, in "messages.search" it will actually return 0 items.
|
||||||
|
|
||||||
|
|
||||||
|
Requests in Parallel
|
||||||
|
====================
|
||||||
|
|
||||||
|
The library will automatically merge outgoing requests into a single
|
||||||
|
*container*. Telegram's API supports sending multiple requests in a
|
||||||
|
single container, which is faster because it has less overhead and
|
||||||
|
the server can run them without waiting for others. You can also
|
||||||
|
force using a container manually:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
|
||||||
|
# Letting the library do it behind the scenes
|
||||||
|
await asyncio.wait([
|
||||||
|
client.send_message('me', 'Hello'),
|
||||||
|
client.send_message('me', ','),
|
||||||
|
client.send_message('me', 'World'),
|
||||||
|
client.send_message('me', '.')
|
||||||
|
])
|
||||||
|
|
||||||
|
# Manually invoking many requests at once
|
||||||
|
await client([
|
||||||
|
SendMessageRequest('me', 'Hello'),
|
||||||
|
SendMessageRequest('me', ', '),
|
||||||
|
SendMessageRequest('me', 'World'),
|
||||||
|
SendMessageRequest('me', '.')
|
||||||
|
])
|
||||||
|
|
||||||
|
Note that you cannot guarantee the order in which they are run.
|
||||||
|
Try running the above code more than one time. You will see the
|
||||||
|
order in which the messages arrive is different.
|
||||||
|
|
||||||
|
If you use the raw API (the first option), you can use ``ordered``
|
||||||
|
to tell the server that it should run the requests sequentially.
|
||||||
|
This will still be faster than going one by one, since the server
|
||||||
|
knows all requests directly:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client([
|
||||||
|
SendMessageRequest('me', 'Hello'),
|
||||||
|
SendMessageRequest('me', ', '),
|
||||||
|
SendMessageRequest('me', 'World'),
|
||||||
|
SendMessageRequest('me', '.')
|
||||||
|
], ordered=True)
|
||||||
|
|
||||||
|
If any of the requests fails with a Telegram error (not connection
|
||||||
|
errors or any other unexpected events), the library will raise
|
||||||
|
`telethon.errors.common.MultiError`. You can ``except`` this
|
||||||
|
and still access the successful results:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.errors import MultiError
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client([
|
||||||
|
SendMessageRequest('me', 'Hello'),
|
||||||
|
SendMessageRequest('me', ''),
|
||||||
|
SendMessageRequest('me', 'World')
|
||||||
|
], ordered=True)
|
||||||
|
except MultiError as e:
|
||||||
|
# The first and third requests worked.
|
||||||
|
first = e.results[0]
|
||||||
|
third = e.results[2]
|
||||||
|
# The second request failed.
|
||||||
|
second = e.exceptions[1]
|
||||||
|
|
||||||
|
.. _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
|
|
@ -4,6 +4,14 @@
|
||||||
Session Files
|
Session Files
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
They are an important part for the library to be efficient, such as caching
|
||||||
|
and handling your authorization key (or you would have to login every time!).
|
||||||
|
|
||||||
|
What are Sessions?
|
||||||
|
==================
|
||||||
|
|
||||||
The first parameter you pass to the constructor of the
|
The first parameter you pass to the constructor of the
|
||||||
:ref:`TelegramClient <telethon-client>` is
|
:ref:`TelegramClient <telethon-client>` is
|
||||||
the ``session``, and defaults to be the session name (or full path). That is,
|
the ``session``, and defaults to be the session name (or full path). That is,
|
||||||
|
@ -38,10 +46,14 @@ by setting ``client.session.save_entities = False``.
|
||||||
|
|
||||||
|
|
||||||
Different Session Storage
|
Different Session Storage
|
||||||
*************************
|
=========================
|
||||||
|
|
||||||
If you don't want to use the default SQLite session storage, you can also use
|
If you don't want to use the default SQLite session storage, you can also
|
||||||
one of the other implementations or implement your own storage.
|
use one of the other implementations or implement your own storage.
|
||||||
|
|
||||||
|
While it's often not the case, it's possible that SQLite is slow enough to
|
||||||
|
be noticeable, in which case you can also use a different storage. Note that
|
||||||
|
this is rare and most people won't have this issue, but it's worth a mention.
|
||||||
|
|
||||||
To use a custom session storage, simply pass the custom session instance to
|
To use a custom session storage, simply pass the custom session instance to
|
||||||
:ref:`TelegramClient <telethon-client>` instead of
|
:ref:`TelegramClient <telethon-client>` instead of
|
||||||
|
@ -49,13 +61,15 @@ the session name.
|
||||||
|
|
||||||
Telethon contains three implementations of the abstract ``Session`` class:
|
Telethon contains three implementations of the abstract ``Session`` class:
|
||||||
|
|
||||||
* ``MemorySession``: stores session data within memory.
|
.. currentmodule:: telethon.sessions
|
||||||
* ``SQLiteSession``: stores sessions within on-disk SQLite databases. Default.
|
|
||||||
* ``StringSession``: stores session data within memory,
|
* `MemorySession <memory.MemorySession>`: stores session data within memory.
|
||||||
|
* `SQLiteSession <sqlite.SQLiteSession>`: stores sessions within on-disk SQLite databases. Default.
|
||||||
|
* `StringSession <string.StringSession>`: stores session data within memory,
|
||||||
but can be saved as a string.
|
but can be saved as a string.
|
||||||
|
|
||||||
You can import these ``from telethon.sessions``. For example, using the
|
You can import these ``from telethon.sessions``. For example, using the
|
||||||
``StringSession`` is done as follows:
|
`StringSession <string.StringSession>` is done as follows:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
|
@ -86,25 +100,30 @@ There are other community-maintained implementations available:
|
||||||
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_:
|
* `Redis <https://github.com/ezdev128/telethon-session-redis>`_:
|
||||||
stores all sessions in a single Redis data store.
|
stores all sessions in a single Redis data store.
|
||||||
|
|
||||||
|
* `MongoDB <https://github.com/watzon/telethon-session-mongo>`_:
|
||||||
|
stores the current session in a MongoDB database.
|
||||||
|
|
||||||
|
|
||||||
Creating your Own Storage
|
Creating your Own Storage
|
||||||
*************************
|
=========================
|
||||||
|
|
||||||
The easiest way to create your own storage implementation is to use
|
The easiest way to create your own storage implementation is to use
|
||||||
``MemorySession`` as the base and check out how ``SQLiteSession`` or
|
`MemorySession <memory.MemorySession>` as the base and check out how
|
||||||
one of the community-maintained implementations work. You can find the
|
`SQLiteSession <sqlite.SQLiteSession>` or one of the community-maintained
|
||||||
relevant Python files under the ``sessions`` directory in Telethon.
|
implementations work. You can find the relevant Python files under the
|
||||||
|
``sessions/`` directory in the Telethon's repository.
|
||||||
|
|
||||||
After you have made your own implementation, you can add it to the
|
After you have made your own implementation, you can add it to the
|
||||||
community-maintained session implementation list above with a pull request.
|
community-maintained session implementation list above with a pull request.
|
||||||
|
|
||||||
|
|
||||||
String Sessions
|
String Sessions
|
||||||
***************
|
===============
|
||||||
|
|
||||||
``StringSession`` are a convenient way to embed your login credentials
|
`StringSession <string.StringSession>` are a convenient way to embed your
|
||||||
directly into your code for extremely easy portability, since all they
|
login credentials directly into your code for extremely easy portability,
|
||||||
take is a string to be able to login without asking for your phone and
|
since all they take is a string to be able to login without asking for your
|
||||||
code (or faster start if you're using a bot token).
|
phone and code (or faster start if you're using a bot token).
|
||||||
|
|
||||||
The easiest way to generate a string session is as follows:
|
The easiest way to generate a string session is as follows:
|
||||||
|
|
||||||
|
@ -124,7 +143,7 @@ output (likely your terminal).
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
**Keep this string safe!** Anyone with this string can use it
|
**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,
|
This is similar to leaking your ``*.session`` files online,
|
||||||
but it is easier to leak a string than it is to leak a file.
|
but it is easier to leak a string than it is to leak a file.
|
||||||
|
@ -138,7 +157,7 @@ you can save it in a variable directly:
|
||||||
|
|
||||||
string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...'
|
string = '1aaNk8EX-YRfwoRsebUkugFvht6DUPi_Q25UOCzOAqzc...'
|
||||||
with TelegramClient(StringSession(string), api_id, api_hash) as client:
|
with TelegramClient(StringSession(string), api_id, api_hash) as client:
|
||||||
client.send_message('me', 'Hi')
|
client.loop.run_until_complete(client.send_message('me', 'Hi'))
|
||||||
|
|
||||||
|
|
||||||
These strings are really convenient for using in places like Heroku since
|
These strings are really convenient for using in places like Heroku since
|
88
readthedocs/concepts/strings.rst
Normal file
88
readthedocs/concepts/strings.rst
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
======================
|
||||||
|
String-based Debugging
|
||||||
|
======================
|
||||||
|
|
||||||
|
Debugging is *really* important. Telegram's API is really big and there
|
||||||
|
are a lot of things that you should know. Such as, what attributes or fields
|
||||||
|
does a result have? Well, the easiest thing to do is printing it:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
entity = await client.get_entity('username')
|
||||||
|
print(entity)
|
||||||
|
|
||||||
|
That will show a huge **string** similar to the following:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
Channel(id=1066197625, title='Telegram Usernames', photo=ChatPhotoEmpty(), date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc), version=0, creator=False, left=True, broadcast=True, verified=True, megagroup=False, restricted=False, signatures=False, min=False, scam=False, has_link=False, has_geo=False, slowmode_enabled=False, access_hash=-6309373984955162244, username='username', restriction_reason=[], admin_rights=None, banned_rights=None, default_banned_rights=None, participants_count=None)
|
||||||
|
|
||||||
|
That's a lot of text. But as you can see, all the properties are there.
|
||||||
|
So if you want the title you **don't use regex** or anything like
|
||||||
|
splitting ``str(entity)`` to get what you want. You just access the
|
||||||
|
attribute you need:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
title = entity.title
|
||||||
|
|
||||||
|
Can we get better than the shown string, though? Yes!
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
print(entity.stringify())
|
||||||
|
|
||||||
|
Will show a much better representation:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
Channel(
|
||||||
|
id=1066197625,
|
||||||
|
title='Telegram Usernames',
|
||||||
|
photo=ChatPhotoEmpty(
|
||||||
|
),
|
||||||
|
date=datetime.datetime(2016, 12, 16, 15, 15, 43, tzinfo=datetime.timezone.utc),
|
||||||
|
version=0,
|
||||||
|
creator=False,
|
||||||
|
left=True,
|
||||||
|
broadcast=True,
|
||||||
|
verified=True,
|
||||||
|
megagroup=False,
|
||||||
|
restricted=False,
|
||||||
|
signatures=False,
|
||||||
|
min=False,
|
||||||
|
scam=False,
|
||||||
|
has_link=False,
|
||||||
|
has_geo=False,
|
||||||
|
slowmode_enabled=False,
|
||||||
|
access_hash=-6309373984955162244,
|
||||||
|
username='username',
|
||||||
|
restriction_reason=[
|
||||||
|
],
|
||||||
|
admin_rights=None,
|
||||||
|
banned_rights=None,
|
||||||
|
default_banned_rights=None,
|
||||||
|
participants_count=None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Now it's easy to see how we could get, for example,
|
||||||
|
the ``year`` value. It's inside ``date``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
channel_year = entity.date.year
|
||||||
|
|
||||||
|
You don't need to print everything to see what all the possible values
|
||||||
|
can be. You can just search in http://tl.telethon.dev/.
|
||||||
|
|
||||||
|
Remember that you can use Python's `isinstance
|
||||||
|
<https://docs.python.org/3/library/functions.html#isinstance>`_
|
||||||
|
to check the type of something. For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import types
|
||||||
|
|
||||||
|
if isinstance(entity.photo, types.ChatPhotoEmpty):
|
||||||
|
print('Channel has no photo')
|
228
readthedocs/concepts/updates.rst
Normal file
228
readthedocs/concepts/updates.rst
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
================
|
||||||
|
Updates in Depth
|
||||||
|
================
|
||||||
|
|
||||||
|
Properties vs. Methods
|
||||||
|
======================
|
||||||
|
|
||||||
|
The event shown above acts just like a `custom.Message
|
||||||
|
<telethon.tl.custom.message.Message>`, which means you
|
||||||
|
can access all the properties it has, like ``.sender``.
|
||||||
|
|
||||||
|
**However** events are different to other methods in the client, like
|
||||||
|
`client.get_messages <telethon.client.messages.MessageMethods.get_messages>`.
|
||||||
|
Events *may not* send information about the sender or chat, which means it
|
||||||
|
can be `None`, but all the methods defined in the client always have this
|
||||||
|
information so it doesn't need to be re-fetched. For this reason, you have
|
||||||
|
``get_`` methods, which will make a network call if necessary.
|
||||||
|
|
||||||
|
In short, you should do this:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
async def handler(event):
|
||||||
|
# event.input_chat may be None, use event.get_input_chat()
|
||||||
|
chat = await event.get_input_chat()
|
||||||
|
sender = await event.get_sender()
|
||||||
|
buttons = await event.get_buttons()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async for message in client.iter_messages('me', 10):
|
||||||
|
# Methods from the client always have these properties ready
|
||||||
|
chat = message.input_chat
|
||||||
|
sender = message.sender
|
||||||
|
buttons = message.buttons
|
||||||
|
|
||||||
|
Notice, properties (`message.sender
|
||||||
|
<telethon.tl.custom.message.Message.sender>`) don't need an ``await``, but
|
||||||
|
methods (`message.get_sender
|
||||||
|
<telethon.tl.custom.message.Message.get_sender>`) **do** need an ``await``,
|
||||||
|
and you should use methods in events for these properties that may need network.
|
||||||
|
|
||||||
|
Events Without the client
|
||||||
|
=========================
|
||||||
|
|
||||||
|
The code of your application starts getting big, so you decide to
|
||||||
|
separate the handlers into different files. But how can you access
|
||||||
|
the client from these files? You don't need to! Just `events.register
|
||||||
|
<telethon.events.register>` them:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# handlers/welcome.py
|
||||||
|
from telethon import events
|
||||||
|
|
||||||
|
@events.register(events.NewMessage('(?i)hello'))
|
||||||
|
async def handler(event):
|
||||||
|
client = event.client
|
||||||
|
await event.respond('Hey!')
|
||||||
|
await client.send_message('me', 'I said hello to someone')
|
||||||
|
|
||||||
|
|
||||||
|
Registering events is a way of saying "this method is an event handler".
|
||||||
|
You can use `telethon.events.is_handler` to check if any method is a handler.
|
||||||
|
You can think of them as a different approach to Flask's blueprints.
|
||||||
|
|
||||||
|
It's important to note that this does **not** add the handler to any client!
|
||||||
|
You never specified the client on which the handler should be used. You only
|
||||||
|
declared that it is a handler, and its type.
|
||||||
|
|
||||||
|
To actually use the handler, you need to `client.add_event_handler
|
||||||
|
<telethon.client.updates.UpdateMethods.add_event_handler>` to the
|
||||||
|
client (or clients) where they should be added to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# main.py
|
||||||
|
from telethon import TelegramClient
|
||||||
|
import handlers.welcome
|
||||||
|
|
||||||
|
with TelegramClient(...) as client:
|
||||||
|
client.add_event_handler(handlers.welcome.handler)
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
This also means that you can register an event handler once and
|
||||||
|
then add it to many clients without re-declaring the event.
|
||||||
|
|
||||||
|
|
||||||
|
Events Without Decorators
|
||||||
|
=========================
|
||||||
|
|
||||||
|
If for any reason you don't want to use `telethon.events.register`,
|
||||||
|
you can explicitly pass the event handler to use to the mentioned
|
||||||
|
`client.add_event_handler
|
||||||
|
<telethon.client.updates.UpdateMethods.add_event_handler>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
...
|
||||||
|
|
||||||
|
with TelegramClient(...) as client:
|
||||||
|
client.add_event_handler(handler, events.NewMessage)
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
Similarly, you also have `client.remove_event_handler
|
||||||
|
<telethon.client.updates.UpdateMethods.remove_event_handler>`
|
||||||
|
and `client.list_event_handlers
|
||||||
|
<telethon.client.updates.UpdateMethods.list_event_handlers>`.
|
||||||
|
|
||||||
|
The ``event`` argument is optional in all three methods and defaults to
|
||||||
|
`events.Raw <telethon.events.raw.Raw>` for adding, and `None` when
|
||||||
|
removing (so all callbacks would be removed).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``event`` type is ignored in `client.add_event_handler
|
||||||
|
<telethon.client.updates.UpdateMethods.add_event_handler>`
|
||||||
|
if you have used `telethon.events.register` on the ``callback``
|
||||||
|
before, since that's the point of using such method at all.
|
||||||
|
|
||||||
|
|
||||||
|
Stopping Propagation of Updates
|
||||||
|
===============================
|
||||||
|
|
||||||
|
There might be cases when an event handler is supposed to be used solitary and
|
||||||
|
it makes no sense to process any other handlers in the chain. For this case,
|
||||||
|
it is possible to raise a `telethon.events.StopPropagation` exception which
|
||||||
|
will cause the propagation of the update through your handlers to stop:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.events import StopPropagation
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
async def _(event):
|
||||||
|
# ... some conditions
|
||||||
|
await event.delete()
|
||||||
|
|
||||||
|
# Other handlers won't have an event to work with
|
||||||
|
raise StopPropagation
|
||||||
|
|
||||||
|
@client.on(events.NewMessage)
|
||||||
|
async def _(event):
|
||||||
|
# Will never be reached, because it is the second handler
|
||||||
|
# in the chain.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
Remember to check :ref:`telethon-events` if you're looking for
|
||||||
|
the methods reference.
|
||||||
|
|
||||||
|
Understanding asyncio
|
||||||
|
=====================
|
||||||
|
|
||||||
|
|
||||||
|
With `asyncio`, the library has several tasks running in the background.
|
||||||
|
One task is used for sending requests, another task is used to receive them,
|
||||||
|
and a third one is used to handle updates.
|
||||||
|
|
||||||
|
To handle updates, you must keep your script running. You can do this in
|
||||||
|
several ways. For instance, if you are *not* running `asyncio`'s event
|
||||||
|
loop, you should use `client.run_until_disconnected
|
||||||
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>`:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
client = TelegramClient(...)
|
||||||
|
...
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
Behind the scenes, this method is ``await``'ing on the `client.disconnected
|
||||||
|
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>` property,
|
||||||
|
so the code above and the following are equivalent:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
client = TelegramClient(...)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await client.disconnected
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
You could also run `client.disconnected
|
||||||
|
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`
|
||||||
|
until it completed.
|
||||||
|
|
||||||
|
But if you don't want to ``await``, then you should know what you want
|
||||||
|
to be doing instead! What matters is that you shouldn't let your script
|
||||||
|
die. If you don't care about updates, you don't need any of this.
|
||||||
|
|
||||||
|
Notice that unlike `client.disconnected
|
||||||
|
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`,
|
||||||
|
`client.run_until_disconnected
|
||||||
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>` will
|
||||||
|
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
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await client.run_until_disconnected()
|
||||||
|
|
||||||
|
loop.run_until_complete(main())
|
||||||
|
|
||||||
|
Sequential Updates
|
||||||
|
==================
|
||||||
|
|
||||||
|
If you need to process updates sequentially (i.e. not in parallel),
|
||||||
|
you should set ``sequential_updates=True`` when creating the client:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
with TelegramClient(..., sequential_updates=True) as client:
|
||||||
|
...
|
|
@ -20,14 +20,13 @@
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(os.curdir))
|
sys.path.insert(0, os.path.abspath(os.curdir))
|
||||||
sys.path.insert(0, os.path.abspath(os.pardir))
|
sys.path.insert(0, os.path.abspath(os.pardir))
|
||||||
|
|
||||||
|
|
||||||
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
|
||||||
|
|
||||||
tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
tl_ref_url = 'https://tl.telethon.dev'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
@ -40,9 +39,15 @@ tl_ref_url = 'https://lonamiwebs.github.io/Telethon'
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.autosummary',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
'custom_roles'
|
'custom_roles'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
intersphinx_mapping = {
|
||||||
|
'python': ('https://docs.python.org/3', None)
|
||||||
|
}
|
||||||
|
|
||||||
# Change the default role so we can avoid prefixing everything with :obj:
|
# Change the default role so we can avoid prefixing everything with :obj:
|
||||||
default_role = "py:obj"
|
default_role = "py:obj"
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'Telethon'
|
project = 'Telethon'
|
||||||
copyright = '2017, Lonami'
|
copyright = '2017 - 2019, Lonami'
|
||||||
author = 'Lonami'
|
author = 'Lonami'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
@ -80,7 +85,7 @@ release = version
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# 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
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
|
@ -88,12 +93,30 @@ language = None
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = 'friendly'
|
||||||
|
|
||||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
todo_include_todos = False
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
def skip(app, what, name, obj, would_skip, options):
|
||||||
|
if name.endswith('__'):
|
||||||
|
# We want to show special methods names, except some which add clutter
|
||||||
|
return name in {
|
||||||
|
'__init__',
|
||||||
|
'__abstractmethods__',
|
||||||
|
'__module__',
|
||||||
|
'__doc__',
|
||||||
|
'__dict__'
|
||||||
|
}
|
||||||
|
|
||||||
|
return would_skip
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect("autodoc-skip-member", skip)
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
@ -114,7 +137,7 @@ html_theme_options = {
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
# html_static_path = ['_static']
|
||||||
|
|
||||||
# Custom sidebar templates, must be a dictionary that maps document names
|
# Custom sidebar templates, must be a dictionary that maps document names
|
||||||
# to template names.
|
# to template names.
|
||||||
|
@ -186,5 +209,3 @@ texinfo_documents = [
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ def make_link_node(rawtext, app, name, options):
|
||||||
return node
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnusedLocal
|
||||||
def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||||
"""
|
"""
|
||||||
Link to the TL reference.
|
Link to the TL reference.
|
||||||
|
@ -45,8 +46,6 @@ def tl_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||||
"""
|
"""
|
||||||
if options is None:
|
if options is None:
|
||||||
options = {}
|
options = {}
|
||||||
if content is None:
|
|
||||||
content = []
|
|
||||||
|
|
||||||
# TODO Report error on type not found?
|
# TODO Report error on type not found?
|
||||||
# Usage:
|
# Usage:
|
||||||
|
@ -63,7 +62,6 @@ def setup(app):
|
||||||
|
|
||||||
:param app: Sphinx application context.
|
:param app: Sphinx application context.
|
||||||
"""
|
"""
|
||||||
app.info('Initializing TL reference plugin')
|
|
||||||
app.add_role('tl', tl_role)
|
app.add_role('tl', tl_role)
|
||||||
app.add_config_value('tl_ref_url', None, 'env')
|
app.add_config_value('tl_ref_url', None, 'env')
|
||||||
return
|
return
|
||||||
|
|
|
@ -4,7 +4,7 @@ Project Structure
|
||||||
|
|
||||||
|
|
||||||
Main interface
|
Main interface
|
||||||
**************
|
==============
|
||||||
|
|
||||||
The library itself is under the ``telethon/`` directory. The
|
The library itself is under the ``telethon/`` directory. The
|
||||||
``__init__.py`` file there exposes the main ``TelegramClient``, a class
|
``__init__.py`` file there exposes the main ``TelegramClient``, a class
|
||||||
|
@ -33,13 +33,8 @@ The sender makes use of a ``Connection`` class which knows the format in
|
||||||
which outgoing messages should be sent (how to encode their length and
|
which outgoing messages should be sent (how to encode their length and
|
||||||
their body, if they're further encrypted).
|
their body, if they're further encrypted).
|
||||||
|
|
||||||
For now, all connection modes make use of the ``extensions/tcpclient``,
|
|
||||||
a C#-like ``TcpClient`` to ease working with sockets in Python. All the
|
|
||||||
``TcpClient`` know is how to connect through TCP and writing/reading
|
|
||||||
from the socket with optional cancel.
|
|
||||||
|
|
||||||
Auto-generated code
|
Auto-generated code
|
||||||
*******************
|
===================
|
||||||
|
|
||||||
The files under ``telethon_generator/`` are used to generate the code
|
The files under ``telethon_generator/`` are used to generate the code
|
||||||
that gets placed under ``telethon/tl/``. The parsers take in files in
|
that gets placed under ``telethon/tl/``. The parsers take in files in
|
||||||
|
@ -50,3 +45,7 @@ an index so that they can be imported easily.
|
||||||
|
|
||||||
Custom documentation can also be generated to easily navigate through
|
Custom documentation can also be generated to easily navigate through
|
||||||
the vast amount of items offered by the API.
|
the vast amount of items offered by the API.
|
||||||
|
|
||||||
|
If you clone the repository, you will have to run ``python setup.py gen``
|
||||||
|
in order to generate the code. Installing the library runs the generator
|
||||||
|
too, but the mentioned command will just generate code.
|
13
readthedocs/developing/telegram-api-in-other-languages.rst
Normal file
13
readthedocs/developing/telegram-api-in-other-languages.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
===============================
|
||||||
|
Telegram API in Other Languages
|
||||||
|
===============================
|
||||||
|
|
||||||
|
Telethon was made for **Python**, and it has inspired other libraries such as
|
||||||
|
`gramjs <https://github.com/gram-js/gramjs>`__ (JavaScript) and `grammers
|
||||||
|
<https://github.com/Lonami/grammers>`__ (Rust). But there is a lot more beyond
|
||||||
|
those, made independently by different developers.
|
||||||
|
|
||||||
|
If you're looking for something like Telethon but in a different programming
|
||||||
|
language, head over to `Telegram API in Other Languages in the official wiki
|
||||||
|
<https://github.com/LonamiWebs/Telethon/wiki/Telegram-API-in-Other-Languages>`__
|
||||||
|
for a (mostly) up-to-date list.
|
|
@ -12,7 +12,7 @@ To run Telethon on a test server, use the following code:
|
||||||
|
|
||||||
You can check your ``'test ip'`` on https://my.telegram.org.
|
You can check your ``'test ip'`` on https://my.telegram.org.
|
||||||
|
|
||||||
You should set ``None`` session so to ensure you're generating a new
|
You should set `None` session so to ensure you're generating a new
|
||||||
authorization key for it (it would fail if you used a session where you
|
authorization key for it (it would fail if you used a session where you
|
||||||
had previously connected to another data center).
|
had previously connected to another data center).
|
||||||
|
|
||||||
|
@ -35,3 +35,7 @@ times, in this case, ``22222`` so we can hardcode that:
|
||||||
client.start(
|
client.start(
|
||||||
phone='9996621234', code_callback=lambda: '22222'
|
phone='9996621234', code_callback=lambda: '22222'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Note that Telegram has changed the length of login codes multiple times in the
|
||||||
|
past, so if ``dc_id`` repeated five times does not work, try repeating it six
|
||||||
|
times.
|
87
readthedocs/developing/testing.rst
Normal file
87
readthedocs/developing/testing.rst
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
=====
|
||||||
|
Tests
|
||||||
|
=====
|
||||||
|
|
||||||
|
Telethon uses `Pytest <https://pytest.org/>`__, for testing, `Tox
|
||||||
|
<https://tox.readthedocs.io/en/latest/>`__ for environment setup, and
|
||||||
|
`pytest-asyncio <https://pypi.org/project/pytest-asyncio/>`__ and `pytest-cov
|
||||||
|
<https://pytest-cov.readthedocs.io/en/latest/>`__ for asyncio and
|
||||||
|
`coverage <https://coverage.readthedocs.io/>`__ integration.
|
||||||
|
|
||||||
|
While reading the full documentation for these is probably a good idea, there
|
||||||
|
is a lot to read, so a brief summary of these tools is provided below for
|
||||||
|
convienience.
|
||||||
|
|
||||||
|
Brief Introduction to Pytest
|
||||||
|
============================
|
||||||
|
|
||||||
|
`Pytest <https://pytest.org/>`__ is a tool for discovering and running python
|
||||||
|
tests, as well as allowing modular reuse of test setup code using fixtures.
|
||||||
|
|
||||||
|
Most Pytest tests will look something like this::
|
||||||
|
|
||||||
|
from module import my_thing, my_other_thing
|
||||||
|
|
||||||
|
def test_my_thing(fixture):
|
||||||
|
assert my_thing(fixture) == 42
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_my_thing(event_loop):
|
||||||
|
assert await my_other_thing(loop=event_loop) == 42
|
||||||
|
|
||||||
|
Note here:
|
||||||
|
|
||||||
|
1. The test imports one specific function. The role of unit tests is to test
|
||||||
|
that the implementation of some unit, like a function or class, works.
|
||||||
|
It's role is not so much to test that components interact well with each
|
||||||
|
other. I/O, such as connecting to remote servers, should be avoided. This
|
||||||
|
helps with quickly identifying the source of an error, finding silent
|
||||||
|
breakage, and makes it easier to cover all possible code paths.
|
||||||
|
|
||||||
|
System or integration tests can also be useful, but are currently out of
|
||||||
|
scope of Telethon's automated testing.
|
||||||
|
|
||||||
|
2. A function ``test_my_thing`` is declared. Pytest searches for files
|
||||||
|
starting with ``test_``, classes starting with ``Test`` and executes any
|
||||||
|
functions or methods starting with ``test_`` it finds.
|
||||||
|
|
||||||
|
3. The function is declared with a parameter ``fixture``. Fixtures are used to
|
||||||
|
request things required to run the test, such as temporary directories,
|
||||||
|
free TCP ports, Connections, etc. Fixtures are declared by simply adding
|
||||||
|
the fixture name as parameter. A full list of available fixtures can be
|
||||||
|
found with the ``pytest --fixtures`` command.
|
||||||
|
|
||||||
|
4. The test uses a simple ``assert`` to test some condition is valid. Pytest
|
||||||
|
uses some magic to ensure that the errors from this are readable and easy
|
||||||
|
to debug.
|
||||||
|
|
||||||
|
5. The ``pytest.mark.asyncio`` fixture is provided by ``pytest-asyncio``. It
|
||||||
|
starts a loop and executes a test function as coroutine. This should be
|
||||||
|
used for testing asyncio code. It also declares the ``event_loop``
|
||||||
|
fixture, which will request an ``asyncio`` event loop.
|
||||||
|
|
||||||
|
Brief Introduction to Tox
|
||||||
|
=========================
|
||||||
|
|
||||||
|
`Tox <https://tox.readthedocs.io/en/latest/>`__ is a tool for automated setup
|
||||||
|
of virtual environments for testing. While the tests can be run directly by
|
||||||
|
just running ``pytest``, this only tests one specific python version in your
|
||||||
|
existing environment, which will not catch e.g. undeclared dependencies, or
|
||||||
|
version incompatabilities.
|
||||||
|
|
||||||
|
Tox environments are declared in the ``tox.ini`` file. The default
|
||||||
|
environments, declared at the top, can be simply run with ``tox``. The option
|
||||||
|
``tox -e py36,flake`` can be used to request specific environments to be run.
|
||||||
|
|
||||||
|
Brief Introduction to Pytest-cov
|
||||||
|
================================
|
||||||
|
|
||||||
|
Coverage is a useful metric for testing. It measures the lines of code and
|
||||||
|
branches that are exercised by the tests. The higher the coverage, the more
|
||||||
|
likely it is that any coding errors will be caught by the tests.
|
||||||
|
|
||||||
|
A brief coverage report can be generated with the ``--cov`` option to ``tox``,
|
||||||
|
which will be passed on to ``pytest``. Additionally, the very useful HTML
|
||||||
|
report can be generated with ``--cov --cov-report=html``, which contains a
|
||||||
|
browsable copy of the source code, annotated with coverage information for each
|
||||||
|
line.
|
128
readthedocs/examples/chats-and-channels.rst
Normal file
128
readthedocs/examples/chats-and-channels.rst
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
===============================
|
||||||
|
Working with Chats and Channels
|
||||||
|
===============================
|
||||||
|
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
These examples assume you have read :ref:`full-api`.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
Joining a chat or channel
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a
|
||||||
|
special form of :tl:`Chat`, which can also be super-groups if
|
||||||
|
their ``megagroup`` member is `True`.
|
||||||
|
|
||||||
|
|
||||||
|
Joining a public channel
|
||||||
|
========================
|
||||||
|
|
||||||
|
Once you have the :ref:`entity <entities>` of the channel you want to join
|
||||||
|
to, you can make use of the :tl:`JoinChannelRequest` to join such channel:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.tl.functions.channels import JoinChannelRequest
|
||||||
|
await client(JoinChannelRequest(channel))
|
||||||
|
|
||||||
|
# In the same way, you can also leave such channel
|
||||||
|
from telethon.tl.functions.channels import LeaveChannelRequest
|
||||||
|
await client(LeaveChannelRequest(input_channel))
|
||||||
|
|
||||||
|
|
||||||
|
For more on channels, check the `channels namespace`__.
|
||||||
|
|
||||||
|
|
||||||
|
__ https://tl.telethon.dev/methods/channels/index.html
|
||||||
|
|
||||||
|
|
||||||
|
Joining a private chat or channel
|
||||||
|
=================================
|
||||||
|
|
||||||
|
If all you have is a link like this one:
|
||||||
|
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
|
||||||
|
enough information to join! The part after the
|
||||||
|
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
|
||||||
|
example, is the ``hash`` of the chat or channel. Now you can use
|
||||||
|
:tl:`ImportChatInviteRequest` as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.tl.functions.messages import ImportChatInviteRequest
|
||||||
|
updates = await client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
|
||||||
|
|
||||||
|
|
||||||
|
Adding someone else to such chat or channel
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
If you don't want to add yourself, maybe because you're already in,
|
||||||
|
you can always add someone else with the :tl:`AddChatUserRequest`, which
|
||||||
|
use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# For normal chats
|
||||||
|
from telethon.tl.functions.messages import AddChatUserRequest
|
||||||
|
|
||||||
|
# Note that ``user_to_add`` is NOT the name of the parameter.
|
||||||
|
# It's the user you want to add (``user_id=user_to_add``).
|
||||||
|
await client(AddChatUserRequest(
|
||||||
|
chat_id,
|
||||||
|
user_to_add,
|
||||||
|
fwd_limit=10 # Allow the user to see the 10 last messages
|
||||||
|
))
|
||||||
|
|
||||||
|
# For channels (which includes megagroups)
|
||||||
|
from telethon.tl.functions.channels import InviteToChannelRequest
|
||||||
|
|
||||||
|
await client(InviteToChannelRequest(
|
||||||
|
channel,
|
||||||
|
[users_to_add]
|
||||||
|
))
|
||||||
|
|
||||||
|
Note that this method will only really work for friends or bot accounts.
|
||||||
|
Trying to mass-add users with this approach will not work, and can put both
|
||||||
|
your account and group to risk, possibly being flagged as spam and limited.
|
||||||
|
|
||||||
|
|
||||||
|
Checking a link without joining
|
||||||
|
===============================
|
||||||
|
|
||||||
|
If you don't need to join but rather check whether it's a group or a
|
||||||
|
channel, you can use the :tl:`CheckChatInviteRequest`, which takes in
|
||||||
|
the hash of said channel or group.
|
||||||
|
|
||||||
|
|
||||||
|
Increasing View Count in a Channel
|
||||||
|
==================================
|
||||||
|
|
||||||
|
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
|
||||||
|
while I don't understand why so many people ask this, the solution is to
|
||||||
|
use :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
|
||||||
|
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
||||||
|
# Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list.
|
||||||
|
|
||||||
|
await client(GetMessagesViewsRequest(
|
||||||
|
peer=channel,
|
||||||
|
id=msg_ids,
|
||||||
|
increment=True
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
Note that you can only do this **once or twice a day** per account,
|
||||||
|
running this in a loop will obviously not increase the views forever
|
||||||
|
unless you wait a day between each iteration. If you run it any sooner
|
||||||
|
than that, the views simply won't be increased.
|
||||||
|
|
||||||
|
__ https://github.com/LonamiWebs/Telethon/issues/233
|
||||||
|
__ https://github.com/LonamiWebs/Telethon/issues/305
|
||||||
|
__ https://github.com/LonamiWebs/Telethon/issues/409
|
||||||
|
__ https://github.com/LonamiWebs/Telethon/issues/447
|
|
@ -5,11 +5,13 @@ Users
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
These examples assume you have read :ref:`full-api`.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
Retrieving full information
|
Retrieving full information
|
||||||
***************************
|
===========================
|
||||||
|
|
||||||
If you need to retrieve the bio, biography or about information for a user
|
If you need to retrieve the bio, biography or about information for a user
|
||||||
you should use :tl:`GetFullUser`:
|
you should use :tl:`GetFullUser`:
|
||||||
|
@ -19,18 +21,18 @@ you should use :tl:`GetFullUser`:
|
||||||
|
|
||||||
from telethon.tl.functions.users import GetFullUserRequest
|
from telethon.tl.functions.users import GetFullUserRequest
|
||||||
|
|
||||||
full = client(GetFullUserRequest(user))
|
full = await client(GetFullUserRequest(user))
|
||||||
# or even
|
# or even
|
||||||
full = client(GetFullUserRequest('username'))
|
full = await client(GetFullUserRequest('username'))
|
||||||
|
|
||||||
bio = full.about
|
bio = full.full_user.about
|
||||||
|
|
||||||
|
|
||||||
See :tl:`UserFull` to know what other fields you can access.
|
See :tl:`UserFull` to know what other fields you can access.
|
||||||
|
|
||||||
|
|
||||||
Updating your name and/or bio
|
Updating your name and/or bio
|
||||||
*****************************
|
=============================
|
||||||
|
|
||||||
The first name, last name and bio (about) can all be changed with the same
|
The first name, last name and bio (about) can all be changed with the same
|
||||||
request. Omitted fields won't change after invoking :tl:`UpdateProfile`:
|
request. Omitted fields won't change after invoking :tl:`UpdateProfile`:
|
||||||
|
@ -39,13 +41,13 @@ request. Omitted fields won't change after invoking :tl:`UpdateProfile`:
|
||||||
|
|
||||||
from telethon.tl.functions.account import UpdateProfileRequest
|
from telethon.tl.functions.account import UpdateProfileRequest
|
||||||
|
|
||||||
client(UpdateProfileRequest(
|
await client(UpdateProfileRequest(
|
||||||
about='This is a test from Telethon'
|
about='This is a test from Telethon'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
Updating your username
|
Updating your username
|
||||||
**********************
|
======================
|
||||||
|
|
||||||
You need to use :tl:`account.UpdateUsername`:
|
You need to use :tl:`account.UpdateUsername`:
|
||||||
|
|
||||||
|
@ -53,11 +55,11 @@ You need to use :tl:`account.UpdateUsername`:
|
||||||
|
|
||||||
from telethon.tl.functions.account import UpdateUsernameRequest
|
from telethon.tl.functions.account import UpdateUsernameRequest
|
||||||
|
|
||||||
client(UpdateUsernameRequest('new_username'))
|
await client(UpdateUsernameRequest('new_username'))
|
||||||
|
|
||||||
|
|
||||||
Updating your profile photo
|
Updating your profile photo
|
||||||
***************************
|
===========================
|
||||||
|
|
||||||
The easiest way is to upload a new file and use that as the profile photo
|
The easiest way is to upload a new file and use that as the profile photo
|
||||||
through :tl:`UploadProfilePhoto`:
|
through :tl:`UploadProfilePhoto`:
|
||||||
|
@ -67,6 +69,6 @@ through :tl:`UploadProfilePhoto`:
|
||||||
|
|
||||||
from telethon.tl.functions.photos import UploadProfilePhotoRequest
|
from telethon.tl.functions.photos import UploadProfilePhotoRequest
|
||||||
|
|
||||||
client(UploadProfilePhotoRequest(
|
await client(UploadProfilePhotoRequest(
|
||||||
client.upload_file('/path/to/some/file')
|
await client.upload_file('/path/to/some/file')
|
||||||
)))
|
))
|
17
readthedocs/examples/word-of-warning.rst
Normal file
17
readthedocs/examples/word-of-warning.rst
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
=================
|
||||||
|
A Word of Warning
|
||||||
|
=================
|
||||||
|
|
||||||
|
Full API is **not** how you are intended to use the library. You **should**
|
||||||
|
always prefer the :ref:`client-ref`. However, not everything is implemented
|
||||||
|
as a friendly method, so full API is your last resort.
|
||||||
|
|
||||||
|
If you select a method in :ref:`client-ref`, you will most likely find an
|
||||||
|
example for that method. This is how you are intended to use the library.
|
||||||
|
|
||||||
|
Full API **will** break between different minor versions of the library,
|
||||||
|
since Telegram changes very often. The friendly methods will be kept
|
||||||
|
compatible between major versions.
|
||||||
|
|
||||||
|
If you need to see real-world examples, please refer to the
|
||||||
|
`wiki page of projects using Telethon <https://github.com/LonamiWebs/Telethon/wiki/Projects-using-Telethon>`__.
|
13
readthedocs/examples/working-with-messages.rst
Normal file
13
readthedocs/examples/working-with-messages.rst
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
=====================
|
||||||
|
Working with messages
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
These examples assume you have read :ref:`full-api`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. _moved to the wiki: https://github.com/LonamiWebs/Telethon/wiki/Sending-more-than-just-messages
|
|
@ -1,231 +0,0 @@
|
||||||
.. _accessing-the-full-api:
|
|
||||||
|
|
||||||
======================
|
|
||||||
Accessing the Full API
|
|
||||||
======================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
While you have access to this, you should always use the friendly
|
|
||||||
methods listed on :ref:`telethon-client` unless you have a better
|
|
||||||
reason not to, like a method not existing or you wanting more control.
|
|
||||||
|
|
||||||
|
|
||||||
The :ref:`TelegramClient <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.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The reason to keep both https://lonamiwebs.github.io/Telethon and this
|
|
||||||
documentation alive is that the former allows instant search results
|
|
||||||
as you type, and a "Copy import" button. If you like namespaces, you
|
|
||||||
can also do ``from telethon.tl import types, functions``. Both work.
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
All the examples in this documentation assume that you have
|
|
||||||
``from telethon import sync`` or ``import telethon.sync``
|
|
||||||
for the sake of simplicity and that you understand what
|
|
||||||
it does (see :ref:`asyncio-magic` for more). Simply add
|
|
||||||
either line at the beginning of your project and it will work.
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Say `client.send_message
|
|
||||||
<telethon.client.messages.MessageMethods.send_message>` didn't exist,
|
|
||||||
we could use the `search`__ to look for "message". There we would find
|
|
||||||
:tl:`SendMessageRequest`, which we can work with.
|
|
||||||
|
|
||||||
Every request is a Python class, and has the parameters needed for you
|
|
||||||
to invoke it. You can also call ``help(request)`` for information on
|
|
||||||
what input parameters it takes. Remember to "Copy import to the
|
|
||||||
clipboard", or your script won't be aware of this class! Now we have:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import SendMessageRequest
|
|
||||||
|
|
||||||
If you're going to use a lot of these, you may do:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl import types, functions
|
|
||||||
# We now have access to 'functions.messages.SendMessageRequest'
|
|
||||||
|
|
||||||
We see that this request must take at least two parameters, a ``peer``
|
|
||||||
of type :tl:`InputPeer`, and a ``message`` which is just a Python
|
|
||||||
``str``\ ing.
|
|
||||||
|
|
||||||
How can we retrieve this :tl:`InputPeer`? We have two options. We manually
|
|
||||||
construct one, for instance:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.types import InputPeerUser
|
|
||||||
|
|
||||||
peer = InputPeerUser(user_id, user_hash)
|
|
||||||
|
|
||||||
Or we call `client.get_input_entity
|
|
||||||
<telethon.client.users.UserMethods.get_input_entity>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import telethon.sync
|
|
||||||
peer = client.get_input_entity('someone')
|
|
||||||
|
|
||||||
|
|
||||||
When you're going to invoke an API method, most require you to pass an
|
|
||||||
:tl:`InputUser`, :tl:`InputChat`, or so on, this is why using
|
|
||||||
`client.get_input_entity <telethon.client.users.UserMethods.get_input_entity>`
|
|
||||||
is more straightforward (and often immediate, if you've seen the user before,
|
|
||||||
know their ID, etc.). If you also **need** to have information about the whole
|
|
||||||
user, use `client.get_entity <telethon.client.users.UserMethods.get_entity>`
|
|
||||||
instead:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
entity = client.get_entity('someone')
|
|
||||||
|
|
||||||
In the later case, when you use the entity, the library will cast it to
|
|
||||||
its "input" version for you. If you already have the complete user and
|
|
||||||
want to cache its input version so the library doesn't have to do this
|
|
||||||
every time its used, simply call `telethon.utils.get_input_peer`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import utils
|
|
||||||
peer = utils.get_input_peer(entity)
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Since ``v0.16.2`` this is further simplified. The ``Request`` itself
|
|
||||||
will call `client.get_input_entity <
|
|
||||||
telethon.client.users.UserMethods.get_input_entity>` for you when required,
|
|
||||||
but it's good to remember what's happening.
|
|
||||||
|
|
||||||
|
|
||||||
After this small parenthesis about `client.get_entity
|
|
||||||
<telethon.client.users.UserMethods.get_entity>` versus
|
|
||||||
`client.get_input_entity <telethon.client.users.UserMethods.get_input_entity>`,
|
|
||||||
we have everything we need. To invoke our
|
|
||||||
request we do:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
result = client(SendMessageRequest(peer, 'Hello there!'))
|
|
||||||
# __call__ is an alias for client.invoke(request). Both will work
|
|
||||||
|
|
||||||
Message sent! Of course, this is only an example. There are over 250
|
|
||||||
methods available as of layer 80, and you can use every single of them
|
|
||||||
as you wish. Remember to use the right types! To sum up:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
result = client(SendMessageRequest(
|
|
||||||
client.get_input_entity('username'), 'Hello there!'
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
This can further be simplified to:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
result = client(SendMessageRequest('username', 'Hello there!'))
|
|
||||||
# Or even
|
|
||||||
result = client(SendMessageRequest(PeerChannel(id), 'Hello there!'))
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Note that some requests have a "hash" parameter. This is **not**
|
|
||||||
your ``api_hash``! It likely isn't your self-user ``.access_hash`` either.
|
|
||||||
|
|
||||||
It's a special hash used by Telegram to only send a difference of new data
|
|
||||||
that you don't already have with that request, so you can leave it to 0,
|
|
||||||
and it should work (which means no hash is known yet).
|
|
||||||
|
|
||||||
For those requests having a "limit" parameter, you can often set it to
|
|
||||||
zero to signify "return default amount". This won't work for all of them
|
|
||||||
though, for instance, in "messages.search" it will actually return 0 items.
|
|
||||||
|
|
||||||
|
|
||||||
Requests in Parallel
|
|
||||||
********************
|
|
||||||
|
|
||||||
The library will automatically merge outgoing requests into a single
|
|
||||||
*container*. Telegram's API supports sending multiple requests in a
|
|
||||||
single container, which is faster because it has less overhead and
|
|
||||||
the server can run them without waiting for others. You can also
|
|
||||||
force using a container manually:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
|
|
||||||
# Letting the library do it behind the scenes
|
|
||||||
await asyncio.wait([
|
|
||||||
client.send_message('me', 'Hello'),
|
|
||||||
client.send_message('me', ','),
|
|
||||||
client.send_message('me', 'World'),
|
|
||||||
client.send_message('me', '.')
|
|
||||||
])
|
|
||||||
|
|
||||||
# Manually invoking many requests at once
|
|
||||||
await client([
|
|
||||||
SendMessageRequest('me', 'Hello'),
|
|
||||||
SendMessageRequest('me', ', '),
|
|
||||||
SendMessageRequest('me', 'World'),
|
|
||||||
SendMessageRequest('me', '.')
|
|
||||||
])
|
|
||||||
|
|
||||||
Note that you cannot guarantee the order in which they are run.
|
|
||||||
Try running the above code more than one time. You will see the
|
|
||||||
order in which the messages arrive is different.
|
|
||||||
|
|
||||||
If you use the raw API (the first option), you can use ``ordered``
|
|
||||||
to tell the server that it should run the requests sequentially.
|
|
||||||
This will still be faster than going one by one, since the server
|
|
||||||
knows all requests directly:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client([
|
|
||||||
SendMessageRequest('me', 'Hello'),
|
|
||||||
SendMessageRequest('me', ', '),
|
|
||||||
SendMessageRequest('me', 'World'),
|
|
||||||
SendMessageRequest('me', '.')
|
|
||||||
], ordered=True)
|
|
||||||
|
|
||||||
If any of the requests fails with a Telegram error (not connection
|
|
||||||
errors or any other unexpected events), the library will raise
|
|
||||||
`telethon.errors.common.MultiError`. You can ``except`` this
|
|
||||||
and still access the successful results:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.errors import MultiError
|
|
||||||
|
|
||||||
try:
|
|
||||||
client([
|
|
||||||
SendMessageRequest('me', 'Hello'),
|
|
||||||
SendMessageRequest('me', ''),
|
|
||||||
SendMessageRequest('me', 'World')
|
|
||||||
], ordered=True)
|
|
||||||
except MultiError as e:
|
|
||||||
# The first and third requests worked.
|
|
||||||
first = e.results[0]
|
|
||||||
third = e.results[2]
|
|
||||||
# The second request failed.
|
|
||||||
second = e.exceptions[1]
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/index.html
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/?q=message&redirect=no
|
|
|
@ -1,338 +0,0 @@
|
||||||
.. _mastering-telethon:
|
|
||||||
|
|
||||||
==================
|
|
||||||
Mastering Telethon
|
|
||||||
==================
|
|
||||||
|
|
||||||
You've come far! In this section you will learn best practices, as well
|
|
||||||
as how to fix some silly (yet common) errors you may have found. Let's
|
|
||||||
start with a simple one.
|
|
||||||
|
|
||||||
Asyncio madness
|
|
||||||
***************
|
|
||||||
|
|
||||||
We promise ``asyncio`` is worth learning. Take your time to learn it.
|
|
||||||
It's a powerful tool that enables you to use this powerful library.
|
|
||||||
You need to be comfortable with it if you want to master Telethon.
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
AttributeError: 'coroutine' object has no attribute 'id'
|
|
||||||
|
|
||||||
You probably had a previous version, upgraded, and expected everything
|
|
||||||
to work. Remember, just add this line:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import telethon.sync
|
|
||||||
|
|
||||||
If you're inside an event handler you need to ``await`` **everything** that
|
|
||||||
*makes a network request*. Getting users, sending messages, and nearly
|
|
||||||
everything in the library needs access to the network, so they need to
|
|
||||||
be awaited:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
print((await event.get_sender()).username)
|
|
||||||
|
|
||||||
|
|
||||||
You may want to read https://lonamiwebs.github.io/blog/asyncio/ to help
|
|
||||||
you understand ``asyncio`` better. I'm open for `feedback
|
|
||||||
<https://t.me/LonamiWebs>`_ regarding that blog post
|
|
||||||
|
|
||||||
Entities
|
|
||||||
********
|
|
||||||
|
|
||||||
A lot of methods and requests require *entities* to work. For example,
|
|
||||||
you send a message to an *entity*, get the username of an *entity*, and
|
|
||||||
so on. There is an entire section on this at :ref:`entities` due to their
|
|
||||||
importance.
|
|
||||||
|
|
||||||
There are a lot of things that work as entities: usernames, phone numbers,
|
|
||||||
chat links, invite links, IDs, and the types themselves. That is, you can
|
|
||||||
use any of those when you see an "entity" is needed.
|
|
||||||
|
|
||||||
You should use, **from better to worse**:
|
|
||||||
|
|
||||||
1. Input entities. For example, `event.input_chat
|
|
||||||
<telethon.tl.custom.chatgetter.ChatGetter.input_chat>`,
|
|
||||||
`message.input_sender
|
|
||||||
<telethon.tl.custom.sendergetter.SenderGetter.input_sender>`,
|
|
||||||
or caching an entity you will use a lot with
|
|
||||||
``entity = await client.get_input_entity(...)``.
|
|
||||||
|
|
||||||
2. Entities. For example, if you had to get someone's
|
|
||||||
username, you can just use ``user`` or ``channel``.
|
|
||||||
It will work. Only use this option if you already have the entity!
|
|
||||||
|
|
||||||
3. IDs. This will always look the entity up from the
|
|
||||||
cache (the ``*.session`` file caches seen entities).
|
|
||||||
|
|
||||||
4. Usernames, phone numbers and links. The cache will be
|
|
||||||
used too (unless you force a `client.get_entity()
|
|
||||||
<telethon.client.users.UserMethods.get_entity>`),
|
|
||||||
but may make a request if the username, phone or link
|
|
||||||
has not been found yet.
|
|
||||||
|
|
||||||
In short, unlike in most bot API libraries where you use the ID, you
|
|
||||||
**should not** use the ID *if* you have the input entity. This is OK:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def handler(event):
|
|
||||||
await client.send_message(event.sender_id, 'Hi')
|
|
||||||
|
|
||||||
However, **this is better**:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def handler(event):
|
|
||||||
await client.send_message(event.input_sender, 'Hi')
|
|
||||||
|
|
||||||
Note that this also works for `message <telethon.tl.custom.message.Message>`
|
|
||||||
instead of ``event``. Telegram may not send the sender information, so if you
|
|
||||||
want to be 99% confident that the above will work you should do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def handler(event):
|
|
||||||
await client.send_message(await event.get_input_sender(), 'Hi')
|
|
||||||
|
|
||||||
Methods are able to make network requests to get information that
|
|
||||||
could be missing. Properties will never make a network request.
|
|
||||||
|
|
||||||
Of course, it is convenient to IDs or usernames for most purposes. It will
|
|
||||||
be fast enough and caching with `client.get_input_entity(...)
|
|
||||||
<telethon.client.users.UserMethods.get_input_entity>` will
|
|
||||||
be a micro-optimization. However it's worth knowing, and it
|
|
||||||
will also let you know if the entity cannot be found beforehand.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Sometimes Telegram doesn't send the access hash inside entities,
|
|
||||||
so using `chat <telethon.tl.custom.chatgetter.ChatGetter.chat>`
|
|
||||||
or `sender <telethon.tl.custom.sendergetter.SenderGetter.sender>`
|
|
||||||
may not work, but `input_chat
|
|
||||||
<telethon.tl.custom.chatgetter.ChatGetter.input_chat>`
|
|
||||||
and `input_sender
|
|
||||||
<telethon.tl.custom.sendergetter.SenderGetter.input_sender>`
|
|
||||||
while making requests definitely will since that's what they exist
|
|
||||||
for. If Telegram did not send information about the access hash,
|
|
||||||
you will get something like "Invalid channel object" or
|
|
||||||
"Invalid user object".
|
|
||||||
|
|
||||||
|
|
||||||
Debugging
|
|
||||||
*********
|
|
||||||
|
|
||||||
**Please enable logging**:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
|
||||||
|
|
||||||
Change it for ``logging.DEBUG`` if you are asked for logs. It will save you
|
|
||||||
a lot of headaches and time when you work with events. This is for errors.
|
|
||||||
|
|
||||||
Debugging is *really* important. Telegram's API is really big and there
|
|
||||||
is a lot of things that you should know. Such as, what attributes or fields
|
|
||||||
does a result have? Well, the easiest thing to do is printing it:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
user = client.get_entity('Lonami')
|
|
||||||
print(user)
|
|
||||||
|
|
||||||
That will show a huge line similar to the following:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
User(id=10885151, is_self=False, contact=False, mutual_contact=False, deleted=False, bot=False, bot_chat_history=False, bot_nochats=False, verified=False, restricted=False, min=False, bot_inline_geo=False, access_hash=123456789012345678, first_name='Lonami', last_name=None, username='Lonami', phone=None, photo=UserProfilePhoto(photo_id=123456789012345678, photo_small=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678), photo_big=FileLocation(dc_id=4, volume_id=1234567890, local_id=1234567890, secret=123456789012345678)), status=UserStatusOffline(was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)), bot_info_version=None, restriction_reason=None, bot_inline_placeholder=None, lang_code=None)
|
|
||||||
|
|
||||||
That's a lot of text. But as you can see, all the properties are there.
|
|
||||||
So if you want the username you **don't use regex** or anything like
|
|
||||||
splitting ``str(user)`` to get what you want. You just access the
|
|
||||||
attribute you need:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
username = user.username
|
|
||||||
|
|
||||||
Can we get better than the shown string, though? Yes!
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
print(user.stringify())
|
|
||||||
|
|
||||||
Will show a much better:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
User(
|
|
||||||
id=10885151,
|
|
||||||
is_self=False,
|
|
||||||
contact=False,
|
|
||||||
mutual_contact=False,
|
|
||||||
deleted=False,
|
|
||||||
bot=False,
|
|
||||||
bot_chat_history=False,
|
|
||||||
bot_nochats=False,
|
|
||||||
verified=False,
|
|
||||||
restricted=False,
|
|
||||||
min=False,
|
|
||||||
bot_inline_geo=False,
|
|
||||||
access_hash=123456789012345678,
|
|
||||||
first_name='Lonami',
|
|
||||||
last_name=None,
|
|
||||||
username='Lonami',
|
|
||||||
phone=None,
|
|
||||||
photo=UserProfilePhoto(
|
|
||||||
photo_id=123456789012345678,
|
|
||||||
photo_small=FileLocation(
|
|
||||||
dc_id=4,
|
|
||||||
volume_id=123456789,
|
|
||||||
local_id=123456789,
|
|
||||||
secret=-123456789012345678
|
|
||||||
),
|
|
||||||
photo_big=FileLocation(
|
|
||||||
dc_id=4,
|
|
||||||
volume_id=123456789,
|
|
||||||
local_id=123456789,
|
|
||||||
secret=123456789012345678
|
|
||||||
)
|
|
||||||
),
|
|
||||||
status=UserStatusOffline(
|
|
||||||
was_online=datetime.datetime(2018, 1, 2, 3, 4, 5, tzinfo=datetime.timezone.utc)
|
|
||||||
),
|
|
||||||
bot_info_version=None,
|
|
||||||
restriction_reason=None,
|
|
||||||
bot_inline_placeholder=None,
|
|
||||||
lang_code=None
|
|
||||||
)
|
|
||||||
|
|
||||||
Now it's easy to see how we could get, for example,
|
|
||||||
the ``was_online`` time. It's inside ``status``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
online_at = user.status.was_online
|
|
||||||
|
|
||||||
You don't need to print everything to see what all the possible values
|
|
||||||
can be. You can just search in http://lonamiwebs.github.io/Telethon/.
|
|
||||||
|
|
||||||
Remember that you can use Python's `isinstance
|
|
||||||
<https://docs.python.org/3/library/functions.html#isinstance>`_
|
|
||||||
to check the type of something. For example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import types
|
|
||||||
|
|
||||||
if isinstance(user.status, types.UserStatusOffline):
|
|
||||||
print(user.status.was_online)
|
|
||||||
|
|
||||||
Avoiding Limits
|
|
||||||
***************
|
|
||||||
|
|
||||||
Don't spam. You won't get ``FloodWaitError`` or your account banned or
|
|
||||||
deleted if you use the library *for legit use cases*. Make cool tools.
|
|
||||||
Don't spam! Nobody knows the exact limits for all requests since they
|
|
||||||
depend on a lot of factors, so don't bother asking.
|
|
||||||
|
|
||||||
Still, if you do have a legit use case and still get those errors, the
|
|
||||||
library will automatically sleep when they are smaller than 60 seconds
|
|
||||||
by default. You can set different "auto-sleep" thresholds:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.flood_sleep_threshold = 0 # Don't auto-sleep
|
|
||||||
client.flood_sleep_threshold = 24 * 60 * 60 # Sleep always
|
|
||||||
|
|
||||||
You can also except it and act as you prefer:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.errors import FloodWaitError
|
|
||||||
try:
|
|
||||||
...
|
|
||||||
except FloodWaitError as e:
|
|
||||||
print('Flood waited for', e.seconds)
|
|
||||||
quit(1)
|
|
||||||
|
|
||||||
VoIP numbers are very limited, and some countries are more limited too.
|
|
||||||
|
|
||||||
Chat or User From Messages
|
|
||||||
**************************
|
|
||||||
|
|
||||||
Although it's explicitly noted in the documentation that messages
|
|
||||||
*subclass* `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
|
||||||
and `SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`,
|
|
||||||
some people still don't get inheritance.
|
|
||||||
|
|
||||||
When the documentation says "Bases: `telethon.tl.custom.chatgetter.ChatGetter`"
|
|
||||||
it means that the class you're looking at, *also* can act as the class it
|
|
||||||
bases. In this case, `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`
|
|
||||||
knows how to get the *chat* where a thing belongs to.
|
|
||||||
|
|
||||||
So, a `Message <telethon.tl.custom.message.Message>` is a
|
|
||||||
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.
|
|
||||||
That means you can do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message.is_private
|
|
||||||
message.chat_id
|
|
||||||
message.get_chat()
|
|
||||||
# ...etc
|
|
||||||
|
|
||||||
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is similar:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message.user_id
|
|
||||||
message.get_input_user()
|
|
||||||
message.user
|
|
||||||
# ...etc
|
|
||||||
|
|
||||||
Quite a few things implement them, so it makes sense to reuse the code.
|
|
||||||
For example, all events (except raw updates) implement `ChatGetter
|
|
||||||
<telethon.tl.custom.chatgetter.ChatGetter>` since all events occur
|
|
||||||
in some chat.
|
|
||||||
|
|
||||||
Session Files
|
|
||||||
*************
|
|
||||||
|
|
||||||
They are an important part for the library to be efficient, such as caching
|
|
||||||
and handling your authorization key (or you would have to login every time!).
|
|
||||||
|
|
||||||
However, some people have a lot of trouble with SQLite, especially in Windows:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
...some lines of traceback
|
|
||||||
'insert or replace into entities values (?,?,?,?,?)', rows)
|
|
||||||
sqlite3.OperationalError: database is locked
|
|
||||||
|
|
||||||
This error occurs when **two or more clients use the same session**,
|
|
||||||
that is, when you write the same session name to be used in the client:
|
|
||||||
|
|
||||||
* You have two scripts running (interactive sessions count too).
|
|
||||||
* You have two clients in the same script running at the same time.
|
|
||||||
|
|
||||||
The solution is, if you need two clients, use two sessions. If the
|
|
||||||
problem persists and you're on Linux, you can use ``fuser my.session``
|
|
||||||
to find out the process locking the file. As a last resort, you can
|
|
||||||
reboot your system.
|
|
||||||
|
|
||||||
If you really dislike SQLite, use a different session storage. There
|
|
||||||
is an entire section covering that at :ref:`sessions`.
|
|
||||||
|
|
||||||
Final Words
|
|
||||||
***********
|
|
||||||
|
|
||||||
Now you are aware of some common errors and use cases, this should help
|
|
||||||
you master your Telethon skills to get the most out of the library. Have
|
|
||||||
fun developing awesome things!
|
|
|
@ -1,65 +0,0 @@
|
||||||
.. _update-modes:
|
|
||||||
|
|
||||||
============
|
|
||||||
Update Modes
|
|
||||||
============
|
|
||||||
|
|
||||||
With ``asyncio``, the library has several tasks running in the background.
|
|
||||||
One task is used for sending requests, another task is used to receive them,
|
|
||||||
and a third one is used to handle updates.
|
|
||||||
|
|
||||||
To handle updates, you must keep your script running. You can do this in
|
|
||||||
several ways. For instance, if you are *not* running ``asyncio``'s event
|
|
||||||
loop, you should use `client.run_until_disconnected
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
...
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Behind the scenes, this method is ``await``'ing on the `client.disconnected
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>` property,
|
|
||||||
so the code above and the following are equivalent:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.disconnected
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
You could also run `client.disconnected
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnected>`
|
|
||||||
until it completed.
|
|
||||||
|
|
||||||
But if you don't want to ``await``, then you should know what you want
|
|
||||||
to be doing instead! What matters is that you shouldn't let your script
|
|
||||||
die. If you don't care about updates, you don't need any of this.
|
|
||||||
|
|
||||||
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
|
|
||||||
also be ran while the loop is running, so you can do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.run_until_disconnected()
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
|
@ -1,322 +0,0 @@
|
||||||
.. _asyncio-magic:
|
|
||||||
|
|
||||||
==================
|
|
||||||
Magic with asyncio
|
|
||||||
==================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
TL; DR; If you've upgraded to Telethon 1.0 from a previous version
|
|
||||||
**and you're not using events or updates**, add this line:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import telethon.sync
|
|
||||||
|
|
||||||
At the beginning of your main script and you will be good. If you **do**
|
|
||||||
use updates or events, keep reading, or ``pip install telethon-sync``, a
|
|
||||||
branch that mimics the ``asyncio`` code with threads and should work
|
|
||||||
under Python 3.4.
|
|
||||||
|
|
||||||
You might also want to check the :ref:`changelog`.
|
|
||||||
|
|
||||||
|
|
||||||
The sync module
|
|
||||||
***************
|
|
||||||
|
|
||||||
It's time to tell you the truth. The library has been doing magic behind
|
|
||||||
the scenes. We're sorry to tell you this, but at least it wasn't dark magic!
|
|
||||||
|
|
||||||
You may have noticed one of these lines across the documentation:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import sync
|
|
||||||
# or
|
|
||||||
import telethon.sync
|
|
||||||
|
|
||||||
Either of these lines will import the *magic* ``sync`` module. When you
|
|
||||||
import this module, you can suddenly use all the methods defined in the
|
|
||||||
:ref:`TelegramClient <telethon-client>` like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('me', 'Hello!')
|
|
||||||
|
|
||||||
for dialog in client.iter_dialogs():
|
|
||||||
print(dialog.title)
|
|
||||||
|
|
||||||
|
|
||||||
What happened behind the scenes is that all those methods, called *coroutines*,
|
|
||||||
were rewritten to be normal methods that will block (with some exceptions).
|
|
||||||
This means you can use the library without worrying about ``asyncio`` and
|
|
||||||
event loops.
|
|
||||||
|
|
||||||
However, this only works until you run the event loop yourself explicitly:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def coro():
|
|
||||||
client.send_message('me', 'Hello!') # <- no longer works!
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(coro())
|
|
||||||
|
|
||||||
|
|
||||||
What things will work and when?
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
You can use all the methods in the :ref:`TelegramClient <telethon-client>`
|
|
||||||
in a synchronous, blocking way without trouble, as long as you're not running
|
|
||||||
the loop as we saw above (the ``loop.run_until_complete(...)`` line runs "the
|
|
||||||
loop"). If you're running the loop, then *you* are the one responsible to
|
|
||||||
``await`` everything. So to fix the code above:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def coro():
|
|
||||||
await client.send_message('me', 'Hello!')
|
|
||||||
# ^ notice this new await
|
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(coro())
|
|
||||||
|
|
||||||
The library can only run the loop until the method completes if the loop
|
|
||||||
isn't already running, which is why the magic can't work if you run the
|
|
||||||
loop yourself.
|
|
||||||
|
|
||||||
**When you work with updates or events**, the loop needs to be
|
|
||||||
running one way or another (using `client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` runs the loop),
|
|
||||||
so your event handlers must be ``async def``.
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Turning your event handlers into ``async def`` is the biggest change
|
|
||||||
between Telethon pre-1.0 and 1.0, but updating will likely cause a
|
|
||||||
noticeable speed-up in your programs. Keep reading!
|
|
||||||
|
|
||||||
|
|
||||||
So in short, you can use **all** methods in the client with ``await`` or
|
|
||||||
without it if the loop isn't running:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('me', 'Hello!') # works
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.send_message('me', 'Hello!') # also works
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
When you work with updates, you should stick using the ``async def main``
|
|
||||||
way, since your event handlers will be ``async def`` too.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
There are two exceptions. Both `client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
|
|
||||||
`client.start() <telethon.client.auth.AuthMethods.start>` work in
|
|
||||||
and outside of ``async def`` for convenience without importing the
|
|
||||||
magic module. The rest of methods remain ``async`` unless you import it.
|
|
||||||
|
|
||||||
You can skip the rest if you already know how ``asyncio`` works and you
|
|
||||||
already understand what the magic does and how it works. Just remember
|
|
||||||
to ``await`` all your methods if you're inside an ``async def`` or are
|
|
||||||
using updates and you will be good.
|
|
||||||
|
|
||||||
|
|
||||||
Why asyncio?
|
|
||||||
************
|
|
||||||
|
|
||||||
Python's `asyncio <https://docs.python.org/3/library/asyncio.html>`_ is the
|
|
||||||
standard way to run asynchronous code from within Python. Since Python 3.5,
|
|
||||||
using ``async def`` and ``await`` became possible, and Python 3.6 further
|
|
||||||
improves what you can do with asynchronous code, although it's not the only
|
|
||||||
way (other projects like `Trio <https://github.com/python-trio>`_ also exist).
|
|
||||||
|
|
||||||
Telegram is a service where all API calls are executed in an asynchronous
|
|
||||||
way. You send your request, and eventually, Telegram will process it and
|
|
||||||
respond to it. It feels natural to make a library that also behaves this
|
|
||||||
way: you send a request, and you can ``await`` for its result.
|
|
||||||
|
|
||||||
Now that we know that Telegram's API follows an asynchronous model, you
|
|
||||||
should understand the benefits of developing a library that does the same,
|
|
||||||
it greatly simplifies the internal code and eases working with the API.
|
|
||||||
|
|
||||||
Using ``asyncio`` keeps a cleaner library that will be easier to understand,
|
|
||||||
develop, and that will be faster than using threads, which are harder to get
|
|
||||||
right and can cause issues. It also enables to use the powerful ``asyncio``
|
|
||||||
system such as futures, timeouts, cancellation, etc. in a natural way.
|
|
||||||
|
|
||||||
If you're still not convinced or you're just not ready for using ``asyncio``,
|
|
||||||
the library offers a synchronous interface without the need for all the
|
|
||||||
``async`` and ``await`` you would otherwise see. `Follow this link
|
|
||||||
<https://github.com/LonamiWebs/Telethon/tree/sync>`_ to find out more.
|
|
||||||
|
|
||||||
|
|
||||||
How do I get started?
|
|
||||||
*********************
|
|
||||||
|
|
||||||
To get started with ``asyncio``, all you need is to setup your main
|
|
||||||
``async def`` like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
pass # Your code goes here
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
You don't need to ``import telethon.sync`` if you're going to work this
|
|
||||||
way. This is the best way to work in real programs since the loop won't
|
|
||||||
be starting and ending all the time, but is a bit more annoying to setup.
|
|
||||||
|
|
||||||
Inside ``async def main()``, you can use the ``await`` keyword. Most
|
|
||||||
methods in the :ref:`TelegramClient <telethon-client>` are ``async def``.
|
|
||||||
You must ``await`` all ``async def``, also known as a *coroutines*:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
# client.start() is a coroutine (async def), it needs an await
|
|
||||||
await client.start()
|
|
||||||
|
|
||||||
# Sending a message also interacts with the API, and needs an await
|
|
||||||
await client.send_message('me', 'Hello myself!')
|
|
||||||
|
|
||||||
|
|
||||||
If you don't know anything else about ``asyncio``, this will be enough
|
|
||||||
to get you started. Once you're ready to learn more about it, you will
|
|
||||||
be able to use that power and everything you've learnt with Telethon.
|
|
||||||
Just remember that if you use ``await``, you need to be inside of an
|
|
||||||
``async def``.
|
|
||||||
|
|
||||||
Another way to use ``async def`` is to use ``loop.run_until_complete(f())``,
|
|
||||||
but the loop must not be running before.
|
|
||||||
|
|
||||||
If you want to handle updates (and don't let the script die), you must
|
|
||||||
`await client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>`
|
|
||||||
which is a property that you can wait on until you call
|
|
||||||
`await client.disconnect()
|
|
||||||
<telethon.client.telegrambaseclient.TelegramBaseClient.disconnect>`:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
print(event)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.start()
|
|
||||||
await client.run_until_disconnected()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
`client.run_until_disconnected()
|
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` and
|
|
||||||
`client.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` are special-cased and work
|
|
||||||
inside or outside ``async def`` for convenience, even without importing
|
|
||||||
the ``sync`` module, so you can also do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
print(event)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Which methods should I use and when?
|
|
||||||
************************************
|
|
||||||
|
|
||||||
Something to note is that you must always get an event loop if you
|
|
||||||
want to be able to make any API calls. This is done as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
The loop must be running, or things will never get sent.
|
|
||||||
Normally, you use ``run_until_complete``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def coroutine():
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
loop.run_until_complete(coroutine())
|
|
||||||
|
|
||||||
Note that ``asyncio.sleep`` is in itself a coroutine, so this will
|
|
||||||
work too:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
|
||||||
|
|
||||||
Generally, you make an ``async def main()`` if you need to ``await``
|
|
||||||
a lot of things, instead of typing ``run_until_complete`` all the time:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
message = await client.send_message('me', 'Hi')
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
await message.delete()
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
# vs
|
|
||||||
|
|
||||||
message = loop.run_until_complete(client.send_message('me', 'Hi'))
|
|
||||||
loop.run_until_complete(asyncio.sleep(1))
|
|
||||||
loop.run_until_complete(message.delete())
|
|
||||||
|
|
||||||
You can see that the first version has more lines, but you had to type
|
|
||||||
a lot less. You can also rename the run method to something shorter:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Note no parenthesis (), we're not running it, just copying the method
|
|
||||||
rc = loop.run_until_complete
|
|
||||||
message = rc(client.send_message('me', 'Hi'))
|
|
||||||
rc(asyncio.sleep(1))
|
|
||||||
rc(message.delete())
|
|
||||||
|
|
||||||
The documentation generally runs the loop until complete behind the
|
|
||||||
scenes if you've imported the magic ``sync`` module, but if you haven't,
|
|
||||||
you need to run the loop yourself. We recommend that you use the
|
|
||||||
``async def main()`` method to do all your work with ``await``.
|
|
||||||
It's the easiest and most performant thing to do.
|
|
||||||
|
|
||||||
|
|
||||||
More resources to learn asyncio
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
If you would like to learn a bit more about why ``asyncio`` is something
|
|
||||||
you should learn, `check out my blog post
|
|
||||||
<https://lonamiwebs.github.io/blog/asyncio/>`_ that goes into more detail.
|
|
|
@ -1,263 +0,0 @@
|
||||||
.. _creating-a-client:
|
|
||||||
|
|
||||||
=================
|
|
||||||
Creating a Client
|
|
||||||
=================
|
|
||||||
|
|
||||||
|
|
||||||
Before working with Telegram's API, you need to get your own API ID and hash:
|
|
||||||
|
|
||||||
1. Follow `this link <https://my.telegram.org/>`_ and login with your
|
|
||||||
phone number.
|
|
||||||
|
|
||||||
2. Click under API Development tools.
|
|
||||||
|
|
||||||
3. A *Create new application* window will appear. Fill in your application
|
|
||||||
details. There is no need to enter any *URL*, and only the first two
|
|
||||||
fields (*App title* and *Short name*) can currently be changed later.
|
|
||||||
|
|
||||||
4. Click on *Create application* at the end. Remember that your
|
|
||||||
**API hash is secret** and Telegram won't let you revoke it.
|
|
||||||
Don't post it anywhere!
|
|
||||||
|
|
||||||
Once that's ready, the next step is to create a ``TelegramClient``.
|
|
||||||
This class will be your main interface with Telegram's API, and creating
|
|
||||||
one is very simple:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
|
|
||||||
# Use your own values here
|
|
||||||
api_id = 12345
|
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
|
||||||
|
|
||||||
client = TelegramClient('some_name', api_id, api_hash)
|
|
||||||
|
|
||||||
|
|
||||||
Note that ``'some_name'`` will be used to save your session (persistent
|
|
||||||
information such as access key and others) as ``'some_name.session'`` in
|
|
||||||
your disk. This is by default a database file using Python's ``sqlite3``.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
It's important that the library always accesses the same session file so
|
|
||||||
that you don't need to re-send the code over and over again. By default it
|
|
||||||
creates the file in your working directory, but absolute paths work too.
|
|
||||||
|
|
||||||
|
|
||||||
Once you have a client ready, simply `.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` it:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
|
|
||||||
This line connects to Telegram, checks whether the current user is
|
|
||||||
authorized or not, and if it's not, it begins the login or sign up process.
|
|
||||||
|
|
||||||
When you're done with your code, you should always disconnect:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
try:
|
|
||||||
client.start()
|
|
||||||
... # your code here
|
|
||||||
finally:
|
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
You can also use a ``with`` block to achieve the same effect:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client = TelegramClient(...)
|
|
||||||
with client:
|
|
||||||
... # your code here
|
|
||||||
|
|
||||||
# or
|
|
||||||
with TelegramClient(...) as client:
|
|
||||||
... # your code here
|
|
||||||
|
|
||||||
|
|
||||||
Wrapping it all together:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
with TelegramClient('session_name', api_id, api_hash) as client:
|
|
||||||
... # your code
|
|
||||||
|
|
||||||
Just two setup lines.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
Please note that if you fail to login around 5 times (or change the first
|
|
||||||
parameter of the :ref:`TelegramClient <telethon-client>`, which is the session
|
|
||||||
name) you will receive a ``FloodWaitError`` of around 22 hours, so be
|
|
||||||
careful not to mess this up! This shouldn't happen if you're doing things
|
|
||||||
as explained, though.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
If you want to use a **proxy**, you have to `install PySocks`__
|
|
||||||
(via pip or manual) and then set the appropriated parameters:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import socks
|
|
||||||
client = TelegramClient('session_id',
|
|
||||||
api_id=12345, api_hash='0123456789abcdef0123456789abcdef',
|
|
||||||
proxy=(socks.SOCKS5, 'localhost', 4444)
|
|
||||||
)
|
|
||||||
|
|
||||||
The ``proxy=`` argument should be a tuple, a list or a dict,
|
|
||||||
consisting of parameters described `here`__.
|
|
||||||
|
|
||||||
|
|
||||||
Manually Signing In
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Skip this unless you need more control when connecting.
|
|
||||||
|
|
||||||
If you need more control, you can replicate what `client.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` is doing behind the scenes
|
|
||||||
for your convenience. The first step is to connect to the servers:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.connect()
|
|
||||||
|
|
||||||
You may or may not be authorized yet. You must be authorized
|
|
||||||
before you're able to send any request:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.is_user_authorized() # Returns True if you can send requests
|
|
||||||
|
|
||||||
If you're not authorized, you need to `.sign_in
|
|
||||||
<telethon.client.auth.AuthMethods.sign_in>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
phone_number = '+34600000000'
|
|
||||||
client.send_code_request(phone_number)
|
|
||||||
myself = client.sign_in(phone_number, input('Enter code: '))
|
|
||||||
# If .sign_in raises PhoneNumberUnoccupiedError, use .sign_up instead
|
|
||||||
# If .sign_in raises SessionPasswordNeeded error, call .sign_in(password=...)
|
|
||||||
# You can import both exceptions from telethon.errors.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you send the code that Telegram sent you over the app through the
|
|
||||||
app itself, it will expire immediately. You can still send the code
|
|
||||||
through the app by "obfuscating" it (maybe add a magic constant, like
|
|
||||||
``12345``, and then subtract it to get the real code back) or any other
|
|
||||||
technique.
|
|
||||||
|
|
||||||
``myself`` is your Telegram user. You can view all the information about
|
|
||||||
yourself by doing ``print(myself.stringify())``. You're now ready to use
|
|
||||||
the client as you wish! Remember that any object returned by the API has
|
|
||||||
mentioned ``.stringify()`` method, and printing these might prove useful.
|
|
||||||
|
|
||||||
As a full example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
client = TelegramClient('session_name', api_id, api_hash)
|
|
||||||
|
|
||||||
client.connect()
|
|
||||||
if not client.is_user_authorized():
|
|
||||||
client.send_code_request(phone_number)
|
|
||||||
me = client.sign_in(phone_number, input('Enter code: '))
|
|
||||||
|
|
||||||
|
|
||||||
Remember that this is the manual process and it's so much easier
|
|
||||||
to use the code snippets shown at the beginning of the page.
|
|
||||||
|
|
||||||
The code shown is just what `.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` will be doing behind the scenes
|
|
||||||
(with a few extra checks), so that you know how to sign in case you want
|
|
||||||
to avoid using ``input()`` (the default) for whatever reason. If no phone
|
|
||||||
or bot token is provided, you will be asked one through ``input()``. The
|
|
||||||
method also accepts a ``phone=`` and ``bot_token`` parameters.
|
|
||||||
|
|
||||||
You can use either, as both will work. Determining which
|
|
||||||
is just a matter of taste, and how much control you need.
|
|
||||||
|
|
||||||
Remember that you can get yourself at any time with `client.get_me()
|
|
||||||
<telethon.client.users.UserMethods.get_me>`.
|
|
||||||
|
|
||||||
|
|
||||||
Two Factor Authorization (2FA)
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
If you have Two Factor Authorization (from now on, 2FA) enabled on your
|
|
||||||
account, calling `.sign_in()
|
|
||||||
<telethon.client.auth.AuthMethods.sign_in>` will raise a
|
|
||||||
``SessionPasswordNeededError``. When this happens, just use the method
|
|
||||||
again with a ``password=``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import getpass
|
|
||||||
from telethon.errors import SessionPasswordNeededError
|
|
||||||
|
|
||||||
client.sign_in(phone)
|
|
||||||
try:
|
|
||||||
client.sign_in(code=input('Enter code: '))
|
|
||||||
except SessionPasswordNeededError:
|
|
||||||
client.sign_in(password=getpass.getpass())
|
|
||||||
|
|
||||||
|
|
||||||
The mentioned `.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>` method will handle this for you as
|
|
||||||
well, but you must set the ``password=`` parameter beforehand (it won't be
|
|
||||||
asked).
|
|
||||||
|
|
||||||
If you don't have 2FA enabled, but you would like to do so through the
|
|
||||||
library, use `client.edit_2fa()
|
|
||||||
<telethon.client.auth.AuthMethods.edit_2fa>`.
|
|
||||||
|
|
||||||
Be sure to know what you're doing when using this function and
|
|
||||||
you won't run into any problems. Take note that if you want to
|
|
||||||
set only the email/hint and leave the current password unchanged,
|
|
||||||
you need to "redo" the 2fa.
|
|
||||||
|
|
||||||
See the examples below:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.errors import EmailUnconfirmedError
|
|
||||||
|
|
||||||
# Sets 2FA password for first time:
|
|
||||||
client.edit_2fa(new_password='supersecurepassword')
|
|
||||||
|
|
||||||
# Changes password:
|
|
||||||
client.edit_2fa(current_password='supersecurepassword',
|
|
||||||
new_password='changedmymind')
|
|
||||||
|
|
||||||
# Clears current password (i.e. removes 2FA):
|
|
||||||
client.edit_2fa(current_password='changedmymind', new_password=None)
|
|
||||||
|
|
||||||
# Sets new password with recovery email:
|
|
||||||
try:
|
|
||||||
client.edit_2fa(new_password='memes and dreams',
|
|
||||||
email='JohnSmith@example.com')
|
|
||||||
# Raises error (you need to check your email to complete 2FA setup.)
|
|
||||||
except EmailUnconfirmedError:
|
|
||||||
# You can put email checking code here if desired.
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Also take note that unless you remove 2FA or explicitly
|
|
||||||
# give email parameter again it will keep the last used setting
|
|
||||||
|
|
||||||
# Set hint after already setting password:
|
|
||||||
client.edit_2fa(current_password='memes and dreams',
|
|
||||||
new_password='memes and dreams',
|
|
||||||
hint='It keeps you alive')
|
|
||||||
|
|
||||||
__ https://github.com/Anorov/PySocks#installation
|
|
||||||
__ https://github.com/Anorov/PySocks#usage-1
|
|
|
@ -1,179 +0,0 @@
|
||||||
.. _entities:
|
|
||||||
|
|
||||||
=========================
|
|
||||||
Users, Chats and Channels
|
|
||||||
=========================
|
|
||||||
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
************
|
|
||||||
|
|
||||||
The library widely uses the concept of "entities". An entity will refer
|
|
||||||
to any :tl:`User`, :tl:`Chat` or :tl:`Channel` object that the API may return
|
|
||||||
in response to certain methods, such as :tl:`GetUsersRequest`.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
When something "entity-like" is required, it means that you need to
|
|
||||||
provide something that can be turned into an entity. These things include,
|
|
||||||
but are not limited to, usernames, exact titles, IDs, :tl:`Peer` objects,
|
|
||||||
or even entire :tl:`User`, :tl:`Chat` and :tl:`Channel` objects and even
|
|
||||||
phone numbers from people you have in your contacts.
|
|
||||||
|
|
||||||
To "encounter" an ID, you would have to "find it" like you would in the
|
|
||||||
normal app. If the peer is in your dialogs, you would need to
|
|
||||||
`client.get_dialogs() <telethon.client.dialogs.DialogMethods.get_dialogs>`.
|
|
||||||
If the peer is someone in a group, you would similarly
|
|
||||||
`client.get_participants(group) <telethon.client.chats.ChatMethods.get_participants>`.
|
|
||||||
|
|
||||||
Once you have encountered an ID, the library will (by default) have saved
|
|
||||||
their ``access_hash`` for you, which is needed to invoke most methods.
|
|
||||||
This is why sometimes you might encounter this error when working with
|
|
||||||
the library. You should ``except ValueError`` and run code that you know
|
|
||||||
should work to find the entity.
|
|
||||||
|
|
||||||
|
|
||||||
Getting entities
|
|
||||||
****************
|
|
||||||
|
|
||||||
Through the use of the :ref:`sessions`, the library will automatically
|
|
||||||
remember the ID and hash pair, along with some extra information, so
|
|
||||||
you're able to just do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Dialogs are the "conversations you have open".
|
|
||||||
# This method returns a list of Dialog, which
|
|
||||||
# has the .entity attribute and other information.
|
|
||||||
dialogs = client.get_dialogs()
|
|
||||||
|
|
||||||
# All of these work and do the same.
|
|
||||||
lonami = client.get_entity('lonami')
|
|
||||||
lonami = client.get_entity('t.me/lonami')
|
|
||||||
lonami = client.get_entity('https://telegram.dog/lonami')
|
|
||||||
|
|
||||||
# Other kind of entities.
|
|
||||||
channel = client.get_entity('telegram.me/joinchat/AAAAAEkk2WdoDrB4-Q8-gg')
|
|
||||||
contact = client.get_entity('+34xxxxxxxxx')
|
|
||||||
friend = client.get_entity(friend_id)
|
|
||||||
|
|
||||||
# Getting entities through their ID (User, Chat or Channel)
|
|
||||||
entity = client.get_entity(some_id)
|
|
||||||
|
|
||||||
# You can be more explicit about the type for said ID by wrapping
|
|
||||||
# it inside a Peer instance. This is recommended but not necessary.
|
|
||||||
from telethon.tl.types import PeerUser, PeerChat, PeerChannel
|
|
||||||
|
|
||||||
my_user = client.get_entity(PeerUser(some_id))
|
|
||||||
my_chat = client.get_entity(PeerChat(some_id))
|
|
||||||
my_channel = client.get_entity(PeerChannel(some_id))
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
You **don't** need to get the entity before using it! Just let
|
|
||||||
the library do its job. Use the phone, username, ID or input
|
|
||||||
entity (preferred but not necessary), whatever you already have.
|
|
||||||
|
|
||||||
All methods in the :ref:`telegram-client` call `.get_input_entity()
|
|
||||||
<telethon.client.users.UserMethods.get_input_entity>` prior
|
|
||||||
to sending the requst to save you from the hassle of doing so manually.
|
|
||||||
That way, convenience calls such as `client.send_message('lonami', 'hi!')
|
|
||||||
<telethon.client.messages.MessageMethods.send_message>`
|
|
||||||
become possible.
|
|
||||||
|
|
||||||
Every entity the library encounters (in any response to any call) will by
|
|
||||||
default be cached in the ``.session`` file (an SQLite database), to avoid
|
|
||||||
performing unnecessary API calls. If the entity cannot be found, additonal
|
|
||||||
calls like :tl:`ResolveUsernameRequest` or :tl:`GetContactsRequest` may be
|
|
||||||
made to obtain the required information.
|
|
||||||
|
|
||||||
|
|
||||||
Entities vs. Input Entities
|
|
||||||
***************************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Don't worry if you don't understand this section, just remember some
|
|
||||||
of the details listed here are important. When you're calling a method,
|
|
||||||
don't call `client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
|
||||||
beforehand, just use the username or phone, or the entity retrieved by
|
|
||||||
other means like `client.get_dialogs()
|
|
||||||
<telethon.client.dialogs.DialogMethods.get_dialogs>`.
|
|
||||||
|
|
||||||
On top of the normal types, the API also make use of what they call their
|
|
||||||
``Input*`` versions of objects. The input version of an entity (e.g.
|
|
||||||
:tl:`InputPeerUser`, :tl:`InputChat`, etc.) only contains the minimum
|
|
||||||
information that's required from Telegram to be able to identify
|
|
||||||
who you're referring to: a :tl:`Peer`'s **ID** and **hash**. They
|
|
||||||
are named like this because they are input parameters in the requests.
|
|
||||||
|
|
||||||
Entities' ID are the same for all user and bot accounts, however, the access
|
|
||||||
hash is **different for each account**, so trying to reuse the access hash
|
|
||||||
from one account in another will **not** work.
|
|
||||||
|
|
||||||
Sometimes, Telegram only needs to indicate the type of the entity along
|
|
||||||
with their ID. For this purpose, :tl:`Peer` versions of the entities also
|
|
||||||
exist, which just have the ID. You cannot get the hash out of them since
|
|
||||||
you should not be needing it. The library probably has cached it before.
|
|
||||||
|
|
||||||
Peers are enough to identify an entity, but they are not enough to make
|
|
||||||
a request with them use them. You need to know their hash before you can
|
|
||||||
"use them", and to know the hash you need to "encounter" them, let it
|
|
||||||
be in your dialogs, participants, message forwards, etc.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
You *can* use peers with the library. Behind the scenes, they are
|
|
||||||
replaced with the input variant. Peers "aren't enough" on their own
|
|
||||||
but the library will do some more work to use the right type.
|
|
||||||
|
|
||||||
As we just mentioned, API calls don't need to know the whole information
|
|
||||||
about the entities, only their ID and hash. For this reason, another method,
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
|
||||||
is available. This will always use the cache while possible, making zero API
|
|
||||||
calls most of the time. When a request is made, if you provided the full
|
|
||||||
entity, e.g. an :tl:`User`, the library will convert it to the required
|
|
||||||
:tl:`InputPeer` automatically for you.
|
|
||||||
|
|
||||||
**You should always favour**
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
|
||||||
**over**
|
|
||||||
`client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
|
||||||
for this reason! Calling the latter will always make an API call to get
|
|
||||||
the most recent information about said entity, but invoking requests don't
|
|
||||||
need this information, just the :tl:`InputPeer`. Only use
|
|
||||||
`client.get_entity() <telethon.client.users.UserMethods.get_entity>`
|
|
||||||
if you need to get actual information, like the username, name, title, etc.
|
|
||||||
of the entity.
|
|
||||||
|
|
||||||
To further simplify the workflow, since the version ``0.16.2`` of the
|
|
||||||
library, the raw requests you make to the API are also able to call
|
|
||||||
`client.get_input_entity() <telethon.client.users.UserMethods.get_input_entity>`
|
|
||||||
wherever needed, so you can even do things like:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client(SendMessageRequest('username', 'hello'))
|
|
||||||
|
|
||||||
The library will call the ``.resolve()`` method of the request, which will
|
|
||||||
resolve ``'username'`` with the appropriated :tl:`InputPeer`. Don't worry if
|
|
||||||
you don't get this yet, but remember some of the details here are important.
|
|
||||||
|
|
||||||
|
|
||||||
Full entities
|
|
||||||
*************
|
|
||||||
|
|
||||||
In addition to :tl:`PeerUser`, :tl:`InputPeerUser`, :tl:`User` (and its
|
|
||||||
variants for chats and channels), there is also the concept of :tl:`UserFull`.
|
|
||||||
|
|
||||||
This full variant has additional information such as whether the user is
|
|
||||||
blocked, its notification settings, the bio or about of the user, etc.
|
|
||||||
|
|
||||||
There is also :tl:`messages.ChatFull` which is the equivalent of full entities
|
|
||||||
for chats and channels, with also the about section of the channel. Note that
|
|
||||||
the ``users`` field only contains bots for the channel (so that clients can
|
|
||||||
suggest commands to use).
|
|
||||||
|
|
||||||
You can get both of these by invoking :tl:`GetFullUser`, :tl:`GetFullChat`
|
|
||||||
and :tl:`GetFullChannel` respectively.
|
|
|
@ -1,93 +0,0 @@
|
||||||
.. _getting-started:
|
|
||||||
|
|
||||||
|
|
||||||
===============
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
|
|
||||||
Simple Installation
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon
|
|
||||||
|
|
||||||
**More details**: :ref:`installation`
|
|
||||||
|
|
||||||
|
|
||||||
Creating a client
|
|
||||||
*****************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, sync
|
|
||||||
|
|
||||||
# These example values won't work. You must get your own api_id and
|
|
||||||
# api_hash from https://my.telegram.org, under API Development.
|
|
||||||
api_id = 12345
|
|
||||||
api_hash = '0123456789abcdef0123456789abcdef'
|
|
||||||
|
|
||||||
client = TelegramClient('session_name', api_id, api_hash).start()
|
|
||||||
|
|
||||||
**More details**: :ref:`creating-a-client`
|
|
||||||
|
|
||||||
|
|
||||||
Basic Usage
|
|
||||||
***********
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Getting information about yourself
|
|
||||||
me = client.get_me()
|
|
||||||
print(me.stringify())
|
|
||||||
|
|
||||||
# Sending a message (you can use 'me' or 'self' to message yourself)
|
|
||||||
client.send_message('username', 'Hello World from Telethon!')
|
|
||||||
|
|
||||||
# Sending a file
|
|
||||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
|
||||||
|
|
||||||
# Retrieving messages from a chat
|
|
||||||
from telethon import utils
|
|
||||||
for message in client.iter_messages('username', limit=10):
|
|
||||||
print(utils.get_display_name(message.sender), message.message)
|
|
||||||
|
|
||||||
# Listing all the dialogs (conversations you have open)
|
|
||||||
for dialog in client.get_dialogs(limit=10):
|
|
||||||
print(dialog.name, dialog.draft.text)
|
|
||||||
|
|
||||||
# Downloading profile photos (default path is the working directory)
|
|
||||||
client.download_profile_photo('username')
|
|
||||||
|
|
||||||
# Once you have a message with .media (if message.media)
|
|
||||||
# you can download it using client.download_media(),
|
|
||||||
# or even using message.download_media():
|
|
||||||
messages = client.get_messages('username')
|
|
||||||
messages[0].download_media()
|
|
||||||
|
|
||||||
**More details**: :ref:`telegram-client`
|
|
||||||
|
|
||||||
See :ref:`telethon-client` for all available friendly methods.
|
|
||||||
|
|
||||||
|
|
||||||
Handling Updates
|
|
||||||
****************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import events
|
|
||||||
|
|
||||||
@client.on(events.NewMessage(incoming=True, pattern='(?i)hi'))
|
|
||||||
async def handler(event):
|
|
||||||
await event.reply('Hello!')
|
|
||||||
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
**More details**: :ref:`working-with-updates`
|
|
||||||
|
|
||||||
|
|
||||||
----------
|
|
||||||
|
|
||||||
You can continue by clicking on the "More details" link below each
|
|
||||||
snippet of code or the "Next" button at the bottom of the page.
|
|
|
@ -1,97 +0,0 @@
|
||||||
.. _installation:
|
|
||||||
|
|
||||||
============
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
|
|
||||||
Automatic Installation
|
|
||||||
**********************
|
|
||||||
|
|
||||||
To install Telethon, simply do:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon
|
|
||||||
|
|
||||||
Needless to say, you must have Python 3 and PyPi installed in your system.
|
|
||||||
See https://python.org and https://pypi.python.org/pypi/pip for more.
|
|
||||||
|
|
||||||
If you already have the library installed, upgrade with:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install --upgrade telethon
|
|
||||||
|
|
||||||
You can also install the library directly from GitHub or a fork:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
# pip3 install git+https://github.com/LonamiWebs/Telethon.git
|
|
||||||
or
|
|
||||||
$ git clone https://github.com/LonamiWebs/Telethon.git
|
|
||||||
$ cd Telethon/
|
|
||||||
# pip install -Ue .
|
|
||||||
|
|
||||||
If you don't have root access, simply pass the ``--user`` flag to the pip
|
|
||||||
command. If you want to install a specific branch, append ``@branch`` to
|
|
||||||
the end of the first install command.
|
|
||||||
|
|
||||||
By default the library will use a pure Python implementation for encryption,
|
|
||||||
which can be really slow when uploading or downloading files. If you don't
|
|
||||||
mind using a C extension, install `cryptg <https://github.com/Lonami/cryptg>`__
|
|
||||||
via ``pip`` or as an extra:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install telethon[cryptg]
|
|
||||||
|
|
||||||
|
|
||||||
Manual Installation
|
|
||||||
*******************
|
|
||||||
|
|
||||||
1. Install the required ``pyaes`` (`GitHub`__ | `PyPi`__) and
|
|
||||||
``rsa`` (`GitHub`__ | `PyPi`__) modules:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
pip3 install pyaes rsa
|
|
||||||
|
|
||||||
2. Clone Telethon's GitHub repository:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
git clone https://github.com/LonamiWebs/Telethon.git
|
|
||||||
|
|
||||||
3. Enter the cloned repository:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
cd Telethon
|
|
||||||
|
|
||||||
4. Run the code generator:
|
|
||||||
|
|
||||||
.. code-block:: sh
|
|
||||||
|
|
||||||
python3 setup.py gen
|
|
||||||
|
|
||||||
5. Done!
|
|
||||||
|
|
||||||
To generate the `method documentation`__, ``python3 setup.py gen docs``.
|
|
||||||
|
|
||||||
|
|
||||||
Optional dependencies
|
|
||||||
*********************
|
|
||||||
|
|
||||||
If the `cryptg`__ is installed, you might notice a speed-up in the download
|
|
||||||
and upload speed, since these are the most cryptographic-heavy part of the
|
|
||||||
library and said module is a C extension. Otherwise, the ``pyaes`` fallback
|
|
||||||
will be used.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://github.com/ricmoo/pyaes
|
|
||||||
__ https://pypi.python.org/pypi/pyaes
|
|
||||||
__ https://github.com/sybrenstuvel/python-rsa
|
|
||||||
__ https://pypi.python.org/pypi/rsa/3.4.2
|
|
||||||
__ https://lonamiwebs.github.io/Telethon
|
|
||||||
__ https://github.com/Lonami/cryptg
|
|
|
@ -1,108 +0,0 @@
|
||||||
.. _telegram-client:
|
|
||||||
|
|
||||||
==============
|
|
||||||
TelegramClient
|
|
||||||
==============
|
|
||||||
|
|
||||||
|
|
||||||
Introduction
|
|
||||||
************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Make sure to use the friendly methods described in :ref:`telethon-client`!
|
|
||||||
This section is just an introduction to using the client, but all the
|
|
||||||
available methods are in the :ref:`telethon-client` reference, including
|
|
||||||
detailed descriptions to what they do.
|
|
||||||
|
|
||||||
The :ref:`TelegramClient <telethon-client>` is the
|
|
||||||
central class of the library, the one you will be using most of the time. For
|
|
||||||
this reason, it's important to know what it offers.
|
|
||||||
|
|
||||||
Since we're working with Python, one must not forget that we can do
|
|
||||||
``help(client)`` or ``help(TelegramClient)`` at any time for a more
|
|
||||||
detailed description and a list of all the available methods. Calling
|
|
||||||
``help()`` from an interactive Python session will always list all the
|
|
||||||
methods for any object, even yours!
|
|
||||||
|
|
||||||
Interacting with the Telegram API is done through sending **requests**,
|
|
||||||
this is, any "method" listed on the API. There are a few methods (and
|
|
||||||
growing!) on the :ref:`TelegramClient <telethon-client>` class that abstract
|
|
||||||
you from the need of manually importing the requests you need.
|
|
||||||
|
|
||||||
For instance, retrieving your own user can be done in a single line
|
|
||||||
(assuming you have ``from telethon import sync`` or ``import telethon.sync``):
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
myself = client.get_me()
|
|
||||||
|
|
||||||
Internally, this method has sent a request to Telegram, who replied with
|
|
||||||
the information about your own user, and then the desired information
|
|
||||||
was extracted from their response.
|
|
||||||
|
|
||||||
If you want to retrieve any other user, chat or channel (channels are a
|
|
||||||
special subset of chats), you want to retrieve their "entity". This is
|
|
||||||
how the library refers to either of these:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# The method will infer that you've passed a username
|
|
||||||
# It also accepts phone numbers, and will get the user
|
|
||||||
# from your contact list.
|
|
||||||
lonami = client.get_entity('lonami')
|
|
||||||
|
|
||||||
The so called "entities" are another important whole concept on its own,
|
|
||||||
but for now you don't need to worry about it. Simply know that they are
|
|
||||||
a good way to get information about a user, chat or channel.
|
|
||||||
|
|
||||||
Many other common methods for quick scripts are also available:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Note that you can use 'me' or 'self' to message yourself
|
|
||||||
client.send_message('username', 'Hello World from Telethon!')
|
|
||||||
|
|
||||||
# .send_message's parse mode defaults to markdown, so you
|
|
||||||
# can use **bold**, __italics__, [links](https://example.com), `code`,
|
|
||||||
# and even [mentions](@username)/[mentions](tg://user?id=123456789)
|
|
||||||
client.send_message('username', '**Using** __markdown__ `too`!')
|
|
||||||
|
|
||||||
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
|
|
||||||
|
|
||||||
# The utils package has some goodies, like .get_display_name()
|
|
||||||
from telethon import utils
|
|
||||||
for message in client.iter_messages('username', limit=10):
|
|
||||||
print(utils.get_display_name(message.sender), message.message)
|
|
||||||
|
|
||||||
# Dialogs are the conversations you have open
|
|
||||||
for dialog in client.get_dialogs(limit=10):
|
|
||||||
print(dialog.name, dialog.draft.text)
|
|
||||||
|
|
||||||
# Default path is the working directory
|
|
||||||
client.download_profile_photo('username')
|
|
||||||
|
|
||||||
# Call .disconnect() when you're done
|
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
Remember that you can call ``.stringify()`` to any object Telegram returns
|
|
||||||
to pretty print it. Calling ``str(result)`` does the same operation, but on
|
|
||||||
a single line.
|
|
||||||
|
|
||||||
|
|
||||||
Available methods
|
|
||||||
*****************
|
|
||||||
|
|
||||||
The :ref:`reference <telethon-package>` lists all the "handy" methods
|
|
||||||
available for you to use in the :ref:`TelegramClient <telethon-client>` class.
|
|
||||||
These are simply wrappers around the "raw" Telegram API, making it much more
|
|
||||||
manageable and easier to work with.
|
|
||||||
|
|
||||||
Please refer to :ref:`accessing-the-full-api` if these aren't enough,
|
|
||||||
and don't be afraid to read the source code of the InteractiveTelegramClient_
|
|
||||||
or even the TelegramClient_ itself to learn how it works.
|
|
||||||
|
|
||||||
See the mentioned :ref:`telethon-client` to find the available methods.
|
|
||||||
|
|
||||||
.. _InteractiveTelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon_examples/interactive_telegram_client.py
|
|
||||||
.. _TelegramClient: https://github.com/LonamiWebs/Telethon/blob/master/telethon/telegram_client.py
|
|
|
@ -1,338 +0,0 @@
|
||||||
.. _working-with-updates:
|
|
||||||
|
|
||||||
====================
|
|
||||||
Working with Updates
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Make sure you have read at least the first part of :ref:`asyncio-magic`
|
|
||||||
before working with updates. **This is a big change from Telethon pre-1.0
|
|
||||||
and 1.0, and your old handlers won't work with this version**.
|
|
||||||
|
|
||||||
To port your code to the new version, you should just prefix all your
|
|
||||||
event handlers with ``async`` and ``await`` everything that makes an
|
|
||||||
API call, such as replying, deleting messages, etc.
|
|
||||||
|
|
||||||
|
|
||||||
The library comes with the `telethon.events` module. *Events* are an abstraction
|
|
||||||
over what Telegram calls `updates`__, and are meant to ease simple and common
|
|
||||||
usage when dealing with them, since there are many updates. If you're looking
|
|
||||||
for the method reference, check :ref:`telethon-events-package`, otherwise,
|
|
||||||
let's dive in!
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
The library logs by default no output, and any exception that occurs
|
|
||||||
inside your handlers will be "hidden" from you to prevent the thread
|
|
||||||
from terminating (so it can still deliver events). You should enable
|
|
||||||
logging when working with events, at least the error level, to see if
|
|
||||||
this is happening so you can debug the error.
|
|
||||||
|
|
||||||
**When using updates, please enable logging:**
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
Getting Started
|
|
||||||
***************
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
client = TelegramClient('name', api_id, api_hash)
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def my_event_handler(event):
|
|
||||||
if 'hello' in event.raw_text:
|
|
||||||
await event.reply('hi!')
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Not much, but there might be some things unclear. What does this code do?
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
client = TelegramClient('name', api_id, api_hash)
|
|
||||||
|
|
||||||
|
|
||||||
This is normal creation (of course, pass session name, API ID and hash).
|
|
||||||
Nothing we don't know already.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
|
|
||||||
|
|
||||||
This Python decorator will attach itself to the ``my_event_handler``
|
|
||||||
definition, and basically means that *on* a `NewMessage
|
|
||||||
<telethon.events.newmessage.NewMessage>` *event*,
|
|
||||||
the callback function you're about to define will be called:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def my_event_handler(event):
|
|
||||||
if 'hello' in event.raw_text:
|
|
||||||
await event.reply('hi!')
|
|
||||||
|
|
||||||
|
|
||||||
If a `NewMessage
|
|
||||||
<telethon.events.newmessage.NewMessage>` event occurs,
|
|
||||||
and ``'hello'`` is in the text of the message, we `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` to the event
|
|
||||||
with a ``'hi!'`` message.
|
|
||||||
|
|
||||||
Do you notice anything different? Yes! Event handlers **must** be ``async``
|
|
||||||
for them to work, and **every method using the network** needs to have an
|
|
||||||
``await``, otherwise, Python's ``asyncio`` will tell you that you forgot
|
|
||||||
to do so, so you can easily add it.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Finally, this tells the client that we're done with our code. We run the
|
|
||||||
``asyncio`` loop until the client starts (this is done behind the scenes,
|
|
||||||
since the method is so common), and then we run it again until we are
|
|
||||||
disconnected. Of course, you can do other things instead of running
|
|
||||||
until disconnected. For this refer to :ref:`update-modes`.
|
|
||||||
|
|
||||||
|
|
||||||
More on events
|
|
||||||
**************
|
|
||||||
|
|
||||||
The `NewMessage <telethon.events.newmessage.NewMessage>` event has much
|
|
||||||
more than what was shown. You can access the `.sender
|
|
||||||
<telethon.tl.custom.message.Message.sender>` of the message
|
|
||||||
through that member, or even see if the message had `.media
|
|
||||||
<telethon.tl.custom.message.Message.media>`, a `.photo
|
|
||||||
<telethon.tl.custom.message.Message.photo>` or a `.document
|
|
||||||
<telethon.tl.custom.message.Message.document>` (which you
|
|
||||||
could download with for example `client.download_media(event.photo)
|
|
||||||
<telethon.client.downloads.DownloadMethods.download_media>`.
|
|
||||||
|
|
||||||
If you don't want to `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` as a reply,
|
|
||||||
you can use the `.respond() <telethon.tl.custom.message.Message.respond>`
|
|
||||||
method instead. Of course, there are more events such as `ChatAction
|
|
||||||
<telethon.events.chataction.ChatAction>` or `UserUpdate
|
|
||||||
<telethon.events.userupdate.UserUpdate>`, and they're all
|
|
||||||
used in the same way. Simply add the `@client.on(events.XYZ)
|
|
||||||
<telethon.client.updates.UpdateMethods.on>` decorator on the top
|
|
||||||
of your handler and you're done! The event that will be passed always
|
|
||||||
is of type ``XYZ.Event`` (for instance, `NewMessage.Event
|
|
||||||
<telethon.events.newmessage.NewMessage.Event>`), except for the `Raw
|
|
||||||
<telethon.events.raw.Raw>` event which just passes the :tl:`Update` object.
|
|
||||||
|
|
||||||
Note that `.reply()
|
|
||||||
<telethon.tl.custom.message.Message.reply>` and `.respond()
|
|
||||||
<telethon.tl.custom.message.Message.respond>` are just wrappers around the
|
|
||||||
`client.send_message() <telethon.client.messages.MessageMethods.send_message>`
|
|
||||||
method which supports the ``file=`` parameter.
|
|
||||||
This means you can reply with a photo if you do `event.reply(file=photo)
|
|
||||||
<telethon.tl.custom.message.Message.reply>`.
|
|
||||||
|
|
||||||
You can put the same event on many handlers, and even different events on
|
|
||||||
the same handler. You can also have a handler work on only specific chats,
|
|
||||||
for example:
|
|
||||||
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
# Either a single item or a list of them will work for the chats.
|
|
||||||
# You can also use the IDs, Peers, or even User/Chat/Channel objects.
|
|
||||||
@client.on(events.NewMessage(chats=('TelethonChat', 'TelethonOffTopic')))
|
|
||||||
async def normal_handler(event):
|
|
||||||
if 'roll' in event.raw_text:
|
|
||||||
await event.reply(str(random.randint(1, 6)))
|
|
||||||
|
|
||||||
|
|
||||||
# Similarly, you can use incoming=True for messages that you receive
|
|
||||||
@client.on(events.NewMessage(chats='TelethonOffTopic', outgoing=True,
|
|
||||||
pattern='eval (.+)'))
|
|
||||||
async def admin_handler(event):
|
|
||||||
expression = event.pattern_match.group(1)
|
|
||||||
await event.reply(str(ast.literal_eval(expression)))
|
|
||||||
|
|
||||||
|
|
||||||
You can pass one or more chats to the ``chats`` parameter (as a list or tuple),
|
|
||||||
and only events from there will be processed. You can also specify whether you
|
|
||||||
want to handle incoming or outgoing messages (those you receive or those you
|
|
||||||
send). In this example, people can say ``'roll'`` and you will reply with a
|
|
||||||
random number, while if you say ``'eval 4+4'``, you will reply with the
|
|
||||||
solution. Try it!
|
|
||||||
|
|
||||||
|
|
||||||
Properties vs. Methods
|
|
||||||
**********************
|
|
||||||
|
|
||||||
The event shown above acts just like a `custom.Message
|
|
||||||
<telethon.tl.custom.message.Message>`, which means you
|
|
||||||
can access all the properties it has, like ``.sender``.
|
|
||||||
|
|
||||||
**However** events are different to other methods in the client, like
|
|
||||||
`client.get_messages <telethon.client.messages.MessageMethods.get_messages>`.
|
|
||||||
Events *may not* send information about the sender or chat, which means it
|
|
||||||
can be ``None``, but all the methods defined in the client always have this
|
|
||||||
information so it doesn't need to be re-fetched. For this reason, you have
|
|
||||||
``get_`` methods, which will make a network call if necessary.
|
|
||||||
|
|
||||||
In short, you should do this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def handler(event):
|
|
||||||
# event.input_chat may be None, use event.get_input_chat()
|
|
||||||
chat = await event.get_input_chat()
|
|
||||||
sender = await event.get_sender()
|
|
||||||
buttons = await event.get_buttons()
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
async for message in client.iter_messages('me', 10):
|
|
||||||
# Methods from the client always have these properties ready
|
|
||||||
chat = message.input_chat
|
|
||||||
sender = message.sender
|
|
||||||
buttons = message.buttons
|
|
||||||
|
|
||||||
Notice, properties (`message.sender
|
|
||||||
<telethon.tl.custom.message.Message.sender>`) don't need an ``await``, but
|
|
||||||
methods (`message.get_sender
|
|
||||||
<telethon.tl.custom.message.Message.get_sender>`) **do** need an ``await``,
|
|
||||||
and you should use methods in events for these properties that may need network.
|
|
||||||
|
|
||||||
|
|
||||||
Events Without the client
|
|
||||||
*************************
|
|
||||||
|
|
||||||
The code of your application starts getting big, so you decide to
|
|
||||||
separate the handlers into different files. But how can you access
|
|
||||||
the client from these files? You don't need to! Just `events.register
|
|
||||||
<telethon.events.register>` them:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# handlers/welcome.py
|
|
||||||
from telethon import events
|
|
||||||
|
|
||||||
@events.register(events.NewMessage('(?i)hello'))
|
|
||||||
async def handler(event):
|
|
||||||
client = event.client
|
|
||||||
await event.respond('Hey!')
|
|
||||||
await client.send_message('me', 'I said hello to someone')
|
|
||||||
|
|
||||||
|
|
||||||
Registering events is a way of saying "this method is an event handler".
|
|
||||||
You can use `telethon.events.is_handler` to check if any method is a handler.
|
|
||||||
You can think of them as a different approach to Flask's blueprints.
|
|
||||||
|
|
||||||
It's important to note that this does **not** add the handler to any client!
|
|
||||||
You never specified the client on which the handler should be used. You only
|
|
||||||
declared that it is a handler, and its type.
|
|
||||||
|
|
||||||
To actually use the handler, you need to `client.add_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.add_event_handler>` to the
|
|
||||||
client (or clients) where they should be added to:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# main.py
|
|
||||||
from telethon import TelegramClient
|
|
||||||
import handlers.welcome
|
|
||||||
|
|
||||||
with TelegramClient(...) as client:
|
|
||||||
client.add_event_handler(handlers.welcome.handler)
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
This also means that you can register an event handler once and
|
|
||||||
then add it to many clients without re-declaring the event.
|
|
||||||
|
|
||||||
|
|
||||||
Events Without Decorators
|
|
||||||
*************************
|
|
||||||
|
|
||||||
If for any reason you don't want to use `telethon.events.register`,
|
|
||||||
you can explicitly pass the event handler to use to the mentioned
|
|
||||||
`client.add_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.add_event_handler>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
|
||||||
|
|
||||||
async def handler(event):
|
|
||||||
...
|
|
||||||
|
|
||||||
with TelegramClient(...) as client:
|
|
||||||
client.add_event_handler(handler, events.NewMessage)
|
|
||||||
client.run_until_disconnected()
|
|
||||||
|
|
||||||
|
|
||||||
Similarly, you also have `client.remove_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.remove_event_handler>`
|
|
||||||
and `client.list_event_handlers
|
|
||||||
<telethon.client.updates.UpdateMethods.list_event_handlers>`.
|
|
||||||
|
|
||||||
The ``event`` argument is optional in all three methods and defaults to
|
|
||||||
`events.Raw <telethon.events.raw.Raw>` for adding, and ``None`` when
|
|
||||||
removing (so all callbacks would be removed).
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The ``event`` type is ignored in `client.add_event_handler
|
|
||||||
<telethon.client.updates.UpdateMethods.add_event_handler>`
|
|
||||||
if you have used `telethon.events.register` on the ``callback``
|
|
||||||
before, since that's the point of using such method at all.
|
|
||||||
|
|
||||||
|
|
||||||
Stopping Propagation of Updates
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
There might be cases when an event handler is supposed to be used solitary and
|
|
||||||
it makes no sense to process any other handlers in the chain. For this case,
|
|
||||||
it is possible to raise a `telethon.events.StopPropagation` exception which
|
|
||||||
will cause the propagation of the update through your handlers to stop:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.events import StopPropagation
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def _(event):
|
|
||||||
# ... some conditions
|
|
||||||
await event.delete()
|
|
||||||
|
|
||||||
# Other handlers won't have an event to work with
|
|
||||||
raise StopPropagation
|
|
||||||
|
|
||||||
@client.on(events.NewMessage)
|
|
||||||
async def _(event):
|
|
||||||
# Will never be reached, because it is the second handler
|
|
||||||
# in the chain.
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
Remember to check :ref:`telethon-events-package` if you're looking for
|
|
||||||
the methods reference.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/types/update.html
|
|
|
@ -1,54 +0,0 @@
|
||||||
.. _api-status:
|
|
||||||
|
|
||||||
==========
|
|
||||||
API Status
|
|
||||||
==========
|
|
||||||
|
|
||||||
|
|
||||||
In an attempt to help everyone who works with the Telegram API, the
|
|
||||||
library will by default report all *Remote Procedure Call* errors to
|
|
||||||
`RPC PWRTelegram <https://rpc.pwrtelegram.xyz/>`__, a public database
|
|
||||||
anyone can query, made by `Daniil <https://github.com/danog>`__. All the
|
|
||||||
information sent is a ``GET`` request with the error code, error message
|
|
||||||
and method used.
|
|
||||||
|
|
||||||
If you still would like to opt out, you can disable this feature by setting
|
|
||||||
``client.session.report_errors = False``. However Daniil would really thank
|
|
||||||
you if you helped him (and everyone) by keeping it on!
|
|
||||||
|
|
||||||
Querying the API status
|
|
||||||
***********************
|
|
||||||
|
|
||||||
The API is accessed through ``GET`` requests, which can be made for
|
|
||||||
instance through ``curl``. A JSON response will be returned.
|
|
||||||
|
|
||||||
**All known errors and their description**:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?all
|
|
||||||
|
|
||||||
**Error codes for a specific request**:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?for=messages.sendMessage
|
|
||||||
|
|
||||||
**Number of** ``RPC_CALL_FAIL``:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?rip # last hour
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?rip=$(time()-60) # last minute
|
|
||||||
|
|
||||||
**Description of errors**:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?description_for=SESSION_REVOKED
|
|
||||||
|
|
||||||
**Code of a specific error**:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl https://rpc.pwrtelegram.xyz/?code_for=STICKERSET_INVALID
|
|
|
@ -1,73 +0,0 @@
|
||||||
===============================
|
|
||||||
Telegram API in Other Languages
|
|
||||||
===============================
|
|
||||||
|
|
||||||
|
|
||||||
Telethon was made for **Python**, and as far as I know, there is no
|
|
||||||
*exact* port to other languages. However, there *are* other
|
|
||||||
implementations made by awesome people (one needs to be awesome to
|
|
||||||
understand the official Telegram documentation) on several languages
|
|
||||||
(even more Python too), listed below:
|
|
||||||
|
|
||||||
C
|
|
||||||
*
|
|
||||||
|
|
||||||
Possibly the most well-known unofficial open source implementation out
|
|
||||||
there by `@vysheng <https://github.com/vysheng>`__,
|
|
||||||
`tgl <https://github.com/vysheng/tgl>`__, and its console client
|
|
||||||
`telegram-cli <https://github.com/vysheng/tg>`__. Latest development
|
|
||||||
has been moved to `BitBucket <https://bitbucket.org/vysheng/tdcli>`__.
|
|
||||||
|
|
||||||
C++
|
|
||||||
***
|
|
||||||
|
|
||||||
The newest (and official) library, written from scratch, is called
|
|
||||||
`tdlib <https://github.com/tdlib/td>`__ and is what the Telegram X
|
|
||||||
uses. You can find more information in the official documentation,
|
|
||||||
published `here <https://core.telegram.org/tdlib/docs/>`__.
|
|
||||||
|
|
||||||
JavaScript
|
|
||||||
**********
|
|
||||||
|
|
||||||
`@zerobias <https://github.com/zerobias>`__ is working on
|
|
||||||
`telegram-mtproto <https://github.com/zerobias/telegram-mtproto>`__,
|
|
||||||
a work-in-progress JavaScript library installable via
|
|
||||||
`npm <https://www.npmjs.com/>`__.
|
|
||||||
|
|
||||||
Kotlin
|
|
||||||
******
|
|
||||||
|
|
||||||
`Kotlogram <https://github.com/badoualy/kotlogram>`__ is a Telegram
|
|
||||||
implementation written in Kotlin (one of the
|
|
||||||
`official <https://blog.jetbrains.com/kotlin/2017/05/kotlin-on-android-now-official/>`__
|
|
||||||
languages for
|
|
||||||
`Android <https://developer.android.com/kotlin/index.html>`__) by
|
|
||||||
`@badoualy <https://github.com/badoualy>`__, currently as a beta–
|
|
||||||
yet working.
|
|
||||||
|
|
||||||
PHP
|
|
||||||
***
|
|
||||||
|
|
||||||
A PHP implementation is also available thanks to
|
|
||||||
`@danog <https://github.com/danog>`__ and his
|
|
||||||
`MadelineProto <https://github.com/danog/MadelineProto>`__ project, with
|
|
||||||
a very nice `online
|
|
||||||
documentation <https://daniil.it/MadelineProto/API_docs/>`__ too.
|
|
||||||
|
|
||||||
Python
|
|
||||||
******
|
|
||||||
|
|
||||||
A fairly new (as of the end of 2017) Telegram library written from the
|
|
||||||
ground up in Python by
|
|
||||||
`@delivrance <https://github.com/delivrance>`__ and his
|
|
||||||
`Pyrogram <https://github.com/pyrogram/pyrogram>`__ library.
|
|
||||||
There isn't really a reason to pick it over Telethon and it'd be kinda
|
|
||||||
sad to see you go, but it would be nice to know what you miss from each
|
|
||||||
other library in either one so both can improve.
|
|
||||||
|
|
||||||
Rust
|
|
||||||
****
|
|
||||||
|
|
||||||
Yet another work-in-progress implementation, this time for Rust thanks
|
|
||||||
to `@JuanPotato <https://github.com/JuanPotato>`__ under the fancy
|
|
||||||
name of `Vail <https://github.com/JuanPotato/Vail>`__.
|
|
|
@ -1,71 +0,0 @@
|
||||||
====
|
|
||||||
Bots
|
|
||||||
====
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
|
|
||||||
Talking to Inline Bots
|
|
||||||
**********************
|
|
||||||
|
|
||||||
You can query an inline bot, such as `@VoteBot`__ (note, *query*,
|
|
||||||
not *interact* with a voting message), by making use of the
|
|
||||||
:tl:`GetInlineBotResultsRequest` request:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import GetInlineBotResultsRequest
|
|
||||||
|
|
||||||
bot_results = client(GetInlineBotResultsRequest(
|
|
||||||
bot, user_or_chat, 'query', ''
|
|
||||||
))
|
|
||||||
|
|
||||||
And you can select any of their results by using
|
|
||||||
:tl:`SendInlineBotResultRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import SendInlineBotResultRequest
|
|
||||||
|
|
||||||
client(SendInlineBotResultRequest(
|
|
||||||
get_input_peer(user_or_chat),
|
|
||||||
obtained_query_id,
|
|
||||||
obtained_str_id
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
Talking to Bots with special reply markup
|
|
||||||
*****************************************
|
|
||||||
|
|
||||||
Generally, you just use the `message.click()
|
|
||||||
<telethon.tl.custom.message.Message.click>` method:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
messages = client.get_messages('somebot')
|
|
||||||
messages[0].click(0)
|
|
||||||
|
|
||||||
You can also do it manually.
|
|
||||||
|
|
||||||
To interact with a message that has a special reply markup, such as
|
|
||||||
`@VoteBot`__ polls, you would use :tl:`GetBotCallbackAnswerRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import GetBotCallbackAnswerRequest
|
|
||||||
|
|
||||||
client(GetBotCallbackAnswerRequest(
|
|
||||||
user_or_chat,
|
|
||||||
msg.id,
|
|
||||||
data=msg.reply_markup.rows[wanted_row].buttons[wanted_button].data
|
|
||||||
))
|
|
||||||
|
|
||||||
It's a bit verbose, but it has all the information you would need to
|
|
||||||
show it visually (button rows, and buttons within each row, each with
|
|
||||||
its own data).
|
|
||||||
|
|
||||||
__ https://t.me/vote
|
|
||||||
__ https://t.me/vote
|
|
|
@ -1,325 +0,0 @@
|
||||||
===============================
|
|
||||||
Working with Chats and Channels
|
|
||||||
===============================
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
|
|
||||||
Joining a chat or channel
|
|
||||||
*************************
|
|
||||||
|
|
||||||
Note that :tl:`Chat` are normal groups, and :tl:`Channel` are a
|
|
||||||
special form of :tl:`Chat`, which can also be super-groups if
|
|
||||||
their ``megagroup`` member is ``True``.
|
|
||||||
|
|
||||||
|
|
||||||
Joining a public channel
|
|
||||||
************************
|
|
||||||
|
|
||||||
Once you have the :ref:`entity <entities>` of the channel you want to join
|
|
||||||
to, you can make use of the :tl:`JoinChannelRequest` to join such channel:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import JoinChannelRequest
|
|
||||||
client(JoinChannelRequest(channel))
|
|
||||||
|
|
||||||
# In the same way, you can also leave such channel
|
|
||||||
from telethon.tl.functions.channels import LeaveChannelRequest
|
|
||||||
client(LeaveChannelRequest(input_channel))
|
|
||||||
|
|
||||||
|
|
||||||
For more on channels, check the `channels namespace`__.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/methods/channels/index.html
|
|
||||||
|
|
||||||
|
|
||||||
Joining a private chat or channel
|
|
||||||
*********************************
|
|
||||||
|
|
||||||
If all you have is a link like this one:
|
|
||||||
``https://t.me/joinchat/AAAAAFFszQPyPEZ7wgxLtd``, you already have
|
|
||||||
enough information to join! The part after the
|
|
||||||
``https://t.me/joinchat/``, this is, ``AAAAAFFszQPyPEZ7wgxLtd`` on this
|
|
||||||
example, is the ``hash`` of the chat or channel. Now you can use
|
|
||||||
:tl:`ImportChatInviteRequest` as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import ImportChatInviteRequest
|
|
||||||
updates = client(ImportChatInviteRequest('AAAAAEHbEkejzxUjAUCfYg'))
|
|
||||||
|
|
||||||
|
|
||||||
Adding someone else to such chat or channel
|
|
||||||
*******************************************
|
|
||||||
|
|
||||||
If you don't want to add yourself, maybe because you're already in,
|
|
||||||
you can always add someone else with the :tl:`AddChatUserRequest`, which
|
|
||||||
use is very straightforward, or :tl:`InviteToChannelRequest` for channels:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# For normal chats
|
|
||||||
from telethon.tl.functions.messages import AddChatUserRequest
|
|
||||||
|
|
||||||
# Note that ``user_to_add`` is NOT the name of the parameter.
|
|
||||||
# It's the user you want to add (``user_id=user_to_add``).
|
|
||||||
client(AddChatUserRequest(
|
|
||||||
chat_id,
|
|
||||||
user_to_add,
|
|
||||||
fwd_limit=10 # Allow the user to see the 10 last messages
|
|
||||||
))
|
|
||||||
|
|
||||||
# For channels (which includes megagroups)
|
|
||||||
from telethon.tl.functions.channels import InviteToChannelRequest
|
|
||||||
|
|
||||||
client(InviteToChannelRequest(
|
|
||||||
channel,
|
|
||||||
[users_to_add]
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
Checking a link without joining
|
|
||||||
*******************************
|
|
||||||
|
|
||||||
If you don't need to join but rather check whether it's a group or a
|
|
||||||
channel, you can use the :tl:`CheckChatInviteRequest`, which takes in
|
|
||||||
the hash of said channel or group.
|
|
||||||
|
|
||||||
|
|
||||||
Retrieving all chat members (channels too)
|
|
||||||
******************************************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.telegram_client.TelegramClient.iter_participants`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method will handle different chat types for you automatically.
|
|
||||||
|
|
||||||
|
|
||||||
Here is the easy way to do it:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
participants = client.get_participants(group)
|
|
||||||
|
|
||||||
Now we will show how the method works internally.
|
|
||||||
|
|
||||||
In order to get all the members from a mega-group or channel, you need
|
|
||||||
to use :tl:`GetParticipantsRequest`. As we can see it needs an
|
|
||||||
:tl:`InputChannel`, (passing the mega-group or channel you're going to
|
|
||||||
use will work), and a mandatory :tl:`ChannelParticipantsFilter`. The
|
|
||||||
closest thing to "no filter" is to simply use
|
|
||||||
:tl:`ChannelParticipantsSearch` with an empty ``'q'`` string.
|
|
||||||
|
|
||||||
If we want to get *all* the members, we need to use a moving offset and
|
|
||||||
a fixed limit:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import GetParticipantsRequest
|
|
||||||
from telethon.tl.types import ChannelParticipantsSearch
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
offset = 0
|
|
||||||
limit = 100
|
|
||||||
all_participants = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
participants = client(GetParticipantsRequest(
|
|
||||||
channel, ChannelParticipantsSearch(''), offset, limit, hash=0
|
|
||||||
))
|
|
||||||
if not participants.users:
|
|
||||||
break
|
|
||||||
all_participants.extend(participants.users)
|
|
||||||
offset += len(participants.users)
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
If you need more than 10,000 members from a group you should use the
|
|
||||||
mentioned ``client.get_participants(..., aggressive=True)``. It will
|
|
||||||
do some tricks behind the scenes to get as many entities as possible.
|
|
||||||
Refer to `issue 573`__ for more on this.
|
|
||||||
|
|
||||||
|
|
||||||
Note that :tl:`GetParticipantsRequest` returns :tl:`ChannelParticipants`,
|
|
||||||
which may have more information you need (like the role of the
|
|
||||||
participants, total count of members, etc.)
|
|
||||||
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/573
|
|
||||||
|
|
||||||
|
|
||||||
Recent Actions
|
|
||||||
**************
|
|
||||||
|
|
||||||
"Recent actions" is simply the name official applications have given to
|
|
||||||
the "admin log". Simply use :tl:`GetAdminLogRequest` for that, and
|
|
||||||
you'll get AdminLogResults.events in return which in turn has the final
|
|
||||||
`.action`__.
|
|
||||||
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/types/channel_admin_log_event_action.html
|
|
||||||
|
|
||||||
|
|
||||||
Admin Permissions
|
|
||||||
*****************
|
|
||||||
|
|
||||||
Giving or revoking admin permissions can be done with the :tl:`EditAdminRequest`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import EditAdminRequest
|
|
||||||
from telethon.tl.types import ChannelAdminRights
|
|
||||||
|
|
||||||
# You need both the channel and who to grant permissions
|
|
||||||
# They can either be channel/user or input channel/input user.
|
|
||||||
#
|
|
||||||
# ChannelAdminRights is a list of granted permissions.
|
|
||||||
# Set to True those you want to give.
|
|
||||||
rights = ChannelAdminRights(
|
|
||||||
post_messages=None,
|
|
||||||
add_admins=None,
|
|
||||||
invite_users=None,
|
|
||||||
change_info=True,
|
|
||||||
ban_users=None,
|
|
||||||
delete_messages=True,
|
|
||||||
pin_messages=True,
|
|
||||||
invite_link=None,
|
|
||||||
edit_messages=None
|
|
||||||
)
|
|
||||||
# Equivalent to:
|
|
||||||
# rights = ChannelAdminRights(
|
|
||||||
# change_info=True,
|
|
||||||
# delete_messages=True,
|
|
||||||
# pin_messages=True
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Once you have a ChannelAdminRights, invoke it
|
|
||||||
client(EditAdminRequest(channel, user, rights))
|
|
||||||
|
|
||||||
# User will now be able to change group info, delete other people's
|
|
||||||
# messages and pin messages.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Thanks to `@Kyle2142`__ for `pointing out`__ that you **cannot** set all
|
|
||||||
parameters to ``True`` to give a user full permissions, as not all
|
|
||||||
permissions are related to both broadcast channels/megagroups.
|
|
||||||
|
|
||||||
E.g. trying to set ``post_messages=True`` in a megagroup will raise an
|
|
||||||
error. It is recommended to always use keyword arguments, and to set only
|
|
||||||
the permissions the user needs. If you don't need to change a permission,
|
|
||||||
it can be omitted (full list `here`__).
|
|
||||||
|
|
||||||
|
|
||||||
Restricting Users
|
|
||||||
*****************
|
|
||||||
|
|
||||||
Similar to how you give or revoke admin permissions, you can edit the
|
|
||||||
banned rights of a user through :tl:`EditBannedRequest` and its parameter
|
|
||||||
:tl:`ChannelBannedRights`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import EditBannedRequest
|
|
||||||
from telethon.tl.types import ChannelBannedRights
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
# Restricting a user for 7 days, only allowing view/send messages.
|
|
||||||
#
|
|
||||||
# Note that it's "reversed". You must set to ``True`` the permissions
|
|
||||||
# you want to REMOVE, and leave as ``None`` those you want to KEEP.
|
|
||||||
rights = ChannelBannedRights(
|
|
||||||
until_date=timedelta(days=7),
|
|
||||||
view_messages=None,
|
|
||||||
send_messages=None,
|
|
||||||
send_media=True,
|
|
||||||
send_stickers=True,
|
|
||||||
send_gifs=True,
|
|
||||||
send_games=True,
|
|
||||||
send_inline=True,
|
|
||||||
embed_links=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# The above is equivalent to
|
|
||||||
rights = ChannelBannedRights(
|
|
||||||
until_date=datetime.now() + timedelta(days=7),
|
|
||||||
send_media=True,
|
|
||||||
send_stickers=True,
|
|
||||||
send_gifs=True,
|
|
||||||
send_games=True,
|
|
||||||
send_inline=True,
|
|
||||||
embed_links=True
|
|
||||||
)
|
|
||||||
|
|
||||||
client(EditBannedRequest(channel, user, rights))
|
|
||||||
|
|
||||||
|
|
||||||
You can also use a ``datetime`` object for ``until_date=``, or even a
|
|
||||||
Unix timestamp. Note that if you ban someone for less than 30 seconds
|
|
||||||
or for more than 366 days, Telegram will consider the ban to actually
|
|
||||||
last forever. This is officially documented under
|
|
||||||
https://core.telegram.org/bots/api#restrictchatmember.
|
|
||||||
|
|
||||||
|
|
||||||
Kicking a member
|
|
||||||
****************
|
|
||||||
|
|
||||||
Telegram doesn't actually have a request to kick a user from a group.
|
|
||||||
Instead, you need to restrict them so they can't see messages. Any date
|
|
||||||
is enough:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.channels import EditBannedRequest
|
|
||||||
from telethon.tl.types import ChannelBannedRights
|
|
||||||
|
|
||||||
client(EditBannedRequest(
|
|
||||||
channel, user, ChannelBannedRights(
|
|
||||||
until_date=None,
|
|
||||||
view_messages=True
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
__ https://github.com/Kyle2142
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/490
|
|
||||||
__ https://lonamiwebs.github.io/Telethon/constructors/channel_admin_rights.html
|
|
||||||
|
|
||||||
|
|
||||||
Increasing View Count in a Channel
|
|
||||||
**********************************
|
|
||||||
|
|
||||||
It has been asked `quite`__ `a few`__ `times`__ (really, `many`__), and
|
|
||||||
while I don't understand why so many people ask this, the solution is to
|
|
||||||
use :tl:`GetMessagesViewsRequest`, setting ``increment=True``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
|
||||||
# Obtain `channel' through dialogs or through client.get_entity() or anyhow.
|
|
||||||
# Obtain `msg_ids' through `.get_messages()` or anyhow. Must be a list.
|
|
||||||
|
|
||||||
client(GetMessagesViewsRequest(
|
|
||||||
peer=channel,
|
|
||||||
id=msg_ids,
|
|
||||||
increment=True
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
Note that you can only do this **once or twice a day** per account,
|
|
||||||
running this in a loop will obviously not increase the views forever
|
|
||||||
unless you wait a day between each iteration. If you run it any sooner
|
|
||||||
than that, the views simply won't be increased.
|
|
||||||
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/233
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/305
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/409
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/447
|
|
|
@ -1,55 +0,0 @@
|
||||||
=======================
|
|
||||||
Projects using Telethon
|
|
||||||
=======================
|
|
||||||
|
|
||||||
This page lists some real world examples showcasing what can be built with
|
|
||||||
the library.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Do you have a project that uses the library or know of any that's not
|
|
||||||
listed here? Feel free to leave a comment at
|
|
||||||
`issue 744 <https://github.com/LonamiWebs/Telethon/issues/744>`_
|
|
||||||
so it can be included in the next revision of the documentation!
|
|
||||||
|
|
||||||
.. _projects-telegram-export:
|
|
||||||
|
|
||||||
telethon_examples/
|
|
||||||
******************
|
|
||||||
|
|
||||||
`Link <https://github.com/LonamiWebs/Telethon/tree/master/telethon_examples>`_ /
|
|
||||||
`Author's website <https://lonamiwebs.github.io>`_
|
|
||||||
|
|
||||||
This documentation is not the only place where you can find useful code
|
|
||||||
snippets using the library. The main repository also has a folder with
|
|
||||||
some cool examples (even a Tkinter GUI!) which you can download, edit
|
|
||||||
and run to learn and play with them.
|
|
||||||
|
|
||||||
telegram-export
|
|
||||||
***************
|
|
||||||
|
|
||||||
`Link <https://github.com/expectocode/telegram-export>`_ /
|
|
||||||
`Author's website <https://github.com/expectocode>`_
|
|
||||||
|
|
||||||
A tool to download Telegram data (users, chats, messages, and media)
|
|
||||||
into a database (and display the saved data).
|
|
||||||
|
|
||||||
.. _projects-mautrix-telegram:
|
|
||||||
|
|
||||||
mautrix-telegram
|
|
||||||
****************
|
|
||||||
|
|
||||||
`Link <https://github.com/tulir/mautrix-telegram>`_ /
|
|
||||||
`Author's website <https://maunium.net/>`_
|
|
||||||
|
|
||||||
A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
|
||||||
|
|
||||||
.. _projects-telegramtui:
|
|
||||||
|
|
||||||
TelegramTUI
|
|
||||||
***********
|
|
||||||
|
|
||||||
`Link <https://github.com/bad-day/TelegramTUI>`_ /
|
|
||||||
`Author's website <https://github.com/bad-day>`_
|
|
||||||
|
|
||||||
A Telegram client on your terminal.
|
|
|
@ -1,619 +0,0 @@
|
||||||
.. _telegram-client-example:
|
|
||||||
|
|
||||||
|
|
||||||
========================
|
|
||||||
Examples with the Client
|
|
||||||
========================
|
|
||||||
|
|
||||||
This section explores the methods defined in the :ref:`telegram-client`
|
|
||||||
with some practical examples. The section assumes that you have imported
|
|
||||||
the ``telethon.sync`` package and that you have a client ready to use.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
There are some very common errors (such as forgetting to add
|
|
||||||
``import telethon.sync``) for newcomers to ``asyncio``:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# AttributeError: 'coroutine' object has no attribute 'first_name'
|
|
||||||
print(client.get_me().first_name)
|
|
||||||
|
|
||||||
# TypeError: 'AsyncGenerator' object is not iterable
|
|
||||||
for message in client.iter_messages('me'):
|
|
||||||
...
|
|
||||||
|
|
||||||
# RuntimeError: This event loop is already running
|
|
||||||
with client.conversation('me') as conv:
|
|
||||||
...
|
|
||||||
|
|
||||||
That error means you're probably inside an ``async def`` so you
|
|
||||||
need to use:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
print((await client.get_me()).first_name)
|
|
||||||
async for message in client.iter_messages('me'):
|
|
||||||
...
|
|
||||||
|
|
||||||
async with client.conversation('me') as conv:
|
|
||||||
...
|
|
||||||
|
|
||||||
You can of course call other ``def`` functions from your ``async def``
|
|
||||||
event handlers, but if they need making API calls, make your own
|
|
||||||
functions ``async def`` so you can ``await`` things:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
async def helper(client):
|
|
||||||
await client.send_message('me', 'Hi')
|
|
||||||
|
|
||||||
If you're not inside an ``async def`` you can enter one like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
loop.run_until_complete(my_async_def())
|
|
||||||
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
Authorization
|
|
||||||
*************
|
|
||||||
|
|
||||||
Starting the client is as easy as calling `client.start()
|
|
||||||
<telethon.client.auth.AuthMethods.start>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.start()
|
|
||||||
... # code using the client
|
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
And you can even use a ``with`` block:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
with client:
|
|
||||||
... # code using the client
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Remember we assume you have ``import telethon.sync``. You can of course
|
|
||||||
use the library without importing it. The code would be rewritten as:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
await client.start()
|
|
||||||
...
|
|
||||||
await client.disconnect()
|
|
||||||
|
|
||||||
# or
|
|
||||||
async with client:
|
|
||||||
...
|
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
All methods that need access to the network (e.g. to make an API call)
|
|
||||||
**must** be awaited (or their equivalent such as ``async for`` and
|
|
||||||
``async with``). You can do this yourself or you can let the library
|
|
||||||
do it for you by using ``import telethon.sync``. With event handlers,
|
|
||||||
you must do this yourself.
|
|
||||||
|
|
||||||
The cleanest way to delete your ``*.session`` file is `client.log_out
|
|
||||||
<telethon.client.auth.AuthMethods.log_out>`. Note that you will obviously
|
|
||||||
need to login again if you use this:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# Logs out and deletes the session file; you will need to sign in again
|
|
||||||
client.log_out()
|
|
||||||
|
|
||||||
# You often simply want to disconnect. You will not need to sign in again
|
|
||||||
client.disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
Group Chats
|
|
||||||
***********
|
|
||||||
|
|
||||||
You can easily iterate over all the :tl:`User` in a chat and
|
|
||||||
do anything you want with them by using `client.iter_participants
|
|
||||||
<telethon.client.chats.ChatMethods.iter_participants>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
for user in client.iter_participants(chat):
|
|
||||||
... # do something with the user
|
|
||||||
|
|
||||||
You can also search by their name:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
for user in client.iter_participants(chat, search='name'):
|
|
||||||
...
|
|
||||||
|
|
||||||
Or by their type (e.g. if they are admin) with :tl:`ChannelParticipantsFilter`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.types import ChannelParticipantsAdmins
|
|
||||||
|
|
||||||
for user in client.iter_participants(chat, filter=ChannelParticipantsAdmins):
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
Open Conversations and Joined Channels
|
|
||||||
**************************************
|
|
||||||
|
|
||||||
The conversations you have open and the channels you have joined
|
|
||||||
are in your "dialogs", so to get them you need to `client.get_dialogs
|
|
||||||
<telethon.client.dialogs.DialogMethods.get_dialogs>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
dialogs = client.get_dialogs()
|
|
||||||
first = dialogs[0]
|
|
||||||
print(first.title)
|
|
||||||
|
|
||||||
You can then use the dialog as if it were a peer:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message(first, 'hi')
|
|
||||||
|
|
||||||
|
|
||||||
You can access `dialog.draft <telethon.tl.custom.draft.Draft>` or you can
|
|
||||||
get them all at once without getting the dialogs:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
drafts = client.get_drafts()
|
|
||||||
|
|
||||||
|
|
||||||
Downloading Media
|
|
||||||
*****************
|
|
||||||
|
|
||||||
It's easy to `download_profile_photo
|
|
||||||
<telethon.client.downloads.DownloadMethods.download_profile_photo>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.download_profile_photo(user)
|
|
||||||
|
|
||||||
Or `download_media <telethon.client.downloads.DownloadMethods.download_media>`
|
|
||||||
from a message:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.download_media(message)
|
|
||||||
client.download_media(message, filename)
|
|
||||||
# or
|
|
||||||
message.download_media()
|
|
||||||
message.download_media(filename)
|
|
||||||
|
|
||||||
Remember that these methods return the final filename where the
|
|
||||||
media was downloaded (e.g. it may add the extension automatically).
|
|
||||||
|
|
||||||
Getting Messages
|
|
||||||
****************
|
|
||||||
|
|
||||||
You can easily iterate over all the `messages
|
|
||||||
<telethon.tl.custom.message.Message>` of a chat with `iter_messages
|
|
||||||
<telethon.client.messages.MessageMethods.iter_messages>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
for message in client.iter_messages(chat):
|
|
||||||
... # do something with the message from recent to older
|
|
||||||
|
|
||||||
for message in client.iter_messages(chat, reverse=True):
|
|
||||||
... # going from the oldest to the most recent
|
|
||||||
|
|
||||||
You can also use it to search for messages from a specific person:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
for message in client.iter_messages(chat, from_user='me'):
|
|
||||||
...
|
|
||||||
|
|
||||||
Or you can search by text:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
for message in client.iter_messages(chat, search='hello'):
|
|
||||||
...
|
|
||||||
|
|
||||||
Or you can search by media with a :tl:`MessagesFilter`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.types import InputMessagesFilterPhotos
|
|
||||||
|
|
||||||
for message in client.iter_messages(chat, filter=InputMessagesFilterPhotos):
|
|
||||||
...
|
|
||||||
|
|
||||||
If you want a list instead, use the get variant. The second
|
|
||||||
argument is the limit, and ``None`` means "get them all":
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
|
|
||||||
from telethon.tl.types import InputMessagesFilterPhotos
|
|
||||||
|
|
||||||
# Get 0 photos and print the total
|
|
||||||
photos = client.get_messages(chat, 0, filter=InputMessagesFilterPhotos)
|
|
||||||
print(photos.total)
|
|
||||||
|
|
||||||
# Get all the photos
|
|
||||||
photos = client.get_messages(chat, None, filter=InputMessagesFilterPhotos)
|
|
||||||
|
|
||||||
Or just some IDs:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message_1337 = client.get_messages(chats, ids=1337)
|
|
||||||
|
|
||||||
|
|
||||||
Sending Messages
|
|
||||||
****************
|
|
||||||
|
|
||||||
Just use `send_message <telethon.client.messages.MessageMethods.send_message>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('lonami', 'Thanks for the Telethon library!')
|
|
||||||
|
|
||||||
The function returns the `custom.Message <telethon.tl.custom.message.Message>`
|
|
||||||
that was sent so you can do more things with it if you want.
|
|
||||||
|
|
||||||
You can also `reply <telethon.tl.custom.message.Message.reply>` or
|
|
||||||
`respond <telethon.tl.custom.message.Message.respond>` to messages:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message.reply('Hello')
|
|
||||||
message.respond('World')
|
|
||||||
|
|
||||||
Sending Markdown or HTML messages
|
|
||||||
*********************************
|
|
||||||
|
|
||||||
Markdown (``'md'`` or ``'markdown'``) is the default `parse_mode
|
|
||||||
<telethon.client.messageparse.MessageParseMethods.parse_mode>`
|
|
||||||
for the client. You can change the default parse mode like so:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.parse_mode = 'html'
|
|
||||||
|
|
||||||
|
|
||||||
Now all messages will be formatted as HTML by default:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message('me', 'Some <b>bold</b> and <i>italic</i> text')
|
|
||||||
client.send_message('me', 'An <a href="https://example.com">URL</a>')
|
|
||||||
client.send_message('me', '<code>code</code> and <pre>pre\nblocks</pre>')
|
|
||||||
client.send_message('me', '<a href="tg://user?id=me">Mentions</a>')
|
|
||||||
|
|
||||||
|
|
||||||
You can override the default parse mode to use for special cases:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# No parse mode by default
|
|
||||||
client.parse_mode = None
|
|
||||||
|
|
||||||
# ...but here I want markdown
|
|
||||||
client.send_message('me', 'Hello, **world**!', parse_mode='md')
|
|
||||||
|
|
||||||
# ...and here I need HTML
|
|
||||||
client.send_message('me', 'Hello, <i>world</i>!', parse_mode='html')
|
|
||||||
|
|
||||||
The rules are the same as for Bot API, so please refer to
|
|
||||||
https://core.telegram.org/bots/api#formatting-options.
|
|
||||||
|
|
||||||
Sending Messages with Media
|
|
||||||
***************************
|
|
||||||
|
|
||||||
Sending media can be done with `send_file
|
|
||||||
<telethon.client.uploads.UploadMethods.send_file>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_file(chat, '/my/photos/me.jpg', caption="It's me!")
|
|
||||||
# or
|
|
||||||
client.send_message(chat, "It's me!", file='/my/photos/me.jpg')
|
|
||||||
|
|
||||||
You can send voice notes or round videos by setting the right arguments:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_file(chat, '/my/songs/song.mp3', voice_note=True)
|
|
||||||
client.send_file(chat, '/my/videos/video.mp4', video_note=True)
|
|
||||||
|
|
||||||
You can set a JPG thumbnail for any document:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_file(chat, '/my/documents/doc.txt', thumb='photo.jpg')
|
|
||||||
|
|
||||||
You can force sending images as documents:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_file(chat, '/my/photos/photo.png', force_document=True)
|
|
||||||
|
|
||||||
You can send albums if you pass more than one file:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_file(chat, [
|
|
||||||
'/my/photos/holiday1.jpg',
|
|
||||||
'/my/photos/holiday2.jpg',
|
|
||||||
'/my/drawings/portrait.png'
|
|
||||||
])
|
|
||||||
|
|
||||||
The caption can also be a list to match the different photos.
|
|
||||||
|
|
||||||
Sending Messages with Buttons
|
|
||||||
*****************************
|
|
||||||
|
|
||||||
You must sign in as a bot in order to add inline buttons (or normal
|
|
||||||
keyboards) to your messages. Once you have signed in as a bot specify
|
|
||||||
the `Button <telethon.tl.custom.button.Button>` or buttons to use:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.custom import Button
|
|
||||||
|
|
||||||
async def callback(event):
|
|
||||||
await event.edit('Thank you!')
|
|
||||||
|
|
||||||
client.send_message(chat, 'Hello!',
|
|
||||||
buttons=Button.inline('Click me', callback))
|
|
||||||
|
|
||||||
|
|
||||||
You can also add the event handler yourself, or change the data payload:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import events
|
|
||||||
|
|
||||||
@client.on(events.CallbackQuery)
|
|
||||||
async def handler(event):
|
|
||||||
await event.answer('You clicked {}!'.format(event.data))
|
|
||||||
|
|
||||||
client.send_message(chat, 'Pick one', buttons=[
|
|
||||||
[Button.inline('Left'), Button.inline('Right')],
|
|
||||||
[Button.url('Check my site!', 'https://lonamiwebs.github.io')]
|
|
||||||
])
|
|
||||||
|
|
||||||
You can also use normal buttons (not inline) to request the user's
|
|
||||||
location, phone number, or simply for them to easily send a message:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message(chat, 'Welcome', buttons=[
|
|
||||||
Button.text('Thanks!'),
|
|
||||||
Button.request_phone('Send phone'),
|
|
||||||
Button.request_location('Send location')
|
|
||||||
])
|
|
||||||
|
|
||||||
Forcing a reply or removing the keyboard can also be done:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message(chat, 'Reply to me', buttons=Button.force_reply())
|
|
||||||
client.send_message(chat, 'Bye Keyboard!', buttons=Button.clear())
|
|
||||||
|
|
||||||
Remember to check `Button <telethon.tl.custom.button.Button>` for more.
|
|
||||||
|
|
||||||
Making Inline Queries
|
|
||||||
*********************
|
|
||||||
|
|
||||||
You can send messages ``via @bot`` by first making an inline query:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
results = client.inline_query('like', 'Do you like Telethon?')
|
|
||||||
|
|
||||||
Then access the result you want and `click
|
|
||||||
<telethon.tl.custom.inlineresult.InlineResult.click>` it in the chat
|
|
||||||
where you want to send it to:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message = results[0].click('TelethonOffTopic')
|
|
||||||
|
|
||||||
Sending messages through inline bots lets you use buttons as a normal user.
|
|
||||||
|
|
||||||
Clicking Buttons
|
|
||||||
****************
|
|
||||||
|
|
||||||
Let's `click <telethon.tl.custom.message.Message.click>`
|
|
||||||
the message we sent in the example above!
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
message.click(0)
|
|
||||||
|
|
||||||
This will click the first button in the message. You could also
|
|
||||||
``click(row, column)``, using some text such as ``click(text='👍')``
|
|
||||||
or even the data directly ``click(data=b'payload')``.
|
|
||||||
|
|
||||||
Conversations: Waiting for Messages or Replies
|
|
||||||
**********************************************
|
|
||||||
|
|
||||||
This one is really useful for unit testing your bots, which you can
|
|
||||||
even write within Telethon itself! You can open a `Conversation
|
|
||||||
<telethon.tl.custom.conversation.Conversation>` in any chat as:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
with client.conversation(chat) as conv:
|
|
||||||
...
|
|
||||||
|
|
||||||
Conversations let you program a finite state machine with the
|
|
||||||
higher-level constructs we are all used to, such as ``while``
|
|
||||||
and ``if`` conditionals instead setting the state and jumping
|
|
||||||
from one place to another which is less clean.
|
|
||||||
|
|
||||||
For instance, let's imagine ``you`` are the bot talking to ``usr``:
|
|
||||||
|
|
||||||
.. code-block:: text
|
|
||||||
|
|
||||||
<you> Hi!
|
|
||||||
<usr> Hello!
|
|
||||||
<you> Please tell me your name
|
|
||||||
<usr> ?
|
|
||||||
<you> Your name didn't have any letters! Try again
|
|
||||||
<usr> Lonami
|
|
||||||
<you> Thanks!
|
|
||||||
|
|
||||||
This can be programmed as follows:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
with bot.conversation(chat) as conv:
|
|
||||||
conv.send_message('Hi!')
|
|
||||||
hello = conv.get_response()
|
|
||||||
|
|
||||||
conv.send_message('Please tell me your name')
|
|
||||||
name = conv.get_response().raw_text
|
|
||||||
while not any(x.isalpha() for x in name):
|
|
||||||
conv.send_message("Your name didn't have any letters! Try again")
|
|
||||||
name = conv.get_response().raw_text
|
|
||||||
|
|
||||||
conv.send_message('Thanks {}!'.format(name))
|
|
||||||
|
|
||||||
Note how we sent a message **with the conversation**, not with the client.
|
|
||||||
This is important so the conversation remembers what messages you sent.
|
|
||||||
|
|
||||||
The method reference for getting a response, getting a reply or marking
|
|
||||||
the conversation as read can be found by clicking here: `Conversation
|
|
||||||
<telethon.tl.custom.conversation.Conversation>`.
|
|
||||||
|
|
||||||
Sending a message or getting a response returns a `Message
|
|
||||||
<telethon.tl.custom.message.Message>`. Reading its documentation
|
|
||||||
will also be really useful!
|
|
||||||
|
|
||||||
If a reply never arrives or too many messages come in, getting
|
|
||||||
responses will raise ``asyncio.TimeoutError`` or ``ValueError``
|
|
||||||
respectively. You may want to ``except`` these and tell the user
|
|
||||||
they were too slow, or simply drop the conversation.
|
|
||||||
|
|
||||||
|
|
||||||
Forwarding Messages
|
|
||||||
*******************
|
|
||||||
|
|
||||||
You can forward up to 100 messages with `forward_messages
|
|
||||||
<telethon.client.messages.MessageMethods.forward_messages>`,
|
|
||||||
or a single one if you have the message with `forward_to
|
|
||||||
<telethon.tl.custom.message.Message.forward_to>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# a single one
|
|
||||||
client.forward_messages(chat, message)
|
|
||||||
# or
|
|
||||||
client.forward_messages(chat, message_id, from_chat)
|
|
||||||
# or
|
|
||||||
message.forward_to(chat)
|
|
||||||
|
|
||||||
# multiple
|
|
||||||
client.forward_messages(chat, messages)
|
|
||||||
# or
|
|
||||||
client.forward_messages(chat, message_ids, from_chat)
|
|
||||||
|
|
||||||
You can also "forward" messages without showing "Forwarded from" by
|
|
||||||
re-sending the message:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_message(chat, message)
|
|
||||||
|
|
||||||
|
|
||||||
Editing Messages
|
|
||||||
****************
|
|
||||||
|
|
||||||
With `edit_message <telethon.client.messages.MessageMethods.edit_message>`
|
|
||||||
or `message.edit <telethon.tl.custom.message.Message.edit>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.edit_message(message, 'New text')
|
|
||||||
# or
|
|
||||||
message.edit('New text')
|
|
||||||
# or
|
|
||||||
client.edit_message(chat, message_id, 'New text')
|
|
||||||
|
|
||||||
Deleting Messages
|
|
||||||
*****************
|
|
||||||
|
|
||||||
With `delete_messages <telethon.client.messages.MessageMethods.delete_messages>`
|
|
||||||
or `message.delete <telethon.tl.custom.message.Message.delete>`. Note that the
|
|
||||||
first one supports deleting entire chats at once!:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.delete_messages(chat, messages)
|
|
||||||
# or
|
|
||||||
message.delete()
|
|
||||||
|
|
||||||
|
|
||||||
Marking Messages as Read
|
|
||||||
************************
|
|
||||||
|
|
||||||
Marking messages up to a certain point as read with `send_read_acknowledge
|
|
||||||
<telethon.client.messages.MessageMethods.send_read_acknowledge>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
client.send_read_acknowledge(last_message)
|
|
||||||
# or
|
|
||||||
client.send_read_acknowledge(last_message_id)
|
|
||||||
# or
|
|
||||||
client.send_read_acknowledge(messages)
|
|
||||||
|
|
||||||
|
|
||||||
Getting Entities
|
|
||||||
****************
|
|
||||||
|
|
||||||
Entities are users, chats, or channels. You can get them by their ID if
|
|
||||||
you have seen them before (e.g. you probably need to get all dialogs or
|
|
||||||
all the members from a chat first):
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon import utils
|
|
||||||
|
|
||||||
me = client.get_entity('me')
|
|
||||||
print(utils.get_display_name(me))
|
|
||||||
|
|
||||||
chat = client.get_input_entity('username')
|
|
||||||
for message in client.iter_messages(chat):
|
|
||||||
...
|
|
||||||
|
|
||||||
# Note that you could have used the username directly, but it's
|
|
||||||
# good to use get_input_entity if you will reuse it a lot.
|
|
||||||
for message in client.iter_messages('username'):
|
|
||||||
...
|
|
||||||
|
|
||||||
some_id = client.get_peer_id('+34123456789')
|
|
||||||
|
|
||||||
The documentation for shown methods are `get_entity
|
|
||||||
<telethon.client.users.UserMethods.get_entity>`, `get_input_entity
|
|
||||||
<telethon.client.users.UserMethods.get_input_entity>` and `get_peer_id
|
|
||||||
<telethon.client.users.UserMethods.get_peer_id>`.
|
|
||||||
|
|
||||||
Note that the utils package also has a `get_peer_id
|
|
||||||
<telethon.utils.get_peer_id>` but it won't work with things
|
|
||||||
that need access to the network such as usernames or phones,
|
|
||||||
which need to be in your contact list.
|
|
|
@ -1,138 +0,0 @@
|
||||||
=====================
|
|
||||||
Working with messages
|
|
||||||
=====================
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
|
|
||||||
Forwarding messages
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.client.messages.MessageMethods.forward_messages`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method automatically accepts either a single message or many of them.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# If you only have the message IDs
|
|
||||||
client.forward_messages(
|
|
||||||
entity, # to which entity you are forwarding the messages
|
|
||||||
message_ids, # the IDs of the messages (or message) to forward
|
|
||||||
from_entity # who sent the messages?
|
|
||||||
)
|
|
||||||
|
|
||||||
# If you have ``Message`` objects
|
|
||||||
client.forward_messages(
|
|
||||||
entity, # to which entity you are forwarding the messages
|
|
||||||
messages # the messages (or message) to forward
|
|
||||||
)
|
|
||||||
|
|
||||||
# You can also do it manually if you prefer
|
|
||||||
from telethon.tl.functions.messages import ForwardMessagesRequest
|
|
||||||
|
|
||||||
messages = foo() # retrieve a few messages (or even one, in a list)
|
|
||||||
from_entity = bar()
|
|
||||||
to_entity = baz()
|
|
||||||
|
|
||||||
client(ForwardMessagesRequest(
|
|
||||||
from_peer=from_entity, # who sent these messages?
|
|
||||||
id=[msg.id for msg in messages], # which are the messages?
|
|
||||||
to_peer=to_entity # who are we forwarding them to?
|
|
||||||
))
|
|
||||||
|
|
||||||
The named arguments are there for clarity, although they're not needed because
|
|
||||||
they appear in order. You can obviously just wrap a single message on the list
|
|
||||||
too, if that's all you have.
|
|
||||||
|
|
||||||
|
|
||||||
Searching Messages
|
|
||||||
*******************
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Use the `telethon.client.messages.MessageMethods.iter_messages`
|
|
||||||
friendly method instead unless you have a better reason not to!
|
|
||||||
|
|
||||||
This method has ``search`` and ``filter`` parameters that will
|
|
||||||
suit your needs.
|
|
||||||
|
|
||||||
Messages are searched through the obvious :tl:`SearchRequest`, but you may run
|
|
||||||
into issues_. A valid example would be:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from telethon.tl.functions.messages import SearchRequest
|
|
||||||
from telethon.tl.types import InputMessagesFilterEmpty
|
|
||||||
|
|
||||||
filter = InputMessagesFilterEmpty()
|
|
||||||
result = client(SearchRequest(
|
|
||||||
peer=peer, # On which chat/conversation
|
|
||||||
q='query', # What to search for
|
|
||||||
filter=filter, # Filter to use (maybe filter for media)
|
|
||||||
min_date=None, # Minimum date
|
|
||||||
max_date=None, # Maximum date
|
|
||||||
offset_id=0, # ID of the message to use as offset
|
|
||||||
add_offset=0, # Additional offset
|
|
||||||
limit=10, # How many results
|
|
||||||
max_id=0, # Maximum message ID
|
|
||||||
min_id=0, # Minimum message ID
|
|
||||||
from_id=None, # Who must have sent the message (peer)
|
|
||||||
hash=0 # Special number to return nothing on no-change
|
|
||||||
))
|
|
||||||
|
|
||||||
It's important to note that the optional parameter ``from_id`` could have
|
|
||||||
been omitted (defaulting to ``None``). Changing it to :tl:`InputUserEmpty`, as one
|
|
||||||
could think to specify "no user", won't work because this parameter is a flag,
|
|
||||||
and it being unspecified has a different meaning.
|
|
||||||
|
|
||||||
If one were to set ``from_id=InputUserEmpty()``, it would filter messages
|
|
||||||
from "empty" senders, which would likely match no users.
|
|
||||||
|
|
||||||
If you get a ``ChatAdminRequiredError`` on a channel, it's probably because
|
|
||||||
you tried setting the ``from_id`` filter, and as the error says, you can't
|
|
||||||
do that. Leave it set to ``None`` and it should work.
|
|
||||||
|
|
||||||
As with every method, make sure you use the right ID/hash combination for
|
|
||||||
your :tl:`InputUser` or :tl:`InputChat`, or you'll likely run into errors like
|
|
||||||
``UserIdInvalidError``.
|
|
||||||
|
|
||||||
|
|
||||||
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 = 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 = client(GetStickerSetRequest(
|
|
||||||
stickerset=InputStickerSetID(
|
|
||||||
id=sticker_set.id, access_hash=sticker_set.access_hash
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
# Stickers are nothing more than files, so send that
|
|
||||||
client.send_file('me', stickers.documents[0])
|
|
||||||
|
|
||||||
|
|
||||||
.. _issues: https://github.com/LonamiWebs/Telethon/issues/215
|
|
|
@ -1,27 +0,0 @@
|
||||||
========================================
|
|
||||||
Deleted, Limited or Deactivated Accounts
|
|
||||||
========================================
|
|
||||||
|
|
||||||
If you're from Iran or Russia, 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.
|
|
||||||
|
|
||||||
We have also had reports from Kazakhstan and China, where connecting
|
|
||||||
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.
|
|
||||||
|
|
||||||
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``,
|
|
||||||
which means you're limited, for instance,
|
|
||||||
when sending a message to some accounts but not others.
|
|
||||||
|
|
||||||
For more discussion, please see `issue 297`__.
|
|
||||||
|
|
||||||
|
|
||||||
__ https://t.me/SpamBot
|
|
||||||
__ https://github.com/LonamiWebs/Telethon/issues/297
|
|
|
@ -1,40 +0,0 @@
|
||||||
================
|
|
||||||
Enabling Logging
|
|
||||||
================
|
|
||||||
|
|
||||||
Telethon makes use of the `logging`__ module, and you can enable it as follows:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
The library has the `NullHandler`__ added by default so that no log calls
|
|
||||||
will be printed unless you explicitly enable it.
|
|
||||||
|
|
||||||
You can also `use the module`__ on your own project very easily:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
logger.debug('Debug messages')
|
|
||||||
logger.info('Useful information')
|
|
||||||
logger.warning('This is a warning!')
|
|
||||||
|
|
||||||
|
|
||||||
If you want to enable ``logging`` for your project *but* use a different
|
|
||||||
log level for the library:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
# For instance, show only warnings and above
|
|
||||||
logging.getLogger('telethon').setLevel(level=logging.WARNING)
|
|
||||||
|
|
||||||
|
|
||||||
__ https://docs.python.org/3/library/logging.html
|
|
||||||
__ https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library
|
|
||||||
__ https://docs.python.org/3/howto/logging.html
|
|
|
@ -1,29 +0,0 @@
|
||||||
==========
|
|
||||||
RPC Errors
|
|
||||||
==========
|
|
||||||
|
|
||||||
RPC stands for Remote Procedure Call, and when the library raises
|
|
||||||
a ``RPCError``, it's because you have invoked some of the API
|
|
||||||
methods incorrectly (wrong parameters, wrong permissions, or even
|
|
||||||
something went wrong on Telegram's server). All the errors are
|
|
||||||
available in :ref:`telethon-errors-package`, but some examples are:
|
|
||||||
|
|
||||||
- ``FloodWaitError`` (420), the same request was repeated many times.
|
|
||||||
Must wait ``.seconds`` (you can access this parameter).
|
|
||||||
- ``SessionPasswordNeededError``, if you have setup two-steps
|
|
||||||
verification on Telegram.
|
|
||||||
- ``CdnFileTamperedError``, if the media you were trying to download
|
|
||||||
from a CDN has been altered.
|
|
||||||
- ``ChatAdminRequiredError``, you don't have permissions to perform
|
|
||||||
said operation on a chat or channel. Try avoiding filters, i.e. when
|
|
||||||
searching messages.
|
|
||||||
|
|
||||||
The generic classes for different error codes are:
|
|
||||||
|
|
||||||
- ``InvalidDCError`` (303), the request must be repeated on another DC.
|
|
||||||
- ``BadRequestError`` (400), the request contained errors.
|
|
||||||
- ``UnauthorizedError`` (401), the user is not authorized yet.
|
|
||||||
- ``ForbiddenError`` (403), privacy violation error.
|
|
||||||
- ``NotFoundError`` (404), make sure you're invoking ``Request``\ 's!
|
|
||||||
|
|
||||||
If the error is not recognised, it will only be an ``RPCError``.
|
|
|
@ -1,43 +1,32 @@
|
||||||
.. Telethon documentation master file, created by
|
========================
|
||||||
sphinx-quickstart on Fri Nov 17 15:36:11 2017.
|
Telethon's Documentation
|
||||||
You can adapt this file completely to your liking, but it should at least
|
========================
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
====================================
|
.. code-block:: python
|
||||||
Welcome to Telethon's documentation!
|
|
||||||
====================================
|
from telethon.sync import TelegramClient, events
|
||||||
|
|
||||||
|
with TelegramClient('name', api_id, api_hash) as client:
|
||||||
|
client.send_message('me', 'Hello, myself!')
|
||||||
|
print(client.download_profile_photo('me'))
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(pattern='(?i).*Hello'))
|
||||||
|
async def handler(event):
|
||||||
|
await event.reply('Hey!')
|
||||||
|
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
Pure Python 3 Telegram client library.
|
* Are you new here? Jump straight into :ref:`installation`!
|
||||||
Official Site `here <https://lonamiwebs.github.io/Telethon>`_.
|
* Looking for the method reference? See :ref:`client-ref`.
|
||||||
Please follow the links on the index below to navigate from here,
|
* Did you upgrade the library? Please read :ref:`changelog`.
|
||||||
or use the menu on the left. Remember to read the :ref:`changelog`
|
* Used Telethon before v1.0? See :ref:`compatibility-and-convenience`.
|
||||||
when you upgrade!
|
* Coming from Bot API or want to create new bots? See :ref:`botapi`.
|
||||||
|
* Need the full API reference? https://tl.telethon.dev/.
|
||||||
.. important::
|
|
||||||
If you're new here, you want to read :ref:`getting-started`. If you're
|
|
||||||
looking for the method reference, you should check :ref:`telethon-client`.
|
|
||||||
|
|
||||||
The mentioned :ref:`telethon-client` is an important section and it
|
|
||||||
contains the friendly methods that **you should use** most of the time.
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
The library uses `asyncio <https://docs.python.org/3/library/asyncio.html>`_
|
|
||||||
under the hood, but you don't need to know anything about it unless you're
|
|
||||||
going to work with updates! If you're a user of Telethon pre-1.0 and you
|
|
||||||
aren't ready to convert your event handlers into ``async``, you can use
|
|
||||||
`a simpler version <https://github.com/LonamiWebs/Telethon/tree/sync>`_
|
|
||||||
(select the "sync" version in ``readthedocs``' bottom left corner).
|
|
||||||
|
|
||||||
If you used Telethon pre-1.0 but your scripts don't use updates or threads,
|
|
||||||
running ``import telethon.sync`` should make them Just Work. Otherwise,
|
|
||||||
we have :ref:`asyncio-magic` to teach you why ``asyncio`` is good and
|
|
||||||
how to use it.
|
|
||||||
|
|
||||||
|
|
||||||
What is this?
|
What is this?
|
||||||
*************
|
-------------
|
||||||
|
|
||||||
Telegram is a popular messaging application. This library is meant
|
Telegram is a popular messaging application. This library is meant
|
||||||
to make it easy for you to write Python programs that can interact
|
to make it easy for you to write Python programs that can interact
|
||||||
|
@ -45,93 +34,87 @@ with Telegram. Think of it as a wrapper that has already done the
|
||||||
heavy job for you, so you can focus on developing an application.
|
heavy job for you, so you can focus on developing an application.
|
||||||
|
|
||||||
|
|
||||||
.. _installation-and-usage:
|
How should I use the documentation?
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
If you are getting started with the library, you should follow the
|
||||||
|
documentation in order by pressing the "Next" button at the bottom-right
|
||||||
|
of every page.
|
||||||
|
|
||||||
|
You can also use the menu on the left to quickly skip over sections.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: Installation and Simple Usage
|
:caption: First Steps
|
||||||
|
|
||||||
extra/basic/getting-started
|
basic/installation
|
||||||
extra/basic/installation
|
basic/signing-in
|
||||||
extra/basic/creating-a-client
|
basic/quick-start
|
||||||
extra/basic/telegram-client
|
basic/updates
|
||||||
extra/basic/entities
|
basic/next-steps
|
||||||
extra/basic/asyncio-magic
|
|
||||||
extra/basic/working-with-updates
|
|
||||||
|
|
||||||
|
|
||||||
.. _Advanced-usage:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: Advanced Usage
|
:caption: Quick References
|
||||||
|
|
||||||
extra/advanced-usage/accessing-the-full-api
|
quick-references/faq
|
||||||
extra/advanced-usage/sessions
|
quick-references/client-reference
|
||||||
extra/advanced-usage/update-modes
|
quick-references/events-reference
|
||||||
extra/advanced-usage/mastering-telethon
|
quick-references/objects-reference
|
||||||
|
|
||||||
|
|
||||||
.. _Examples:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: Examples
|
:caption: Concepts
|
||||||
|
|
||||||
extra/examples/telegram-client
|
concepts/strings
|
||||||
extra/examples/working-with-messages
|
concepts/entities
|
||||||
extra/examples/chats-and-channels
|
concepts/chats-vs-channels
|
||||||
extra/examples/users
|
concepts/updates
|
||||||
extra/examples/bots
|
concepts/sessions
|
||||||
extra/examples/projects-using-telethon
|
concepts/full-api
|
||||||
|
concepts/errors
|
||||||
|
concepts/botapi-vs-mtproto
|
||||||
.. _Troubleshooting:
|
concepts/asyncio
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: Troubleshooting
|
:caption: Full API Examples
|
||||||
|
|
||||||
extra/troubleshooting/enable-logging
|
examples/word-of-warning
|
||||||
extra/troubleshooting/deleted-limited-or-deactivated-accounts
|
examples/chats-and-channels
|
||||||
extra/troubleshooting/rpc-errors
|
examples/users
|
||||||
|
examples/working-with-messages
|
||||||
|
|
||||||
.. _Developing:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: Developing
|
:caption: Developing
|
||||||
|
|
||||||
extra/developing/philosophy.rst
|
developing/philosophy.rst
|
||||||
extra/developing/api-status.rst
|
developing/test-servers.rst
|
||||||
extra/developing/test-servers.rst
|
developing/project-structure.rst
|
||||||
extra/developing/project-structure.rst
|
developing/coding-style.rst
|
||||||
extra/developing/coding-style.rst
|
developing/testing.rst
|
||||||
extra/developing/understanding-the-type-language.rst
|
developing/understanding-the-type-language.rst
|
||||||
extra/developing/tips-for-porting-the-project.rst
|
developing/tips-for-porting-the-project.rst
|
||||||
extra/developing/telegram-api-in-other-languages.rst
|
developing/telegram-api-in-other-languages.rst
|
||||||
|
|
||||||
|
|
||||||
.. _More:
|
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:hidden:
|
||||||
:caption: More
|
:caption: Miscellaneous
|
||||||
|
|
||||||
extra/changelog
|
|
||||||
extra/wall-of-shame.rst
|
|
||||||
|
|
||||||
|
misc/changelog
|
||||||
|
misc/wall-of-shame.rst
|
||||||
|
misc/compatibility-and-convenience
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:caption: Telethon modules
|
:hidden:
|
||||||
|
:caption: Telethon Modules
|
||||||
|
|
||||||
modules
|
modules/client
|
||||||
|
modules/events
|
||||||
|
modules/custom
|
||||||
Indices and tables
|
modules/utils
|
||||||
==================
|
modules/errors
|
||||||
|
modules/sessions
|
||||||
* :ref:`genindex`
|
modules/network
|
||||||
* :ref:`modindex`
|
modules/helpers
|
||||||
* :ref:`search`
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
185
readthedocs/misc/compatibility-and-convenience.rst
Normal file
185
readthedocs/misc/compatibility-and-convenience.rst
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
.. _compatibility-and-convenience:
|
||||||
|
|
||||||
|
=============================
|
||||||
|
Compatibility and Convenience
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Telethon is an `asyncio` library. Compatibility is an important concern,
|
||||||
|
and while it can't always be kept and mistakes happens, the :ref:`changelog`
|
||||||
|
is there to tell you when these important changes happen.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
Compatibility
|
||||||
|
=============
|
||||||
|
|
||||||
|
Some decisions when developing will inevitable be proven wrong in the future.
|
||||||
|
One of these decisions was using threads. Now that Python 3.4 is reaching EOL
|
||||||
|
and using `asyncio` is usable as of Python 3.5 it makes sense for a library
|
||||||
|
like Telethon to make a good use of it.
|
||||||
|
|
||||||
|
If you have old code, **just use old versions** of the library! There is
|
||||||
|
nothing wrong with that other than not getting new updates or fixes, but
|
||||||
|
using a fixed version with ``pip install telethon==0.19.1.6`` is easy
|
||||||
|
enough to do.
|
||||||
|
|
||||||
|
You might want to consider using `Virtual Environments
|
||||||
|
<https://docs.python.org/3/tutorial/venv.html>`_ in your projects.
|
||||||
|
|
||||||
|
There's no point in maintaining a synchronous version because the whole point
|
||||||
|
is that people don't have time to upgrade, and there has been several changes
|
||||||
|
and clean-ups. Using an older version is the right way to go.
|
||||||
|
|
||||||
|
Sometimes, other small decisions are made. These all will be reflected in the
|
||||||
|
:ref:`changelog` which you should read when upgrading.
|
||||||
|
|
||||||
|
If you want to jump the `asyncio` boat, here are some of the things you will
|
||||||
|
need to start migrating really old code:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# 1. Import the client from telethon.sync
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
|
||||||
|
# 2. Change this monster...
|
||||||
|
try:
|
||||||
|
assert client.connect()
|
||||||
|
if not client.is_user_authorized():
|
||||||
|
client.send_code_request(phone_number)
|
||||||
|
me = client.sign_in(phone_number, input('Enter code: '))
|
||||||
|
|
||||||
|
... # REST OF YOUR CODE
|
||||||
|
finally:
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
# ...for this:
|
||||||
|
with client:
|
||||||
|
... # REST OF YOUR CODE
|
||||||
|
|
||||||
|
# 3. client.idle() no longer exists.
|
||||||
|
# Change this...
|
||||||
|
client.idle()
|
||||||
|
# ...to this:
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
# 4. client.add_update_handler no longer exists.
|
||||||
|
# Change this...
|
||||||
|
client.add_update_handler(handler)
|
||||||
|
# ...to this:
|
||||||
|
client.add_event_handler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
In addition, all the update handlers must be ``async def``, and you need
|
||||||
|
to ``await`` method calls that rely on network requests, such as getting
|
||||||
|
the chat or sender. If you don't use updates, you're done!
|
||||||
|
|
||||||
|
|
||||||
|
Convenience
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The entire documentation assumes you have done one of the following:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import TelegramClient, sync
|
||||||
|
# or
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
|
||||||
|
This makes the examples shorter and easier to think about.
|
||||||
|
|
||||||
|
For quick scripts that don't need updates, it's a lot more convenient to
|
||||||
|
forget about `asyncio` and just work with sequential code. This can prove
|
||||||
|
to be a powerful hybrid for running under the Python REPL too.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon.sync import TelegramClient
|
||||||
|
# ^~~~~ note this part; it will manage the asyncio loop for you
|
||||||
|
|
||||||
|
with TelegramClient(...) as client:
|
||||||
|
print(client.get_me().username)
|
||||||
|
# ^ notice the lack of await, or loop.run_until_complete().
|
||||||
|
# Since there is no loop running, this is done behind the scenes.
|
||||||
|
#
|
||||||
|
message = client.send_message('me', 'Hi!')
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
message.delete()
|
||||||
|
|
||||||
|
# You can also have an hybrid between a synchronous
|
||||||
|
# part and asynchronous event handlers.
|
||||||
|
#
|
||||||
|
from telethon import events
|
||||||
|
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||||
|
async def handler(event):
|
||||||
|
await event.reply('hey')
|
||||||
|
|
||||||
|
client.run_until_disconnected()
|
||||||
|
|
||||||
|
|
||||||
|
Some methods, such as ``with``, ``start``, ``disconnect`` and
|
||||||
|
``run_until_disconnected`` work both in synchronous and asynchronous
|
||||||
|
contexts by default for convenience, and to avoid the little overhead
|
||||||
|
it has when using methods like sending a message, getting messages, etc.
|
||||||
|
This keeps the best of both worlds as a sane default.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
As a rule of thumb, if you're inside an ``async def`` and you need
|
||||||
|
the client, you need to ``await`` calls to the API. If you call other
|
||||||
|
functions that also need API calls, make them ``async def`` and ``await``
|
||||||
|
them too. Otherwise, there is no need to do so with this mode.
|
||||||
|
|
||||||
|
Speed
|
||||||
|
=====
|
||||||
|
|
||||||
|
When you're ready to micro-optimize your application, or if you simply
|
||||||
|
don't need to call any non-basic methods from a synchronous context,
|
||||||
|
just get rid of ``telethon.sync`` and work inside an ``async def``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from telethon import TelegramClient, events
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with TelegramClient(...) as client:
|
||||||
|
print((await client.get_me()).username)
|
||||||
|
# ^_____________________^ notice these parenthesis
|
||||||
|
# You want to ``await`` the call, not the username.
|
||||||
|
#
|
||||||
|
message = await client.send_message('me', 'Hi!')
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
@client.on(events.NewMessage(pattern='(?i)hi|hello'))
|
||||||
|
async def handler(event):
|
||||||
|
await event.reply('hey')
|
||||||
|
|
||||||
|
await client.run_until_disconnected()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
|
The ``telethon.sync`` magic module essentially wraps every method behind:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
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
|
||||||
|
========
|
||||||
|
|
||||||
|
You know the library uses `asyncio` everywhere, and you want to learn
|
||||||
|
how to do things right. Even though `asyncio` is its own topic, the
|
||||||
|
documentation wants you to learn how to use Telethon correctly, and for
|
||||||
|
that, you need to use `asyncio` correctly too. For this reason, there
|
||||||
|
is a section called :ref:`mastering-asyncio` that will introduce you to
|
||||||
|
the `asyncio` world, with links to more resources for learning how to
|
||||||
|
use it. Feel free to check that section out once you have read the rest.
|
|
@ -10,7 +10,7 @@ library. Said section is **not** for issues on *your* program but rather
|
||||||
issues with Telethon itself.
|
issues with Telethon itself.
|
||||||
|
|
||||||
If you have not made the effort to 1. read through the docs and 2.
|
If you have not made the effort to 1. read through the docs and 2.
|
||||||
`look for the method you need <https://lonamiwebs.github.io/Telethon/>`__,
|
`look for the method you need <https://tl.telethon.dev/>`__,
|
||||||
you will end up on the `Wall of
|
you will end up on the `Wall of
|
||||||
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
|
Shame <https://github.com/LonamiWebs/Telethon/issues?q=is%3Aissue+label%3ARTFM+is%3Aclosed>`__,
|
||||||
i.e. all issues labeled
|
i.e. all issues labeled
|
|
@ -1,7 +0,0 @@
|
||||||
telethon
|
|
||||||
========
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 3
|
|
||||||
|
|
||||||
telethon
|
|
|
@ -1,38 +1,66 @@
|
||||||
.. _telethon-client:
|
.. _telethon-client:
|
||||||
|
|
||||||
|
==============
|
||||||
|
TelegramClient
|
||||||
|
==============
|
||||||
|
|
||||||
telethon\.client package
|
.. currentmodule:: telethon.client
|
||||||
========================
|
|
||||||
|
|
||||||
The `telethon.TelegramClient` aggregates several mixin classes to provide
|
The `TelegramClient <telegramclient.TelegramClient>` aggregates several mixin
|
||||||
all the common functionality in a nice, Pythonic interface. Each mixin has
|
classes to provide all the common functionality in a nice, Pythonic interface.
|
||||||
its own methods, which you all can use.
|
Each mixin has its own methods, which you all can use.
|
||||||
|
|
||||||
**In short, to create a client you must run:**
|
**In short, to create a client you must run:**
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from telethon import TelegramClient
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
client = TelegramClient(name, api_id, api_hash)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
client = await TelegramClient(name, api_id, api_hash).start()
|
|
||||||
# Now you can use all client methods listed below, like for example...
|
# Now you can use all client methods listed below, like for example...
|
||||||
await client.send_message('me', 'Hello to myself!')
|
await client.send_message('me', 'Hello to myself!')
|
||||||
|
|
||||||
asyncio.get_event_loop().run_until_complete(main())
|
with client:
|
||||||
|
client.loop.run_until_complete(main())
|
||||||
|
|
||||||
|
|
||||||
You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.
|
You **don't** need to import these `AuthMethods`, `MessageMethods`, etc.
|
||||||
Together they are the `telethon.TelegramClient` and you can access all of
|
Together they are the `TelegramClient <telegramclient.TelegramClient>` and
|
||||||
their methods.
|
you can access all of their methods.
|
||||||
|
|
||||||
|
See :ref:`client-ref` for a short summary.
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.telegramclient
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.telegrambaseclient
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.account
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. automodule:: telethon.client.auth
|
.. automodule:: telethon.client.auth
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.bots
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.buttons
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. automodule:: telethon.client.chats
|
.. automodule:: telethon.client.chats
|
||||||
:members:
|
:members:
|
||||||
|
@ -44,6 +72,11 @@ their methods.
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.client.downloads
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. automodule:: telethon.client.messageparse
|
.. automodule:: telethon.client.messageparse
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
@ -59,11 +92,6 @@ their methods.
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
.. automodule:: telethon.client.downloads
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.uploads
|
.. automodule:: telethon.client.uploads
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
|
@ -73,9 +101,3 @@ their methods.
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.client.telegrambaseclient
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
163
readthedocs/modules/custom.rst
Normal file
163
readthedocs/modules/custom.rst
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
==============
|
||||||
|
Custom package
|
||||||
|
==============
|
||||||
|
|
||||||
|
The `telethon.tl.custom` package contains custom classes that the library
|
||||||
|
uses in order to make working with Telegram easier. Only those that you
|
||||||
|
are supposed to use will be documented here. You can use undocumented ones
|
||||||
|
at your own risk.
|
||||||
|
|
||||||
|
More often than not, you don't need to import these (unless you want
|
||||||
|
type hinting), nor do you need to manually create instances of these
|
||||||
|
classes. They are returned by client methods.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
AdminLogEvent
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.adminlogevent
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Button
|
||||||
|
======
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.button
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
ChatGetter
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.chatgetter
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Conversation
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.conversation
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Dialog
|
||||||
|
======
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.dialog
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Draft
|
||||||
|
=====
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.draft
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
File
|
||||||
|
====
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.file
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Forward
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.forward
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
InlineBuilder
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.inlinebuilder
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
InlineResult
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.inlineresult
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
InlineResults
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.inlineresults
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
Message
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.message
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
MessageButton
|
||||||
|
=============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.messagebutton
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
ParticipantPermissions
|
||||||
|
======================
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.participantpermissions
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
QRLogin
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.qrlogin
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
SenderGetter
|
||||||
|
============
|
||||||
|
|
||||||
|
.. automodule:: telethon.tl.custom.sendergetter
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
20
readthedocs/modules/errors.rst
Normal file
20
readthedocs/modules/errors.rst
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
.. _telethon-errors:
|
||||||
|
|
||||||
|
==========
|
||||||
|
API Errors
|
||||||
|
==========
|
||||||
|
|
||||||
|
These are the base errors that Telegram's API may raise.
|
||||||
|
|
||||||
|
See :ref:`rpc-errors` for a more in-depth explanation on how to handle all
|
||||||
|
known possible errors and learning to determine what a method may raise.
|
||||||
|
|
||||||
|
.. automodule:: telethon.errors.common
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.errors.rpcbaseerrors
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
|
@ -1,9 +1,12 @@
|
||||||
.. _telethon-events-package:
|
.. _telethon-events:
|
||||||
|
|
||||||
telethon\.events package
|
=============
|
||||||
========================
|
Update Events
|
||||||
|
=============
|
||||||
|
|
||||||
Every event (builder) subclasses `telethon.events.common.EventBuilder`,
|
.. currentmodule:: telethon.events
|
||||||
|
|
||||||
|
Every event (builder) subclasses `common.EventBuilder`,
|
||||||
so all the methods in it can be used from any event builder/event instance.
|
so all the methods in it can be used from any event builder/event instance.
|
||||||
|
|
||||||
.. automodule:: telethon.events.common
|
.. automodule:: telethon.events.common
|
||||||
|
@ -11,62 +14,56 @@ so all the methods in it can be used from any event builder/event instance.
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.newmessage
|
.. automodule:: telethon.events.newmessage
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.chataction
|
.. automodule:: telethon.events.chataction
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.userupdate
|
.. automodule:: telethon.events.userupdate
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messageedited
|
.. automodule:: telethon.events.messageedited
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messagedeleted
|
.. automodule:: telethon.events.messagedeleted
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.messageread
|
.. automodule:: telethon.events.messageread
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.callbackquery
|
.. automodule:: telethon.events.callbackquery
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events.inlinequery
|
.. automodule:: telethon.events.inlinequery
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.events.album
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
.. automodule:: telethon.events.raw
|
.. automodule:: telethon.events.raw
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: telethon.events
|
.. automodule:: telethon.events
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
8
readthedocs/modules/helpers.rst
Normal file
8
readthedocs/modules/helpers.rst
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
=======
|
||||||
|
Helpers
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. automodule:: telethon.helpers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
33
readthedocs/modules/network.rst
Normal file
33
readthedocs/modules/network.rst
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.. _telethon-network:
|
||||||
|
|
||||||
|
================
|
||||||
|
Connection Modes
|
||||||
|
================
|
||||||
|
|
||||||
|
The only part about network that you should worry about are
|
||||||
|
the different connection modes, which are the following:
|
||||||
|
|
||||||
|
.. automodule:: telethon.network.connection.tcpfull
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.network.connection.tcpabridged
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.network.connection.tcpintermediate
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.network.connection.tcpobfuscated
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.network.connection.http
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
27
readthedocs/modules/sessions.rst
Normal file
27
readthedocs/modules/sessions.rst
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
.. _telethon-sessions:
|
||||||
|
|
||||||
|
========
|
||||||
|
Sessions
|
||||||
|
========
|
||||||
|
|
||||||
|
These are the different built-in session storage that you may subclass.
|
||||||
|
|
||||||
|
.. automodule:: telethon.sessions.abstract
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.sessions.memory
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.sessions.sqlite
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. automodule:: telethon.sessions.string
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
12
readthedocs/modules/utils.rst
Normal file
12
readthedocs/modules/utils.rst
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
.. _telethon-utils:
|
||||||
|
|
||||||
|
=========
|
||||||
|
Utilities
|
||||||
|
=========
|
||||||
|
|
||||||
|
These are the utilities that the library has to offer.
|
||||||
|
|
||||||
|
.. automodule:: telethon.utils
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
202
readthedocs/quick-references/client-reference.rst
Normal file
202
readthedocs/quick-references/client-reference.rst
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
.. _client-ref:
|
||||||
|
|
||||||
|
================
|
||||||
|
Client Reference
|
||||||
|
================
|
||||||
|
|
||||||
|
This page contains a summary of all the important methods and properties that
|
||||||
|
you may need when using Telethon. They are sorted by relevance and are not in
|
||||||
|
alphabetical order.
|
||||||
|
|
||||||
|
You should use this page to learn about which methods are available, and
|
||||||
|
if you need a usage example or further description of the arguments, be
|
||||||
|
sure to follow the links.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
TelegramClient
|
||||||
|
==============
|
||||||
|
|
||||||
|
This is a summary of the methods and
|
||||||
|
properties you will find at :ref:`telethon-client`.
|
||||||
|
|
||||||
|
Auth
|
||||||
|
----
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.auth.AuthMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
start
|
||||||
|
send_code_request
|
||||||
|
sign_in
|
||||||
|
qr_login
|
||||||
|
log_out
|
||||||
|
edit_2fa
|
||||||
|
|
||||||
|
Base
|
||||||
|
----
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.telegrambaseclient.TelegramBaseClient
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
connect
|
||||||
|
disconnect
|
||||||
|
is_connected
|
||||||
|
disconnected
|
||||||
|
loop
|
||||||
|
set_proxy
|
||||||
|
|
||||||
|
Messages
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.messages.MessageMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
send_message
|
||||||
|
edit_message
|
||||||
|
delete_messages
|
||||||
|
forward_messages
|
||||||
|
iter_messages
|
||||||
|
get_messages
|
||||||
|
pin_message
|
||||||
|
unpin_message
|
||||||
|
send_read_acknowledge
|
||||||
|
|
||||||
|
Uploads
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.uploads.UploadMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
send_file
|
||||||
|
upload_file
|
||||||
|
|
||||||
|
Downloads
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.downloads.DownloadMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
download_media
|
||||||
|
download_profile_photo
|
||||||
|
download_file
|
||||||
|
iter_download
|
||||||
|
|
||||||
|
Dialogs
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.dialogs.DialogMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
iter_dialogs
|
||||||
|
get_dialogs
|
||||||
|
edit_folder
|
||||||
|
iter_drafts
|
||||||
|
get_drafts
|
||||||
|
delete_dialog
|
||||||
|
conversation
|
||||||
|
|
||||||
|
Users
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.users.UserMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
get_me
|
||||||
|
is_bot
|
||||||
|
is_user_authorized
|
||||||
|
get_entity
|
||||||
|
get_input_entity
|
||||||
|
get_peer_id
|
||||||
|
|
||||||
|
Chats
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.chats.ChatMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
iter_participants
|
||||||
|
get_participants
|
||||||
|
kick_participant
|
||||||
|
iter_admin_log
|
||||||
|
get_admin_log
|
||||||
|
iter_profile_photos
|
||||||
|
get_profile_photos
|
||||||
|
edit_admin
|
||||||
|
edit_permissions
|
||||||
|
get_permissions
|
||||||
|
get_stats
|
||||||
|
action
|
||||||
|
|
||||||
|
Parse Mode
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.messageparse.MessageParseMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
parse_mode
|
||||||
|
|
||||||
|
Updates
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. py:currentmodule:: telethon.client.updates.UpdateMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
on
|
||||||
|
run_until_disconnected
|
||||||
|
add_event_handler
|
||||||
|
remove_event_handler
|
||||||
|
list_event_handlers
|
||||||
|
catch_up
|
||||||
|
set_receive_updates
|
||||||
|
|
||||||
|
Bots
|
||||||
|
----
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.bots.BotMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
inline_query
|
||||||
|
|
||||||
|
Buttons
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.buttons.ButtonMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
build_reply_markup
|
||||||
|
|
||||||
|
Account
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.client.account.AccountMethods
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
takeout
|
||||||
|
end_takeout
|
247
readthedocs/quick-references/events-reference.rst
Normal file
247
readthedocs/quick-references/events-reference.rst
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
================
|
||||||
|
Events Reference
|
||||||
|
================
|
||||||
|
|
||||||
|
Here you will find a quick summary of all the methods
|
||||||
|
and properties that you can access when working with events.
|
||||||
|
|
||||||
|
You can access the client that creates this event by doing
|
||||||
|
``event.client``, and you should view the description of the
|
||||||
|
events to find out what arguments it allows on creation and
|
||||||
|
its **attributes** (the properties will be shown here).
|
||||||
|
|
||||||
|
.. important::
|
||||||
|
|
||||||
|
Remember that **all events base** `ChatGetter
|
||||||
|
<telethon.tl.custom.chatgetter.ChatGetter>`! Please see :ref:`faq`
|
||||||
|
if you don't know what this means or the implications of it.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
NewMessage
|
||||||
|
==========
|
||||||
|
|
||||||
|
Occurs whenever a new text message or a message with media arrives.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The new message event **should be treated as** a
|
||||||
|
normal `Message <telethon.tl.custom.message.Message>`, with
|
||||||
|
the following exceptions:
|
||||||
|
|
||||||
|
* ``pattern_match`` is the match object returned by ``pattern=``.
|
||||||
|
* ``message`` is **not** the message string. It's the `Message
|
||||||
|
<telethon.tl.custom.message.Message>` object.
|
||||||
|
|
||||||
|
Remember, this event is just a proxy over the message, so while
|
||||||
|
you won't see its attributes and properties, you can still access
|
||||||
|
them. Please see the full documentation for examples.
|
||||||
|
|
||||||
|
Full documentation for the `NewMessage
|
||||||
|
<telethon.events.newmessage.NewMessage>`.
|
||||||
|
|
||||||
|
|
||||||
|
MessageEdited
|
||||||
|
=============
|
||||||
|
|
||||||
|
Occurs whenever a message is edited. Just like `NewMessage
|
||||||
|
<telethon.events.newmessage.NewMessage>`, you should treat
|
||||||
|
this event as a `Message <telethon.tl.custom.message.Message>`.
|
||||||
|
|
||||||
|
Full documentation for the `MessageEdited
|
||||||
|
<telethon.events.messageedited.MessageEdited>`.
|
||||||
|
|
||||||
|
|
||||||
|
MessageDeleted
|
||||||
|
==============
|
||||||
|
|
||||||
|
Occurs whenever a message is deleted. Note that this event isn't 100%
|
||||||
|
reliable, since Telegram doesn't always notify the clients that a message
|
||||||
|
was deleted.
|
||||||
|
|
||||||
|
It only has the ``deleted_id`` and ``deleted_ids`` attributes
|
||||||
|
(in addition to the chat if the deletion happened in a channel).
|
||||||
|
|
||||||
|
Full documentation for the `MessageDeleted
|
||||||
|
<telethon.events.messagedeleted.MessageDeleted>`.
|
||||||
|
|
||||||
|
|
||||||
|
MessageRead
|
||||||
|
===========
|
||||||
|
|
||||||
|
Occurs whenever one or more messages are read in a chat.
|
||||||
|
|
||||||
|
Full documentation for the `MessageRead
|
||||||
|
<telethon.events.messageread.MessageRead>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.messageread.MessageRead.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
inbox
|
||||||
|
message_ids
|
||||||
|
|
||||||
|
get_messages
|
||||||
|
is_read
|
||||||
|
|
||||||
|
|
||||||
|
ChatAction
|
||||||
|
==========
|
||||||
|
|
||||||
|
Occurs on certain chat actions, such as chat title changes,
|
||||||
|
user join or leaves, pinned messages, photo changes, etc.
|
||||||
|
|
||||||
|
Full documentation for the `ChatAction
|
||||||
|
<telethon.events.chataction.ChatAction>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.chataction.ChatAction.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
added_by
|
||||||
|
kicked_by
|
||||||
|
user
|
||||||
|
input_user
|
||||||
|
user_id
|
||||||
|
users
|
||||||
|
input_users
|
||||||
|
user_ids
|
||||||
|
|
||||||
|
respond
|
||||||
|
reply
|
||||||
|
delete
|
||||||
|
get_pinned_message
|
||||||
|
get_added_by
|
||||||
|
get_kicked_by
|
||||||
|
get_user
|
||||||
|
get_input_user
|
||||||
|
get_users
|
||||||
|
get_input_users
|
||||||
|
|
||||||
|
|
||||||
|
UserUpdate
|
||||||
|
==========
|
||||||
|
|
||||||
|
Occurs whenever a user goes online, starts typing, etc.
|
||||||
|
|
||||||
|
Full documentation for the `UserUpdate
|
||||||
|
<telethon.events.userupdate.UserUpdate>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.userupdate.UserUpdate.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
user
|
||||||
|
input_user
|
||||||
|
user_id
|
||||||
|
|
||||||
|
get_user
|
||||||
|
get_input_user
|
||||||
|
|
||||||
|
typing
|
||||||
|
uploading
|
||||||
|
recording
|
||||||
|
playing
|
||||||
|
cancel
|
||||||
|
geo
|
||||||
|
audio
|
||||||
|
round
|
||||||
|
video
|
||||||
|
contact
|
||||||
|
document
|
||||||
|
photo
|
||||||
|
last_seen
|
||||||
|
until
|
||||||
|
online
|
||||||
|
recently
|
||||||
|
within_weeks
|
||||||
|
within_months
|
||||||
|
|
||||||
|
|
||||||
|
CallbackQuery
|
||||||
|
=============
|
||||||
|
|
||||||
|
Occurs whenever you sign in as a bot and a user
|
||||||
|
clicks one of the inline buttons on your messages.
|
||||||
|
|
||||||
|
Full documentation for the `CallbackQuery
|
||||||
|
<telethon.events.callbackquery.CallbackQuery>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.callbackquery.CallbackQuery.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
id
|
||||||
|
message_id
|
||||||
|
data
|
||||||
|
chat_instance
|
||||||
|
via_inline
|
||||||
|
|
||||||
|
respond
|
||||||
|
reply
|
||||||
|
edit
|
||||||
|
delete
|
||||||
|
answer
|
||||||
|
get_message
|
||||||
|
|
||||||
|
InlineQuery
|
||||||
|
===========
|
||||||
|
|
||||||
|
Occurs whenever you sign in as a bot and a user
|
||||||
|
sends an inline query such as ``@bot query``.
|
||||||
|
|
||||||
|
Full documentation for the `InlineQuery
|
||||||
|
<telethon.events.inlinequery.InlineQuery>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.inlinequery.InlineQuery.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
id
|
||||||
|
text
|
||||||
|
offset
|
||||||
|
geo
|
||||||
|
builder
|
||||||
|
|
||||||
|
answer
|
||||||
|
|
||||||
|
Album
|
||||||
|
=====
|
||||||
|
|
||||||
|
Occurs whenever you receive an entire album.
|
||||||
|
|
||||||
|
Full documentation for the `Album
|
||||||
|
<telethon.events.album.Album>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.events.album.Album.Event
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
grouped_id
|
||||||
|
text
|
||||||
|
raw_text
|
||||||
|
is_reply
|
||||||
|
forward
|
||||||
|
|
||||||
|
get_reply_message
|
||||||
|
respond
|
||||||
|
reply
|
||||||
|
forward_to
|
||||||
|
edit
|
||||||
|
delete
|
||||||
|
mark_read
|
||||||
|
pin
|
||||||
|
|
||||||
|
Raw
|
||||||
|
===
|
||||||
|
|
||||||
|
Raw events are not actual events. Instead, they are the raw
|
||||||
|
:tl:`Update` object that Telegram sends. You normally shouldn't
|
||||||
|
need these.
|
423
readthedocs/quick-references/faq.rst
Normal file
423
readthedocs/quick-references/faq.rst
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
.. _faq:
|
||||||
|
|
||||||
|
===
|
||||||
|
FAQ
|
||||||
|
===
|
||||||
|
|
||||||
|
Let's start the quick references section with some useful tips to keep in
|
||||||
|
mind, with the hope that you will understand why certain things work the
|
||||||
|
way that they do.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
Code without errors doesn't work
|
||||||
|
================================
|
||||||
|
|
||||||
|
Then it probably has errors, but you haven't enabled logging yet.
|
||||||
|
To enable logging, at the following code to the top of your main file:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
level=logging.CRITICAL # won't show errors (same as disabled)
|
||||||
|
level=logging.ERROR # will only show errors that you didn't handle
|
||||||
|
level=logging.WARNING # will also show messages with medium severity, such as internal Telegram issues
|
||||||
|
level=logging.INFO # will also show informational messages, such as connection or disconnections
|
||||||
|
level=logging.DEBUG # will show a lot of output to help debugging issues in the library
|
||||||
|
|
||||||
|
See the official Python documentation for more information on logging_.
|
||||||
|
|
||||||
|
|
||||||
|
How can I except FloodWaitError?
|
||||||
|
================================
|
||||||
|
|
||||||
|
You can use all errors from the API by importing:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import errors
|
||||||
|
|
||||||
|
And except them as such:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
try:
|
||||||
|
await client.send_message(chat, 'Hi')
|
||||||
|
except errors.FloodWaitError as e:
|
||||||
|
# e.seconds is how many seconds you have
|
||||||
|
# to wait before making the request again.
|
||||||
|
print('Flood for', e.seconds)
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
We have also had reports from Kazakhstan and China, where connecting
|
||||||
|
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``,
|
||||||
|
which means you're limited, for instance,
|
||||||
|
when sending a message to some accounts but not others.
|
||||||
|
|
||||||
|
For more discussion, please see `issue 297`_.
|
||||||
|
|
||||||
|
|
||||||
|
How can I use a proxy?
|
||||||
|
======================
|
||||||
|
|
||||||
|
This was one of the first things described in :ref:`signing-in`.
|
||||||
|
|
||||||
|
|
||||||
|
How do I access a field?
|
||||||
|
========================
|
||||||
|
|
||||||
|
This is basic Python knowledge. You should use the dot operator:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
me = await client.get_me()
|
||||||
|
print(me.username)
|
||||||
|
# ^ we used the dot operator to access the username attribute
|
||||||
|
|
||||||
|
result = await client(functions.photos.GetUserPhotosRequest(
|
||||||
|
user_id='me',
|
||||||
|
offset=0,
|
||||||
|
max_id=0,
|
||||||
|
limit=100
|
||||||
|
))
|
||||||
|
|
||||||
|
# Working with list is also pretty basic
|
||||||
|
print(result.photos[0].sizes[-1].type)
|
||||||
|
# ^ ^ ^ ^ ^
|
||||||
|
# | | | | \ type
|
||||||
|
# | | | \ last size
|
||||||
|
# | | \ list of sizes
|
||||||
|
# access | \ first photo from the list
|
||||||
|
# the... \ list of photos
|
||||||
|
#
|
||||||
|
# To print all, you could do (or mix-and-match):
|
||||||
|
for photo in result.photos:
|
||||||
|
for size in photo.sizes:
|
||||||
|
print(size.type)
|
||||||
|
|
||||||
|
|
||||||
|
AttributeError: 'coroutine' object has no attribute 'id'
|
||||||
|
========================================================
|
||||||
|
|
||||||
|
You either forgot to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import telethon.sync
|
||||||
|
# ^^^^^ import sync
|
||||||
|
|
||||||
|
Or:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
me = await client.get_me()
|
||||||
|
# ^^^^^ note the await
|
||||||
|
print(me.username)
|
||||||
|
|
||||||
|
|
||||||
|
sqlite3.OperationalError: database is locked
|
||||||
|
============================================
|
||||||
|
|
||||||
|
An older process is still running and is using the same ``'session'`` file.
|
||||||
|
|
||||||
|
This error occurs when **two or more clients use the same session**,
|
||||||
|
that is, when you write the same session name to be used in the client:
|
||||||
|
|
||||||
|
* You have an older process using the same session file.
|
||||||
|
* You have two different scripts running (interactive sessions count too).
|
||||||
|
* You have two clients in the same script running at the same time.
|
||||||
|
|
||||||
|
The solution is, if you need two clients, use two sessions. If the
|
||||||
|
problem persists and you're on Linux, you can use ``fuser my.session``
|
||||||
|
to find out the process locking the file. As a last resort, you can
|
||||||
|
reboot your system.
|
||||||
|
|
||||||
|
If you really dislike SQLite, use a different session storage. There
|
||||||
|
is an entire section covering that at :ref:`sessions`.
|
||||||
|
|
||||||
|
|
||||||
|
event.chat or event.sender is None
|
||||||
|
==================================
|
||||||
|
|
||||||
|
Telegram doesn't always send this information in order to save bandwidth.
|
||||||
|
If you need the information, you should fetch it yourself, since the library
|
||||||
|
won't do unnecessary work unless you need to:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
async def handler(event):
|
||||||
|
chat = await event.get_chat()
|
||||||
|
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?
|
||||||
|
==================================
|
||||||
|
|
||||||
|
In Python, classes can base others. This is called `inheritance
|
||||||
|
<https://ddg.gg/python%20inheritance>`_. What it means is that
|
||||||
|
"if a class bases another, you can use the other's methods too".
|
||||||
|
|
||||||
|
For example, `Message <telethon.tl.custom.message.Message>` *bases*
|
||||||
|
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`. In turn,
|
||||||
|
`ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` defines
|
||||||
|
things like `obj.chat_id <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||||
|
|
||||||
|
So if you have a message, you can access that too:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# ChatGetter has a chat_id property, and Message bases ChatGetter.
|
||||||
|
# Thus you can use ChatGetter properties and methods from Message
|
||||||
|
print(message.chat_id)
|
||||||
|
|
||||||
|
|
||||||
|
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?
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Yes, if you know what you are doing. However, you will probably have a
|
||||||
|
lot of headaches to get threads and asyncio to work together. Instead,
|
||||||
|
consider using `Quart <https://pgjones.gitlab.io/quart/>`_, an asyncio-based
|
||||||
|
alternative to `Flask <flask.pocoo.org/>`_.
|
||||||
|
|
||||||
|
Check out `quart_login.py`_ for an example web-application based on Quart.
|
||||||
|
|
||||||
|
Can I use Anaconda/Spyder/IPython with the library?
|
||||||
|
===================================================
|
||||||
|
|
||||||
|
Yes, but these interpreters run the asyncio event loop implicitly,
|
||||||
|
which interferes with the ``telethon.sync`` magic module.
|
||||||
|
|
||||||
|
If you use them, you should **not** import ``sync``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Change any of these...:
|
||||||
|
from telethon import TelegramClient, sync, ...
|
||||||
|
from telethon.sync import TelegramClient, ...
|
||||||
|
|
||||||
|
# ...with this:
|
||||||
|
from telethon import TelegramClient, ...
|
||||||
|
|
||||||
|
You are also more likely to get "sqlite3.OperationalError: database is locked"
|
||||||
|
with them. If they cause too much trouble, just write your code in a ``.py``
|
||||||
|
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
|
||||||
|
.. _issue 3759: https://github.com/LonamiWebs/Telethon/issues/3759
|
||||||
|
.. _quart_login.py: https://github.com/LonamiWebs/Telethon/tree/v1/telethon_examples#quart_loginpy
|
353
readthedocs/quick-references/objects-reference.rst
Normal file
353
readthedocs/quick-references/objects-reference.rst
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
=================
|
||||||
|
Objects Reference
|
||||||
|
=================
|
||||||
|
|
||||||
|
This is the quick reference for those objects returned by client methods
|
||||||
|
or other useful modules that the library has to offer. They are kept in
|
||||||
|
a separate page to help finding and discovering them.
|
||||||
|
|
||||||
|
Remember that this page only shows properties and methods,
|
||||||
|
**not attributes**. Make sure to open the full documentation
|
||||||
|
to find out about the attributes.
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
|
||||||
|
|
||||||
|
ChatGetter
|
||||||
|
==========
|
||||||
|
|
||||||
|
All events base `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`,
|
||||||
|
and some of the objects below do too, so it's important to know its methods.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.chatgetter.ChatGetter
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
chat
|
||||||
|
input_chat
|
||||||
|
chat_id
|
||||||
|
is_private
|
||||||
|
is_group
|
||||||
|
is_channel
|
||||||
|
|
||||||
|
get_chat
|
||||||
|
get_input_chat
|
||||||
|
|
||||||
|
|
||||||
|
SenderGetter
|
||||||
|
============
|
||||||
|
|
||||||
|
Similar to `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`, a
|
||||||
|
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>` is the same,
|
||||||
|
but it works for senders instead.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.sendergetter.SenderGetter
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
sender
|
||||||
|
input_sender
|
||||||
|
sender_id
|
||||||
|
|
||||||
|
get_sender
|
||||||
|
get_input_sender
|
||||||
|
|
||||||
|
|
||||||
|
Message
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.message
|
||||||
|
|
||||||
|
The `Message` type is very important, mostly because we are working
|
||||||
|
with a library for a *messaging* platform, so messages are widely used:
|
||||||
|
in events, when fetching history, replies, etc.
|
||||||
|
|
||||||
|
It bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>` and
|
||||||
|
`SenderGetter <telethon.tl.custom.sendergetter.SenderGetter>`.
|
||||||
|
|
||||||
|
Properties
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
We document *custom properties* here, not all the attributes of the
|
||||||
|
`Message` (which is the information Telegram actually returns).
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.message.Message
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
text
|
||||||
|
raw_text
|
||||||
|
is_reply
|
||||||
|
forward
|
||||||
|
buttons
|
||||||
|
button_count
|
||||||
|
file
|
||||||
|
photo
|
||||||
|
document
|
||||||
|
web_preview
|
||||||
|
audio
|
||||||
|
voice
|
||||||
|
video
|
||||||
|
video_note
|
||||||
|
gif
|
||||||
|
sticker
|
||||||
|
contact
|
||||||
|
game
|
||||||
|
geo
|
||||||
|
invoice
|
||||||
|
poll
|
||||||
|
venue
|
||||||
|
action_entities
|
||||||
|
via_bot
|
||||||
|
via_input_bot
|
||||||
|
client
|
||||||
|
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
respond
|
||||||
|
reply
|
||||||
|
forward_to
|
||||||
|
edit
|
||||||
|
delete
|
||||||
|
get_reply_message
|
||||||
|
click
|
||||||
|
mark_read
|
||||||
|
pin
|
||||||
|
download_media
|
||||||
|
get_entities_text
|
||||||
|
get_buttons
|
||||||
|
|
||||||
|
|
||||||
|
File
|
||||||
|
====
|
||||||
|
|
||||||
|
The `File <telethon.tl.custom.file.File>` type is a wrapper object
|
||||||
|
returned by `Message.file <telethon.tl.custom.message.Message.file>`,
|
||||||
|
and you can use it to easily access a document's attributes, such as
|
||||||
|
its name, bot-API style file ID, etc.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.file.File
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
id
|
||||||
|
name
|
||||||
|
ext
|
||||||
|
mime_type
|
||||||
|
width
|
||||||
|
height
|
||||||
|
size
|
||||||
|
duration
|
||||||
|
title
|
||||||
|
performer
|
||||||
|
emoji
|
||||||
|
sticker_set
|
||||||
|
|
||||||
|
|
||||||
|
Conversation
|
||||||
|
============
|
||||||
|
|
||||||
|
The `Conversation <telethon.tl.custom.conversation.Conversation>` object
|
||||||
|
is returned by the `client.conversation()
|
||||||
|
<telethon.client.dialogs.DialogMethods.conversation>` method to easily
|
||||||
|
send and receive responses like a normal conversation.
|
||||||
|
|
||||||
|
It bases `ChatGetter <telethon.tl.custom.chatgetter.ChatGetter>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.conversation.Conversation
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
send_message
|
||||||
|
send_file
|
||||||
|
mark_read
|
||||||
|
get_response
|
||||||
|
get_reply
|
||||||
|
get_edit
|
||||||
|
wait_read
|
||||||
|
wait_event
|
||||||
|
cancel
|
||||||
|
cancel_all
|
||||||
|
|
||||||
|
|
||||||
|
AdminLogEvent
|
||||||
|
=============
|
||||||
|
|
||||||
|
The `AdminLogEvent <telethon.tl.custom.adminlogevent.AdminLogEvent>` object
|
||||||
|
is returned by the `client.iter_admin_log()
|
||||||
|
<telethon.client.chats.ChatMethods.iter_admin_log>` method to easily iterate
|
||||||
|
over past "events" (deleted messages, edits, title changes, leaving members…)
|
||||||
|
|
||||||
|
These are all the properties you can find in it:
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.adminlogevent.AdminLogEvent
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
id
|
||||||
|
date
|
||||||
|
user_id
|
||||||
|
action
|
||||||
|
old
|
||||||
|
new
|
||||||
|
changed_about
|
||||||
|
changed_title
|
||||||
|
changed_username
|
||||||
|
changed_photo
|
||||||
|
changed_sticker_set
|
||||||
|
changed_message
|
||||||
|
deleted_message
|
||||||
|
changed_admin
|
||||||
|
changed_restrictions
|
||||||
|
changed_invites
|
||||||
|
joined
|
||||||
|
joined_invite
|
||||||
|
left
|
||||||
|
changed_hide_history
|
||||||
|
changed_signatures
|
||||||
|
changed_pin
|
||||||
|
changed_default_banned_rights
|
||||||
|
stopped_poll
|
||||||
|
|
||||||
|
|
||||||
|
Button
|
||||||
|
======
|
||||||
|
|
||||||
|
The `Button <telethon.tl.custom.button.Button>` class is used when you login
|
||||||
|
as a bot account to send messages with reply markup, such as inline buttons
|
||||||
|
or custom keyboards.
|
||||||
|
|
||||||
|
These are the static methods you can use to create instances of the markup:
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.button.Button
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
inline
|
||||||
|
switch_inline
|
||||||
|
url
|
||||||
|
auth
|
||||||
|
text
|
||||||
|
request_location
|
||||||
|
request_phone
|
||||||
|
request_poll
|
||||||
|
clear
|
||||||
|
force_reply
|
||||||
|
|
||||||
|
|
||||||
|
InlineResult
|
||||||
|
============
|
||||||
|
|
||||||
|
The `InlineResult <telethon.tl.custom.inlineresult.InlineResult>` object
|
||||||
|
is returned inside a list by the `client.inline_query()
|
||||||
|
<telethon.client.bots.BotMethods.inline_query>` method to make an inline
|
||||||
|
query to a bot that supports being used in inline mode, such as
|
||||||
|
`@like <https://t.me/like>`_.
|
||||||
|
|
||||||
|
Note that the list returned is in fact a *subclass* of a list called
|
||||||
|
`InlineResults <telethon.tl.custom.inlineresults.InlineResults>`, which,
|
||||||
|
in addition of being a list (iterator, indexed access, etc.), has extra
|
||||||
|
attributes and methods.
|
||||||
|
|
||||||
|
These are the constants for the types, properties and methods you
|
||||||
|
can find the individual results:
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.inlineresult.InlineResult
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
ARTICLE
|
||||||
|
PHOTO
|
||||||
|
GIF
|
||||||
|
VIDEO
|
||||||
|
VIDEO_GIF
|
||||||
|
AUDIO
|
||||||
|
DOCUMENT
|
||||||
|
LOCATION
|
||||||
|
VENUE
|
||||||
|
CONTACT
|
||||||
|
GAME
|
||||||
|
type
|
||||||
|
message
|
||||||
|
title
|
||||||
|
description
|
||||||
|
url
|
||||||
|
photo
|
||||||
|
document
|
||||||
|
click
|
||||||
|
download_media
|
||||||
|
|
||||||
|
|
||||||
|
Dialog
|
||||||
|
======
|
||||||
|
|
||||||
|
The `Dialog <telethon.tl.custom.dialog.Dialog>` object is returned when
|
||||||
|
you call `client.iter_dialogs() <telethon.client.dialogs.DialogMethods.iter_dialogs>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.dialog.Dialog
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
send_message
|
||||||
|
archive
|
||||||
|
delete
|
||||||
|
|
||||||
|
|
||||||
|
Draft
|
||||||
|
======
|
||||||
|
|
||||||
|
The `Draft <telethon.tl.custom.draft.Draft>` object is returned when
|
||||||
|
you call `client.iter_drafts() <telethon.client.dialogs.DialogMethods.iter_drafts>`.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.tl.custom.draft.Draft
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
entity
|
||||||
|
input_entity
|
||||||
|
get_entity
|
||||||
|
get_input_entity
|
||||||
|
text
|
||||||
|
raw_text
|
||||||
|
is_empty
|
||||||
|
set_message
|
||||||
|
send
|
||||||
|
delete
|
||||||
|
|
||||||
|
|
||||||
|
Utils
|
||||||
|
=====
|
||||||
|
|
||||||
|
The `telethon.utils` module has plenty of methods that make using the
|
||||||
|
library a lot easier. Only the interesting ones will be listed here.
|
||||||
|
|
||||||
|
.. currentmodule:: telethon.utils
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:nosignatures:
|
||||||
|
|
||||||
|
get_display_name
|
||||||
|
get_extension
|
||||||
|
get_inner_text
|
||||||
|
get_peer_id
|
||||||
|
resolve_id
|
||||||
|
pack_bot_file_id
|
||||||
|
resolve_bot_file_id
|
||||||
|
resolve_invite_link
|
|
@ -1 +1,2 @@
|
||||||
telethon
|
./
|
||||||
|
sphinx-rtd-theme~=1.3.0
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
.. _telethon-errors-package:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.errors package
|
|
||||||
========================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.errors\.common module
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.errors.common
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.errors\.rpcbaseerrors module
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.errors.rpcbaseerrors
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,35 +0,0 @@
|
||||||
telethon\.extensions package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.extensions\.binaryreader module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.binaryreader
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.markdown module
|
|
||||||
-------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.markdown
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.html module
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.html
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.extensions\.tcpclient module
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.extensions.tcpclient
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,35 +0,0 @@
|
||||||
telethon\.network package
|
|
||||||
=========================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.network\.connection module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.connection
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.mtprotoplainsender module
|
|
||||||
------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.mtprotoplainsender
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.mtprotosender module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.mtprotosender
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.network\.authenticator module
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.network.authenticator
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,90 +0,0 @@
|
||||||
.. _telethon-package:
|
|
||||||
|
|
||||||
|
|
||||||
telethon package
|
|
||||||
================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.client module
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.client
|
|
||||||
|
|
||||||
.. automodule:: telethon.client
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.utils module
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.utils
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.helpers module
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.helpers
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.events package
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.events
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.sessions module
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.sessions
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.errors package
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.errors
|
|
||||||
|
|
||||||
telethon\.extensions package
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.extensions
|
|
||||||
|
|
||||||
telethon\.network package
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.network
|
|
||||||
|
|
||||||
telethon\.tl package
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.tl
|
|
||||||
|
|
||||||
|
|
||||||
Module contents
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. automodule:: telethon
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,87 +0,0 @@
|
||||||
telethon\.tl\.custom package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.draft module
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.draft
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.dialog module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.dialog
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.message module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.message
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.messagebutton module
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.messagebutton
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.forward module
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.forward
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.button module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.button
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.inlineresult module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.inlineresult
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.chatgetter module
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.chatgetter
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.sendergetter module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.sendergetter
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.conversation module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.conversation
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,16 +0,0 @@
|
||||||
telethon\.tl\.custom package
|
|
||||||
============================
|
|
||||||
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
telethon.tl.custom
|
|
||||||
|
|
||||||
|
|
||||||
telethon\.tl\.tlobject module
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.tlobject
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
|
@ -1,3 +1,2 @@
|
||||||
pyaes
|
pyaes
|
||||||
rsa
|
rsa
|
||||||
async_generator
|
|
||||||
|
|
24
run_tests.py
24
run_tests.py
|
@ -1,24 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from telethon_tests import \
|
|
||||||
CryptoTests, ParserTests, TLTests, UtilsTests, NetworkTests
|
|
||||||
|
|
||||||
test_classes = [CryptoTests, ParserTests, TLTests, UtilsTests]
|
|
||||||
|
|
||||||
network = input('Run network tests (y/n)?: ').lower() == 'y'
|
|
||||||
if network:
|
|
||||||
test_classes.append(NetworkTests)
|
|
||||||
|
|
||||||
loader = unittest.TestLoader()
|
|
||||||
|
|
||||||
suites_list = []
|
|
||||||
for test_class in test_classes:
|
|
||||||
suite = loader.loadTestsFromTestCase(test_class)
|
|
||||||
suites_list.append(suite)
|
|
||||||
|
|
||||||
big_suite = unittest.TestSuite(suites_list)
|
|
||||||
|
|
||||||
runner = unittest.TextTestRunner()
|
|
||||||
results = runner.run(big_suite)
|
|
163
setup.py
163
setup.py
|
@ -15,68 +15,75 @@ import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from codecs import open
|
import sys
|
||||||
from sys import argv
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import run
|
||||||
|
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
# Needed since we're importing local files
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
class TempWorkDir:
|
class TempWorkDir:
|
||||||
"""Switches the working directory to be the one on which this file lives,
|
"""Switches the working directory to be the one on which this file lives,
|
||||||
while within the 'with' block.
|
while within the 'with' block.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self, new=None):
|
||||||
self.original = None
|
self.original = None
|
||||||
|
self.new = new or str(Path(__file__).parent.resolve())
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.original = os.path.abspath(os.path.curdir)
|
# os.chdir does not work with Path in Python 3.5.x
|
||||||
os.chdir(os.path.abspath(os.path.dirname(__file__)))
|
self.original = str(Path('.').resolve())
|
||||||
|
os.makedirs(self.new, exist_ok=True)
|
||||||
|
os.chdir(self.new)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
os.chdir(self.original)
|
os.chdir(self.original)
|
||||||
|
|
||||||
|
|
||||||
GENERATOR_DIR = 'telethon_generator'
|
API_REF_URL = 'https://tl.telethon.dev/'
|
||||||
LIBRARY_DIR = 'telethon'
|
|
||||||
|
|
||||||
ERRORS_IN_JSON = os.path.join(GENERATOR_DIR, 'data', 'errors.json')
|
GENERATOR_DIR = Path('telethon_generator')
|
||||||
ERRORS_IN_DESC = os.path.join(GENERATOR_DIR, 'data', 'error_descriptions')
|
LIBRARY_DIR = Path('telethon')
|
||||||
ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpcerrorlist.py')
|
|
||||||
|
|
||||||
INVALID_BM_IN = os.path.join(GENERATOR_DIR, 'data', 'invalid_bot_methods.json')
|
ERRORS_IN = GENERATOR_DIR / 'data/errors.csv'
|
||||||
|
ERRORS_OUT = LIBRARY_DIR / 'errors/rpcerrorlist.py'
|
||||||
|
|
||||||
TLOBJECT_IN_CORE_TL = os.path.join(GENERATOR_DIR, 'data', 'mtproto_api.tl')
|
METHODS_IN = GENERATOR_DIR / 'data/methods.csv'
|
||||||
TLOBJECT_IN_TL = os.path.join(GENERATOR_DIR, 'data', 'telegram_api.tl')
|
|
||||||
TLOBJECT_OUT = os.path.join(LIBRARY_DIR, 'tl')
|
# Which raw API methods are covered by *friendly* methods in the client?
|
||||||
|
FRIENDLY_IN = GENERATOR_DIR / 'data/friendly.csv'
|
||||||
|
|
||||||
|
TLOBJECT_IN_TLS = [Path(x) for x in GENERATOR_DIR.glob('data/*.tl')]
|
||||||
|
TLOBJECT_OUT = LIBRARY_DIR / 'tl'
|
||||||
IMPORT_DEPTH = 2
|
IMPORT_DEPTH = 2
|
||||||
|
|
||||||
DOCS_IN_RES = os.path.join(GENERATOR_DIR, 'data', 'html')
|
DOCS_IN_RES = GENERATOR_DIR / 'data/html'
|
||||||
DOCS_OUT = 'docs'
|
DOCS_OUT = Path('docs')
|
||||||
|
|
||||||
|
|
||||||
def generate(which):
|
def generate(which, action='gen'):
|
||||||
from telethon_generator.parsers import parse_errors, parse_tl, find_layer
|
from telethon_generator.parsers import\
|
||||||
|
parse_errors, parse_methods, parse_tl, find_layer
|
||||||
|
|
||||||
from telethon_generator.generators import\
|
from telethon_generator.generators import\
|
||||||
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
|
generate_errors, generate_tlobjects, generate_docs, clean_tlobjects
|
||||||
|
|
||||||
# Older Python versions open the file as bytes instead (3.4.2)
|
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
|
||||||
with open(INVALID_BM_IN, 'r') as f:
|
errors = list(parse_errors(ERRORS_IN))
|
||||||
invalid_bot_methods = set(json.load(f))
|
methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors}))
|
||||||
|
|
||||||
layer = find_layer(TLOBJECT_IN_TL)
|
tlobjects = list(itertools.chain(*(
|
||||||
errors = list(parse_errors(ERRORS_IN_JSON, ERRORS_IN_DESC))
|
parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS)))
|
||||||
tlobjects = list(itertools.chain(
|
|
||||||
parse_tl(TLOBJECT_IN_CORE_TL, layer, invalid_bot_methods),
|
|
||||||
parse_tl(TLOBJECT_IN_TL, layer, invalid_bot_methods)))
|
|
||||||
|
|
||||||
if not which:
|
if not which:
|
||||||
which.extend(('tl', 'errors'))
|
which.extend(('tl', 'errors'))
|
||||||
|
|
||||||
clean = 'clean' in which
|
clean = action == 'clean'
|
||||||
action = 'Cleaning' if clean else 'Generating'
|
action = 'Cleaning' if clean else 'Generating'
|
||||||
if clean:
|
|
||||||
which.remove('clean')
|
|
||||||
|
|
||||||
if 'all' in which:
|
if 'all' in which:
|
||||||
which.remove('all')
|
which.remove('all')
|
||||||
|
@ -96,84 +103,105 @@ def generate(which):
|
||||||
which.remove('errors')
|
which.remove('errors')
|
||||||
print(action, 'RPCErrors...')
|
print(action, 'RPCErrors...')
|
||||||
if clean:
|
if clean:
|
||||||
if os.path.isfile(ERRORS_OUT):
|
if ERRORS_OUT.is_file():
|
||||||
os.remove(ERRORS_OUT)
|
ERRORS_OUT.unlink()
|
||||||
else:
|
else:
|
||||||
with open(ERRORS_OUT, 'w', encoding='utf-8') as file:
|
with ERRORS_OUT.open('w') as file:
|
||||||
generate_errors(errors, file)
|
generate_errors(errors, file)
|
||||||
|
|
||||||
if 'docs' in which:
|
if 'docs' in which:
|
||||||
which.remove('docs')
|
which.remove('docs')
|
||||||
print(action, 'documentation...')
|
print(action, 'documentation...')
|
||||||
if clean:
|
if clean:
|
||||||
if os.path.isdir(DOCS_OUT):
|
if DOCS_OUT.is_dir():
|
||||||
shutil.rmtree(DOCS_OUT)
|
shutil.rmtree(str(DOCS_OUT))
|
||||||
else:
|
else:
|
||||||
generate_docs(tlobjects, errors, layer, DOCS_IN_RES, DOCS_OUT)
|
in_path = DOCS_IN_RES.resolve()
|
||||||
|
with TempWorkDir(DOCS_OUT):
|
||||||
|
generate_docs(tlobjects, methods, layer, in_path)
|
||||||
|
|
||||||
if 'json' in which:
|
if 'json' in which:
|
||||||
which.remove('json')
|
which.remove('json')
|
||||||
print(action, 'JSON schema...')
|
print(action, 'JSON schema...')
|
||||||
mtproto = 'mtproto_api.json'
|
json_files = [x.with_suffix('.json') for x in TLOBJECT_IN_TLS]
|
||||||
telegram = 'telegram_api.json'
|
|
||||||
if clean:
|
if clean:
|
||||||
for x in (mtproto, telegram):
|
for file in json_files:
|
||||||
if os.path.isfile(x):
|
if file.is_file():
|
||||||
os.remove(x)
|
file.unlink()
|
||||||
else:
|
else:
|
||||||
def gen_json(fin, fout):
|
def gen_json(fin, fout):
|
||||||
methods = []
|
meths = []
|
||||||
constructors = []
|
constructors = []
|
||||||
for tl in parse_tl(fin, layer):
|
for tl in parse_tl(fin, layer):
|
||||||
if tl.is_function:
|
if tl.is_function:
|
||||||
methods.append(tl.to_dict())
|
meths.append(tl.to_dict())
|
||||||
else:
|
else:
|
||||||
constructors.append(tl.to_dict())
|
constructors.append(tl.to_dict())
|
||||||
what = {'constructors': constructors, 'methods': methods}
|
what = {'constructors': constructors, 'methods': meths}
|
||||||
with open(fout, 'w') as f:
|
with open(fout, 'w') as f:
|
||||||
json.dump(what, f, indent=2)
|
json.dump(what, f, indent=2)
|
||||||
|
|
||||||
gen_json(TLOBJECT_IN_CORE_TL, mtproto)
|
for fs in zip(TLOBJECT_IN_TLS, json_files):
|
||||||
gen_json(TLOBJECT_IN_TL, telegram)
|
gen_json(*fs)
|
||||||
|
|
||||||
if which:
|
if which:
|
||||||
print('The following items were not understood:', which)
|
print(
|
||||||
print(' Consider using only "tl", "errors" and/or "docs".')
|
'The following items were not understood:', which,
|
||||||
print(' Using only "clean" will clean them. "all" to act on all.')
|
'\n Consider using only "tl", "errors" and/or "docs".'
|
||||||
print(' For instance "gen tl errors".')
|
'\n Using only "clean" will clean them. "all" to act on all.'
|
||||||
|
'\n For instance "gen tl errors".'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main(argv):
|
||||||
if len(argv) >= 2 and argv[1] == 'gen':
|
if len(argv) >= 2 and argv[1] in ('gen', 'clean'):
|
||||||
generate(argv[2:])
|
generate(argv[2:], argv[1])
|
||||||
|
|
||||||
elif len(argv) >= 2 and argv[1] == 'pypi':
|
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
|
# (Re)generate the code to make sure we don't push without it
|
||||||
generate(['tl', 'errors'])
|
generate(['tl', 'errors'])
|
||||||
|
|
||||||
# Try importing the telethon module to assert it has no errors
|
# Try importing the telethon module to assert it has no errors
|
||||||
try:
|
try:
|
||||||
import telethon
|
import telethon
|
||||||
except:
|
except Exception as e:
|
||||||
print('Packaging for PyPi aborted, importing the module failed.')
|
print('Packaging for PyPi aborted, importing the module failed.')
|
||||||
|
print(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Need python3.5 or higher, but Telethon is supposed to support 3.x
|
remove_dirs = ['__pycache__', 'build', 'dist', 'Telethon.egg-info']
|
||||||
# Place it here since noone should be running ./setup.py pypi anyway
|
for root, _dirs, _files in os.walk(LIBRARY_DIR, topdown=False):
|
||||||
from subprocess import run
|
# setuptools is including __pycache__ for some reason (#1605)
|
||||||
from shutil import rmtree
|
if root.endswith('/__pycache__'):
|
||||||
|
remove_dirs.append(root)
|
||||||
|
for x in remove_dirs:
|
||||||
|
shutil.rmtree(x, ignore_errors=True)
|
||||||
|
|
||||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
|
||||||
rmtree(x, ignore_errors=True)
|
|
||||||
run('python3 setup.py sdist', shell=True)
|
run('python3 setup.py sdist', shell=True)
|
||||||
run('python3 setup.py bdist_wheel', shell=True)
|
run('python3 setup.py bdist_wheel', shell=True)
|
||||||
run('twine upload dist/*', shell=True)
|
run('twine upload dist/*', shell=True)
|
||||||
for x in ('build', 'dist', 'Telethon.egg-info'):
|
for x in ('build', 'dist', 'Telethon.egg-info'):
|
||||||
rmtree(x, ignore_errors=True)
|
shutil.rmtree(x, ignore_errors=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# e.g. install from GitHub
|
# e.g. install from GitHub
|
||||||
if os.path.isdir(GENERATOR_DIR):
|
if GENERATOR_DIR.is_dir():
|
||||||
generate(['tl', 'errors'])
|
generate(['tl', 'errors'])
|
||||||
|
|
||||||
# Get the long description from the README file
|
# Get the long description from the README file
|
||||||
|
@ -216,14 +244,15 @@ def main():
|
||||||
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6'
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
],
|
],
|
||||||
keywords='telegram api chat client library messaging mtproto',
|
keywords='telegram api chat client library messaging mtproto',
|
||||||
packages=find_packages(exclude=[
|
packages=find_packages(exclude=[
|
||||||
'telethon_*', 'run_tests.py', 'try_telethon.py'
|
'telethon_*', 'tests*'
|
||||||
]),
|
]),
|
||||||
install_requires=['pyaes', 'rsa',
|
install_requires=['pyaes', 'rsa'],
|
||||||
'async_generator'],
|
|
||||||
extras_require={
|
extras_require={
|
||||||
'cryptg': ['cryptg']
|
'cryptg': ['cryptg']
|
||||||
}
|
}
|
||||||
|
@ -231,5 +260,5 @@ def main():
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with TempWorkDir(): # Could just use a try/finally but this is + reusable
|
with TempWorkDir():
|
||||||
main()
|
main(sys.argv)
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import logging
|
|
||||||
from .client.telegramclient import TelegramClient
|
from .client.telegramclient import TelegramClient
|
||||||
from .network import connection
|
from .network import connection
|
||||||
from .tl import types, functions, custom
|
from .tl.custom import Button
|
||||||
from . import version, events, utils, errors
|
from .tl import patched as _ # import for its side-effects
|
||||||
|
from . import version, events, utils, errors, types, functions, custom
|
||||||
|
|
||||||
__version__ = version.__version__
|
__version__ = version.__version__
|
||||||
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
__all__ = ['TelegramClient', 'types', 'functions', 'custom',
|
__all__ = [
|
||||||
'events', 'utils', 'errors']
|
'TelegramClient', 'Button',
|
||||||
|
'types', 'functions', 'custom', 'errors',
|
||||||
|
'events', 'utils', 'connection'
|
||||||
|
]
|
||||||
|
|
3
telethon/_updates/__init__.py
Normal file
3
telethon/_updates/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from .entitycache import EntityCache
|
||||||
|
from .messagebox import MessageBox, GapError, PrematureEndReason
|
||||||
|
from .session import SessionState, ChannelState, Entity, EntityType
|
62
telethon/_updates/entitycache.py
Normal file
62
telethon/_updates/entitycache.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
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 get_all_entities(self):
|
||||||
|
return [Entity(ty, id, hash) for id, (hash, ty) in self.hash_map.items()]
|
||||||
|
|
||||||
|
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)
|
833
telethon/_updates/messagebox.py
Normal file
833
telethon/_updates/messagebox.py
Normal file
|
@ -0,0 +1,833 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
any_pts_applied = [False] # using a list to pass "by reference"
|
||||||
|
|
||||||
|
result.extend(filter(None, (
|
||||||
|
self.apply_pts_info(u, reset_deadlines=reset_deadlines, any_pts_applied=any_pts_applied)
|
||||||
|
# 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))))
|
||||||
|
|
||||||
|
# > If the updates were applied, local *Updates* state must be updated
|
||||||
|
# > with `seq` (unless it's 0) and `date` from the constructor.
|
||||||
|
#
|
||||||
|
# By "were applied", we assume it means "some other pts was applied".
|
||||||
|
# Updates which can be applied in any order, such as `UpdateChat`,
|
||||||
|
# should not cause `seq` to be updated (or upcoming updates such as
|
||||||
|
# `UpdateChatParticipant` could be missed).
|
||||||
|
if any_pts_applied[0]:
|
||||||
|
if __debug__:
|
||||||
|
self._trace('Updating seq as local pts was updated too')
|
||||||
|
if date != epoch():
|
||||||
|
self.date = date
|
||||||
|
if seq != NO_SEQ:
|
||||||
|
self.seq = seq
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
any_pts_applied=[True], # mutable default is fine as it's write-only
|
||||||
|
):
|
||||||
|
# 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
|
||||||
|
any_pts_applied[0] = True
|
||||||
|
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.
|
195
telethon/_updates/session.py
Normal file
195
telethon/_updates/session.py
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
from enum import IntEnum
|
||||||
|
from ..tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
|
||||||
|
|
||||||
|
|
||||||
|
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 {string!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__})
|
|
@ -19,6 +19,7 @@ from .messages import MessageMethods
|
||||||
from .chats import ChatMethods
|
from .chats import ChatMethods
|
||||||
from .dialogs import DialogMethods
|
from .dialogs import DialogMethods
|
||||||
from .downloads import DownloadMethods
|
from .downloads import DownloadMethods
|
||||||
|
from .account import AccountMethods
|
||||||
from .auth import AuthMethods
|
from .auth import AuthMethods
|
||||||
from .bots import BotMethods
|
from .bots import BotMethods
|
||||||
from .telegramclient import TelegramClient
|
from .telegramclient import TelegramClient
|
||||||
|
|
243
telethon/client/account.py
Normal file
243
telethon/client/account.py
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from .users import _NOT_A_REQUEST
|
||||||
|
from .. import helpers, utils
|
||||||
|
from ..tl import functions, TLRequest
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from .telegramclient import TelegramClient
|
||||||
|
|
||||||
|
|
||||||
|
# TODO Make use of :tl:`InvokeWithMessagesRange` somehow
|
||||||
|
# For that, we need to use :tl:`GetSplitRanges` first.
|
||||||
|
class _TakeoutClient:
|
||||||
|
"""
|
||||||
|
Proxy object over the client.
|
||||||
|
"""
|
||||||
|
__PROXY_INTERFACE = ('__enter__', '__exit__', '__aenter__', '__aexit__')
|
||||||
|
|
||||||
|
def __init__(self, finalize, client, request):
|
||||||
|
# We use the name mangling for attributes to make them inaccessible
|
||||||
|
# from within the shadowed client object and to distinguish them from
|
||||||
|
# its own attributes where needed.
|
||||||
|
self.__finalize = finalize
|
||||||
|
self.__client = client
|
||||||
|
self.__request = request
|
||||||
|
self.__success = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self):
|
||||||
|
return self.__success
|
||||||
|
|
||||||
|
@success.setter
|
||||||
|
def success(self, value):
|
||||||
|
self.__success = value
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
# Enter/Exit behaviour is "overrode", we don't want to call start.
|
||||||
|
client = self.__client
|
||||||
|
if client.session.takeout_id is None:
|
||||||
|
client.session.takeout_id = (await client(self.__request)).id
|
||||||
|
elif self.__request is not None:
|
||||||
|
raise ValueError("Can't send a takeout request while another "
|
||||||
|
"takeout for the current session still not been finished yet.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||||
|
if self.__success is None and self.__finalize:
|
||||||
|
self.__success = exc_type is None
|
||||||
|
|
||||||
|
if self.__success is not None:
|
||||||
|
result = await self(functions.account.FinishTakeoutSessionRequest(
|
||||||
|
self.__success))
|
||||||
|
if not result:
|
||||||
|
raise ValueError("Failed to finish the takeout.")
|
||||||
|
self.session.takeout_id = None
|
||||||
|
|
||||||
|
__enter__ = helpers._sync_enter
|
||||||
|
__exit__ = helpers._sync_exit
|
||||||
|
|
||||||
|
async def __call__(self, request, ordered=False):
|
||||||
|
takeout_id = self.__client.session.takeout_id
|
||||||
|
if takeout_id is None:
|
||||||
|
raise ValueError('Takeout mode has not been initialized '
|
||||||
|
'(are you calling outside of "with"?)')
|
||||||
|
|
||||||
|
single = not utils.is_list_like(request)
|
||||||
|
requests = ((request,) if single else request)
|
||||||
|
wrapped = []
|
||||||
|
for r in requests:
|
||||||
|
if not isinstance(r, TLRequest):
|
||||||
|
raise _NOT_A_REQUEST()
|
||||||
|
await r.resolve(self, utils)
|
||||||
|
wrapped.append(functions.InvokeWithTakeoutRequest(takeout_id, r))
|
||||||
|
|
||||||
|
return await self.__client(
|
||||||
|
wrapped[0] if single else wrapped, ordered=ordered)
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
# We access class via type() because __class__ will recurse infinitely.
|
||||||
|
# Also note that since we've name-mangled our own class attributes,
|
||||||
|
# they'll be passed to __getattribute__() as already decorated. For
|
||||||
|
# example, 'self.__client' will be passed as '_TakeoutClient__client'.
|
||||||
|
# https://docs.python.org/3/tutorial/classes.html#private-variables
|
||||||
|
if name.startswith('__') and name not in type(self).__PROXY_INTERFACE:
|
||||||
|
raise AttributeError # force call of __getattr__
|
||||||
|
|
||||||
|
# Try to access attribute in the proxy object and check for the same
|
||||||
|
# attribute in the shadowed object (through our __getattr__) if failed.
|
||||||
|
return super().__getattribute__(name)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
value = getattr(self.__client, name)
|
||||||
|
if inspect.ismethod(value):
|
||||||
|
# Emulate bound methods behavior by partially applying our proxy
|
||||||
|
# class as the self parameter instead of the client.
|
||||||
|
return functools.partial(
|
||||||
|
getattr(self.__client.__class__, name), self)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __setattr__(self, name, value):
|
||||||
|
if name.startswith('_{}__'.format(type(self).__name__.lstrip('_'))):
|
||||||
|
# This is our own name-mangled attribute, keep calm.
|
||||||
|
return super().__setattr__(name, value)
|
||||||
|
return setattr(self.__client, name, value)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMethods:
|
||||||
|
def takeout(
|
||||||
|
self: 'TelegramClient',
|
||||||
|
finalize: bool = True,
|
||||||
|
*,
|
||||||
|
contacts: bool = None,
|
||||||
|
users: bool = None,
|
||||||
|
chats: bool = None,
|
||||||
|
megagroups: bool = None,
|
||||||
|
channels: bool = None,
|
||||||
|
files: bool = None,
|
||||||
|
max_file_size: bool = None) -> 'TelegramClient':
|
||||||
|
"""
|
||||||
|
Returns a :ref:`telethon-client` which calls methods behind a takeout session.
|
||||||
|
|
||||||
|
It does so by creating a proxy object over the current client through
|
||||||
|
which making requests will use :tl:`InvokeWithTakeoutRequest` to wrap
|
||||||
|
them. In other words, returns the current client modified so that
|
||||||
|
requests are done as a takeout:
|
||||||
|
|
||||||
|
Some of the calls made through the takeout session will have lower
|
||||||
|
flood limits. This is useful if you want to export the data from
|
||||||
|
conversations or mass-download media, since the rate limits will
|
||||||
|
be lower. Only some requests will be affected, and you will need
|
||||||
|
to adjust the `wait_time` of methods like `client.iter_messages
|
||||||
|
<telethon.client.messages.MessageMethods.iter_messages>`.
|
||||||
|
|
||||||
|
By default, all parameters are `None`, and you need to enable those
|
||||||
|
you plan to use by setting them to either `True` or `False`.
|
||||||
|
|
||||||
|
You should ``except errors.TakeoutInitDelayError as e``, since this
|
||||||
|
exception will raise depending on the condition of the session. You
|
||||||
|
can then access ``e.seconds`` to know how long you should wait for
|
||||||
|
before calling the method again.
|
||||||
|
|
||||||
|
There's also a `success` property available in the takeout proxy
|
||||||
|
object, so from the `with` body you can set the boolean result that
|
||||||
|
will be sent back to Telegram. But if it's left `None` as by
|
||||||
|
default, then the action is based on the `finalize` parameter. If
|
||||||
|
it's `True` then the takeout will be finished, and if no exception
|
||||||
|
occurred during it, then `True` will be considered as a result.
|
||||||
|
Otherwise, the takeout will not be finished and its ID will be
|
||||||
|
preserved for future usage as `client.session.takeout_id
|
||||||
|
<telethon.sessions.abstract.Session.takeout_id>`.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
finalize (`bool`):
|
||||||
|
Whether the takeout session should be finalized upon
|
||||||
|
exit or not.
|
||||||
|
|
||||||
|
contacts (`bool`):
|
||||||
|
Set to `True` if you plan on downloading contacts.
|
||||||
|
|
||||||
|
users (`bool`):
|
||||||
|
Set to `True` if you plan on downloading information
|
||||||
|
from users and their private conversations with you.
|
||||||
|
|
||||||
|
chats (`bool`):
|
||||||
|
Set to `True` if you plan on downloading information
|
||||||
|
from small group chats, such as messages and media.
|
||||||
|
|
||||||
|
megagroups (`bool`):
|
||||||
|
Set to `True` if you plan on downloading information
|
||||||
|
from megagroups (channels), such as messages and media.
|
||||||
|
|
||||||
|
channels (`bool`):
|
||||||
|
Set to `True` if you plan on downloading information
|
||||||
|
from broadcast channels, such as messages and media.
|
||||||
|
|
||||||
|
files (`bool`):
|
||||||
|
Set to `True` if you plan on downloading media and
|
||||||
|
you don't only wish to export messages.
|
||||||
|
|
||||||
|
max_file_size (`int`):
|
||||||
|
The maximum file size, in bytes, that you plan
|
||||||
|
to download for each message with media.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import errors
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with client.takeout() as takeout:
|
||||||
|
await client.get_messages('me') # normal call
|
||||||
|
await takeout.get_messages('me') # wrapped through takeout (less limits)
|
||||||
|
|
||||||
|
async for message in takeout.iter_messages(chat, wait_time=0):
|
||||||
|
... # Do something with the message
|
||||||
|
|
||||||
|
except errors.TakeoutInitDelayError as e:
|
||||||
|
print('Must wait', e.seconds, 'before takeout')
|
||||||
|
"""
|
||||||
|
request_kwargs = dict(
|
||||||
|
contacts=contacts,
|
||||||
|
message_users=users,
|
||||||
|
message_chats=chats,
|
||||||
|
message_megagroups=megagroups,
|
||||||
|
message_channels=channels,
|
||||||
|
files=files,
|
||||||
|
file_max_size=max_file_size
|
||||||
|
)
|
||||||
|
arg_specified = (arg is not None for arg in request_kwargs.values())
|
||||||
|
|
||||||
|
if self.session.takeout_id is None or any(arg_specified):
|
||||||
|
request = functions.account.InitTakeoutSessionRequest(
|
||||||
|
**request_kwargs)
|
||||||
|
else:
|
||||||
|
request = None
|
||||||
|
|
||||||
|
return _TakeoutClient(finalize, self, request)
|
||||||
|
|
||||||
|
async def end_takeout(self: 'TelegramClient', success: bool) -> bool:
|
||||||
|
"""
|
||||||
|
Finishes the current takeout session.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
success (`bool`):
|
||||||
|
Whether the takeout completed successfully or not.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
`True` if the operation was successful, `False` otherwise.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
await client.end_takeout(success=False)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with _TakeoutClient(True, self, None) as takeout:
|
||||||
|
takeout.success = success
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
|
@ -1,48 +1,44 @@
|
||||||
import getpass
|
import getpass
|
||||||
import hashlib
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
|
import warnings
|
||||||
|
|
||||||
from .messageparse import MessageParseMethods
|
from .. import utils, helpers, errors, password as pwd_mod
|
||||||
from .users import UserMethods
|
from ..tl import types, functions, custom
|
||||||
from .. import utils, helpers, errors
|
from .._updates import SessionState
|
||||||
from ..tl import types, functions
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from .telegramclient import TelegramClient
|
||||||
|
|
||||||
|
|
||||||
class AuthMethods(MessageParseMethods, UserMethods):
|
class AuthMethods:
|
||||||
|
|
||||||
# region Public methods
|
# region Public methods
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self: 'TelegramClient',
|
||||||
phone=lambda: input('Please enter your phone (or bot token): '),
|
phone: typing.Union[typing.Callable[[], str], str] = lambda: input('Please enter your phone (or bot token): '),
|
||||||
password=lambda: getpass.getpass('Please enter your password: '),
|
password: typing.Union[typing.Callable[[], str], str] = lambda: getpass.getpass('Please enter your password: '),
|
||||||
*,
|
*,
|
||||||
bot_token=None, force_sms=False, code_callback=None,
|
bot_token: str = None,
|
||||||
first_name='New User', last_name='', max_attempts=3):
|
force_sms: bool = False,
|
||||||
|
code_callback: typing.Callable[[], typing.Union[str, int]] = None,
|
||||||
|
first_name: str = 'New User',
|
||||||
|
last_name: str = '',
|
||||||
|
max_attempts: int = 3) -> 'TelegramClient':
|
||||||
"""
|
"""
|
||||||
Convenience method to interactively connect and sign in if required,
|
Starts the client (connects and logs in if necessary).
|
||||||
also taking into consideration that 2FA may be enabled in the account.
|
|
||||||
|
|
||||||
If the phone doesn't belong to an existing account (and will hence
|
By default, this method will be interactive (asking for
|
||||||
`sign_up` for a new one), **you are agreeing to Telegram's
|
user input if needed), and will handle 2FA if enabled too.
|
||||||
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.
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
>>> client = ...
|
|
||||||
>>> client.start(phone)
|
|
||||||
Please enter the code you received: 12345
|
|
||||||
Please enter your password: *******
|
|
||||||
(You are now logged in)
|
|
||||||
|
|
||||||
If the event loop is already running, this method returns a
|
If the event loop is already running, this method returns a
|
||||||
coroutine that you should await on your own code; otherwise
|
coroutine that you should await on your own code; otherwise
|
||||||
the loop is ran until said coroutine completes.
|
the loop is ran until said coroutine completes.
|
||||||
|
|
||||||
Args:
|
Arguments
|
||||||
phone (`str` | `int` | `callable`):
|
phone (`str` | `int` | `callable`):
|
||||||
The phone (or callable without arguments to get it)
|
The phone (or callable without arguments to get it)
|
||||||
to which the code will be sent. If a bot-token-like
|
to which the code will be sent. If a bot-token-like
|
||||||
|
@ -79,9 +75,27 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
How many times the code/password callback should be
|
How many times the code/password callback should be
|
||||||
retried or switching between signing in and signing up.
|
retried or switching between signing in and signing up.
|
||||||
|
|
||||||
Returns:
|
Returns
|
||||||
This `TelegramClient`, so initialization
|
This `TelegramClient`, so initialization
|
||||||
can be chained with ``.start()``.
|
can be chained with ``.start()``.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
client = TelegramClient('anon', api_id, api_hash)
|
||||||
|
|
||||||
|
# Starting as a bot account
|
||||||
|
await client.start(bot_token=bot_token)
|
||||||
|
|
||||||
|
# Starting as a user account
|
||||||
|
await client.start(phone)
|
||||||
|
# Please enter the code you received: 12345
|
||||||
|
# Please enter your password: *******
|
||||||
|
# (You are now logged in)
|
||||||
|
|
||||||
|
# Starting using a context manager (this calls start()):
|
||||||
|
with client:
|
||||||
|
pass
|
||||||
"""
|
"""
|
||||||
if code_callback is None:
|
if code_callback is None:
|
||||||
def code_callback():
|
def code_callback():
|
||||||
|
@ -115,12 +129,36 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _start(
|
async def _start(
|
||||||
self, phone, password, bot_token, force_sms,
|
self: 'TelegramClient', phone, password, bot_token, force_sms,
|
||||||
code_callback, first_name, last_name, max_attempts):
|
code_callback, first_name, last_name, max_attempts):
|
||||||
if not self.is_connected():
|
if not self.is_connected():
|
||||||
await self.connect()
|
await self.connect()
|
||||||
|
|
||||||
if await self.is_user_authorized():
|
# Rather than using `is_user_authorized`, use `get_me`. While this is
|
||||||
|
# more expensive and needs to retrieve more data from the server, it
|
||||||
|
# enables the library to warn users trying to login to a different
|
||||||
|
# account. See #1172.
|
||||||
|
me = await self.get_me()
|
||||||
|
if me is not None:
|
||||||
|
# The warnings here are on a best-effort and may fail.
|
||||||
|
if bot_token:
|
||||||
|
# bot_token's first part has the bot ID, but it may be invalid
|
||||||
|
# so don't try to parse as int (instead cast our ID to string).
|
||||||
|
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; '
|
||||||
|
'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; '
|
||||||
|
'if you were expecting a different user, check whether '
|
||||||
|
'you are accidentally reusing an existing session'
|
||||||
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if not bot_token:
|
if not bot_token:
|
||||||
|
@ -145,27 +183,25 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
attempts = 0
|
attempts = 0
|
||||||
two_step_detected = False
|
two_step_detected = False
|
||||||
|
|
||||||
sent_code = await self.send_code_request(phone, force_sms=force_sms)
|
await self.send_code_request(phone, force_sms=force_sms)
|
||||||
sign_up = not sent_code.phone_registered
|
|
||||||
while attempts < max_attempts:
|
while attempts < max_attempts:
|
||||||
try:
|
try:
|
||||||
value = code_callback()
|
value = code_callback()
|
||||||
if inspect.isawaitable(value):
|
if inspect.isawaitable(value):
|
||||||
value = await value
|
value = await value
|
||||||
|
|
||||||
if sign_up:
|
# Since sign-in with no code works (it sends the code)
|
||||||
me = await self.sign_up(value, first_name, last_name)
|
# we must double-check that here. Else we'll assume we
|
||||||
else:
|
# logged in, and it will return None as the User.
|
||||||
# Raises SessionPasswordNeededError if 2FA enabled
|
if not value:
|
||||||
me = await self.sign_in(phone, code=value)
|
raise errors.PhoneCodeEmptyError(request=None)
|
||||||
|
|
||||||
|
# Raises SessionPasswordNeededError if 2FA enabled
|
||||||
|
me = await self.sign_in(phone, code=value)
|
||||||
break
|
break
|
||||||
except errors.SessionPasswordNeededError:
|
except errors.SessionPasswordNeededError:
|
||||||
two_step_detected = True
|
two_step_detected = True
|
||||||
break
|
break
|
||||||
except errors.PhoneNumberOccupiedError:
|
|
||||||
sign_up = False
|
|
||||||
except errors.PhoneNumberUnoccupiedError:
|
|
||||||
sign_up = True
|
|
||||||
except (errors.PhoneCodeEmptyError,
|
except (errors.PhoneCodeEmptyError,
|
||||||
errors.PhoneCodeExpiredError,
|
errors.PhoneCodeExpiredError,
|
||||||
errors.PhoneCodeHashEmptyError,
|
errors.PhoneCodeHashEmptyError,
|
||||||
|
@ -199,29 +235,58 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
print('Invalid password. Please try again',
|
print('Invalid password. Please try again',
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
raise errors.PasswordHashInvalidError()
|
raise errors.PasswordHashInvalidError(request=None)
|
||||||
else:
|
else:
|
||||||
me = await self.sign_in(phone=phone, password=password)
|
me = await self.sign_in(phone=phone, password=password)
|
||||||
|
|
||||||
# We won't reach here if any step failed (exit by exception)
|
# 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:
|
try:
|
||||||
print(signed, name)
|
print(signed, name, tos, sep='')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
# Some terminals don't support certain characters
|
# Some terminals don't support certain characters
|
||||||
print(signed, name.encode('utf-8', errors='ignore')
|
print(signed, name.encode('utf-8', errors='ignore')
|
||||||
.decode('ascii', errors='ignore'))
|
.decode('ascii', errors='ignore'), tos, sep='')
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def sign_in(
|
def _parse_phone_and_hash(self, phone, phone_hash):
|
||||||
self, phone=None, code=None, *, password=None,
|
|
||||||
bot_token=None, phone_code_hash=None):
|
|
||||||
"""
|
"""
|
||||||
Starts or completes the sign in process with the given phone number
|
Helper method to both parse and validate phone and its hash.
|
||||||
or code that Telegram sent.
|
"""
|
||||||
|
phone = utils.parse_phone(phone) or self._phone
|
||||||
|
if not phone:
|
||||||
|
raise ValueError(
|
||||||
|
'Please make sure to call send_code_request first.'
|
||||||
|
)
|
||||||
|
|
||||||
Args:
|
phone_hash = phone_hash or self._phone_code_hash.get(phone, None)
|
||||||
|
if not phone_hash:
|
||||||
|
raise ValueError('You also need to provide a phone_code_hash.')
|
||||||
|
|
||||||
|
return phone, phone_hash
|
||||||
|
|
||||||
|
async def sign_in(
|
||||||
|
self: 'TelegramClient',
|
||||||
|
phone: str = None,
|
||||||
|
code: typing.Union[str, int] = None,
|
||||||
|
*,
|
||||||
|
password: str = None,
|
||||||
|
bot_token: str = None,
|
||||||
|
phone_code_hash: str = None) -> 'typing.Union[types.User, types.auth.SentCode]':
|
||||||
|
"""
|
||||||
|
Logs in to Telegram to an existing user or bot account.
|
||||||
|
|
||||||
|
You should only use this if you are not authorized yet.
|
||||||
|
|
||||||
|
This method will send the code if it's not provided.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In most cases, you should simply use `start()` and not this method.
|
||||||
|
|
||||||
|
Arguments
|
||||||
phone (`str` | `int`):
|
phone (`str` | `int`):
|
||||||
The phone to send the code to if no code was provided,
|
The phone to send the code to if no code was provided,
|
||||||
or to override the phone that was previously used with
|
or to override the phone that was previously used with
|
||||||
|
@ -235,19 +300,29 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
|
|
||||||
password (`str`):
|
password (`str`):
|
||||||
2FA password, should be used if a previous call raised
|
2FA password, should be used if a previous call raised
|
||||||
SessionPasswordNeededError.
|
``SessionPasswordNeededError``.
|
||||||
|
|
||||||
bot_token (`str`):
|
bot_token (`str`):
|
||||||
Used to sign in as a bot. Not all requests will be available.
|
Used to sign in as a bot. Not all requests will be available.
|
||||||
This should be the hash the @BotFather gave you.
|
This should be the hash the `@BotFather <https://t.me/BotFather>`_
|
||||||
|
gave you.
|
||||||
|
|
||||||
phone_code_hash (`str`):
|
phone_code_hash (`str`, optional):
|
||||||
The hash returned by .send_code_request. This can be set to None
|
The hash returned by `send_code_request`. This can be left as
|
||||||
to use the last hash known.
|
`None` to use the last hash known for the phone to be used.
|
||||||
|
|
||||||
Returns:
|
Returns
|
||||||
The signed in user, or the information about
|
The signed in user, or the information about
|
||||||
:meth:`send_code_request`.
|
:meth:`send_code_request`.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
phone = '+34 123 123 123'
|
||||||
|
await client.sign_in(phone) # send code
|
||||||
|
|
||||||
|
code = input('enter code: ')
|
||||||
|
await client.sign_in(phone, code)
|
||||||
"""
|
"""
|
||||||
me = await self.get_me()
|
me = await self.get_me()
|
||||||
if me:
|
if me:
|
||||||
|
@ -256,248 +331,347 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
if phone and not code and not password:
|
if phone and not code and not password:
|
||||||
return await self.send_code_request(phone)
|
return await self.send_code_request(phone)
|
||||||
elif code:
|
elif code:
|
||||||
phone = utils.parse_phone(phone) or self._phone
|
phone, phone_code_hash = \
|
||||||
phone_code_hash = \
|
self._parse_phone_and_hash(phone, phone_code_hash)
|
||||||
phone_code_hash or self._phone_code_hash.get(phone, None)
|
|
||||||
|
|
||||||
if not phone:
|
|
||||||
raise ValueError(
|
|
||||||
'Please make sure to call send_code_request first.'
|
|
||||||
)
|
|
||||||
if not phone_code_hash:
|
|
||||||
raise ValueError('You also need to provide a phone_code_hash.')
|
|
||||||
|
|
||||||
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
# May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
|
||||||
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
# PhoneCodeHashEmptyError or PhoneCodeInvalidError.
|
||||||
result = await self(functions.auth.SignInRequest(
|
request = functions.auth.SignInRequest(
|
||||||
phone, phone_code_hash, str(code)))
|
phone, phone_code_hash, str(code)
|
||||||
|
)
|
||||||
elif password:
|
elif password:
|
||||||
salt = (await self(
|
pwd = await self(functions.account.GetPasswordRequest())
|
||||||
functions.account.GetPasswordRequest())).current_salt
|
request = functions.auth.CheckPasswordRequest(
|
||||||
result = await self(functions.auth.CheckPasswordRequest(
|
pwd_mod.compute_check(pwd, password)
|
||||||
helpers.get_password_hash(password, salt)
|
)
|
||||||
))
|
|
||||||
elif bot_token:
|
elif bot_token:
|
||||||
result = await self(functions.auth.ImportBotAuthorizationRequest(
|
request = functions.auth.ImportBotAuthorizationRequest(
|
||||||
flags=0, bot_auth_token=bot_token,
|
flags=0, bot_auth_token=bot_token,
|
||||||
api_id=self.api_id, api_hash=self.api_hash
|
api_id=self.api_id, api_hash=self.api_hash
|
||||||
))
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'You must provide a phone and a code the first time, '
|
'You must provide a phone and a code the first time, '
|
||||||
'and a password only if an RPCError was raised before.'
|
'and a password only if an RPCError was raised before.'
|
||||||
)
|
)
|
||||||
|
|
||||||
self._self_input_peer = utils.get_input_peer(
|
try:
|
||||||
result.user, allow_self=False
|
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 await self._on_login(result.user)
|
||||||
|
|
||||||
|
async def sign_up(
|
||||||
|
self: 'TelegramClient',
|
||||||
|
code: typing.Union[str, int],
|
||||||
|
first_name: str,
|
||||||
|
last_name: str = '',
|
||||||
|
*,
|
||||||
|
phone: str = None,
|
||||||
|
phone_code_hash: str = None) -> 'types.User':
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')
|
||||||
|
|
||||||
|
async def _on_login(self, user):
|
||||||
|
"""
|
||||||
|
Callback called whenever the login or sign up process completes.
|
||||||
|
|
||||||
|
Returns the input user parameter.
|
||||||
|
"""
|
||||||
|
self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
|
||||||
self._authorized = True
|
self._authorized = True
|
||||||
return result.user
|
|
||||||
|
|
||||||
async def sign_up(self, code, first_name, last_name=''):
|
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,
|
||||||
|
_retry_count: int = 0) -> 'types.auth.SentCode':
|
||||||
"""
|
"""
|
||||||
Signs up to Telegram if you don't have an account yet.
|
Sends the Telegram code needed to login to the given phone number.
|
||||||
You must call .send_code_request(phone) first.
|
|
||||||
|
|
||||||
**By using this method you're agreeing to Telegram's
|
Arguments
|
||||||
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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
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.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The new created :tl:`User`.
|
|
||||||
"""
|
|
||||||
me = await self.get_me()
|
|
||||||
if me:
|
|
||||||
return me
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
result = await self(functions.auth.SignUpRequest(
|
|
||||||
phone_number=self._phone,
|
|
||||||
phone_code_hash=self._phone_code_hash.get(self._phone, ''),
|
|
||||||
phone_code=str(code),
|
|
||||||
first_name=first_name,
|
|
||||||
last_name=last_name
|
|
||||||
))
|
|
||||||
|
|
||||||
if self._tos:
|
|
||||||
await self(
|
|
||||||
functions.help.AcceptTermsOfServiceRequest(self._tos.id))
|
|
||||||
|
|
||||||
self._self_input_peer = utils.get_input_peer(
|
|
||||||
result.user, allow_self=False
|
|
||||||
)
|
|
||||||
self._authorized = True
|
|
||||||
return result.user
|
|
||||||
|
|
||||||
async def send_code_request(self, phone, *, force_sms=False):
|
|
||||||
"""
|
|
||||||
Sends a code request to the specified phone number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
phone (`str` | `int`):
|
phone (`str` | `int`):
|
||||||
The phone to which the code will be sent.
|
The phone to which the code will be sent.
|
||||||
|
|
||||||
force_sms (`bool`, optional):
|
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:
|
Returns
|
||||||
An instance of :tl:`SentCode`.
|
An instance of :tl:`SentCode`.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
phone = '+34 123 123 123'
|
||||||
|
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 = utils.parse_phone(phone) or self._phone
|
||||||
phone_hash = self._phone_code_hash.get(phone)
|
phone_hash = self._phone_code_hash.get(phone)
|
||||||
|
|
||||||
if not phone_hash:
|
if not phone_hash:
|
||||||
try:
|
try:
|
||||||
result = await self(functions.auth.SendCodeRequest(
|
result = await self(functions.auth.SendCodeRequest(
|
||||||
phone, self.api_id, self.api_hash))
|
phone, self.api_id, self.api_hash, types.CodeSettings()))
|
||||||
except errors.AuthRestartError:
|
except errors.AuthRestartError:
|
||||||
return 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)
|
||||||
|
|
||||||
self._tos = result.terms_of_service
|
# TODO figure out when/if/how this can happen
|
||||||
self._phone_code_hash[phone] = phone_hash = result.phone_code_hash
|
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):
|
||||||
|
force_sms = False
|
||||||
|
|
||||||
|
# phone_code_hash may be empty, if it is, do not save it (#1283)
|
||||||
|
if result.phone_code_hash:
|
||||||
|
self._phone_code_hash[phone] = phone_hash = result.phone_code_hash
|
||||||
else:
|
else:
|
||||||
force_sms = True
|
force_sms = True
|
||||||
|
|
||||||
self._phone = phone
|
self._phone = phone
|
||||||
|
|
||||||
if force_sms:
|
if force_sms:
|
||||||
result = await self(
|
try:
|
||||||
functions.auth.ResendCodeRequest(phone, phone_hash))
|
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
|
self._phone_code_hash[phone] = result.phone_code_hash
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def log_out(self):
|
async def qr_login(self: 'TelegramClient', ignored_ids: typing.List[int] = None) -> custom.QRLogin:
|
||||||
|
"""
|
||||||
|
Initiates the QR login procedure.
|
||||||
|
|
||||||
|
Note that you must be connected before invoking this, as with any
|
||||||
|
other request.
|
||||||
|
|
||||||
|
It is up to the caller to decide how to present the code to the user,
|
||||||
|
whether it's the URL, using the token bytes directly, or generating
|
||||||
|
a QR code and displaying it by other means.
|
||||||
|
|
||||||
|
See the documentation for `QRLogin` to see how to proceed after this.
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
ignored_ids (List[`int`]):
|
||||||
|
List of already logged-in user IDs, to prevent logging in
|
||||||
|
twice with the same user.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
An instance of `QRLogin`.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
def display_url_as_qr(url):
|
||||||
|
pass # do whatever to show url as a qr to the user
|
||||||
|
|
||||||
|
qr_login = await client.qr_login()
|
||||||
|
display_url_as_qr(qr_login.url)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
return qr_login
|
||||||
|
|
||||||
|
async def log_out(self: 'TelegramClient') -> bool:
|
||||||
"""
|
"""
|
||||||
Logs out Telegram and deletes the current ``*.session`` file.
|
Logs out Telegram and deletes the current ``*.session`` file.
|
||||||
|
|
||||||
Returns:
|
The client is unusable after logging out and a new instance should be created.
|
||||||
``True`` if the operation was successful.
|
|
||||||
|
Returns
|
||||||
|
`True` if the operation was successful.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Note: you will need to login again!
|
||||||
|
await client.log_out()
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
await self(functions.auth.LogOutRequest())
|
await self(functions.auth.LogOutRequest())
|
||||||
except errors.RPCError:
|
except errors.RPCError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self._mb_entity_cache.set_self_user(None, None, None)
|
||||||
|
self._authorized = False
|
||||||
|
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
self.session.delete()
|
self.session.delete()
|
||||||
self._authorized = False
|
self.session = None
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def edit_2fa(
|
async def edit_2fa(
|
||||||
self, current_password=None, new_password=None,
|
self: 'TelegramClient',
|
||||||
*, hint='', email=None):
|
current_password: str = None,
|
||||||
|
new_password: str = None,
|
||||||
|
*,
|
||||||
|
hint: str = '',
|
||||||
|
email: str = None,
|
||||||
|
email_code_callback: typing.Callable[[int], str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Changes the 2FA settings of the logged in user, according to the
|
Changes the 2FA settings of the logged in user.
|
||||||
passed parameters. Take note of the parameter explanations.
|
|
||||||
|
Review carefully the parameter explanations before using this method.
|
||||||
|
|
||||||
|
Note that this method may be *incredibly* slow depending on the
|
||||||
|
prime numbers that must be used during the process to make sure
|
||||||
|
that everything is safe.
|
||||||
|
|
||||||
Has no effect if both current and new password are omitted.
|
Has no effect if both current and new password are omitted.
|
||||||
|
|
||||||
current_password (`str`, optional):
|
Arguments
|
||||||
The current password, to authorize changing to ``new_password``.
|
current_password (`str`, optional):
|
||||||
Must be set if changing existing 2FA settings.
|
The current password, to authorize changing to ``new_password``.
|
||||||
Must **not** be set if 2FA is currently disabled.
|
Must be set if changing existing 2FA settings.
|
||||||
Passing this by itself will remove 2FA (if correct).
|
Must **not** be set if 2FA is currently disabled.
|
||||||
|
Passing this by itself will remove 2FA (if correct).
|
||||||
|
|
||||||
new_password (`str`, optional):
|
new_password (`str`, optional):
|
||||||
The password to set as 2FA.
|
The password to set as 2FA.
|
||||||
If 2FA was already enabled, ``current_password`` **must** be set.
|
If 2FA was already enabled, ``current_password`` **must** be set.
|
||||||
Leaving this blank or ``None`` will remove the password.
|
Leaving this blank or `None` will remove the password.
|
||||||
|
|
||||||
hint (`str`, optional):
|
hint (`str`, optional):
|
||||||
Hint to be displayed by Telegram when it asks for 2FA.
|
Hint to be displayed by Telegram when it asks for 2FA.
|
||||||
Leaving unspecified is highly discouraged.
|
Leaving unspecified is highly discouraged.
|
||||||
Has no effect if ``new_password`` is not set.
|
Has no effect if ``new_password`` is not set.
|
||||||
|
|
||||||
email (`str`, optional):
|
email (`str`, optional):
|
||||||
Recovery and verification email. Raises ``EmailUnconfirmedError``
|
Recovery and verification email. If present, you must also
|
||||||
if value differs from current one, and has no effect if
|
set `email_code_callback`, else it raises ``ValueError``.
|
||||||
``new_password`` is not set.
|
|
||||||
|
|
||||||
Returns:
|
email_code_callback (`callable`, optional):
|
||||||
``True`` if successful, ``False`` otherwise.
|
If an email is provided, a callback that returns the code sent
|
||||||
|
to it must also be set. This callback may be asynchronous.
|
||||||
|
It should return a string with the code. The length of the
|
||||||
|
code will be passed to the callback as an input parameter.
|
||||||
|
|
||||||
|
If the callback returns an invalid code, it will raise
|
||||||
|
``CodeInvalidError``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
`True` if successful, `False` otherwise.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Setting a password for your account which didn't have
|
||||||
|
await client.edit_2fa(new_password='I_<3_Telethon')
|
||||||
|
|
||||||
|
# Removing the password
|
||||||
|
await client.edit_2fa(current_password='I_<3_Telethon')
|
||||||
"""
|
"""
|
||||||
if new_password is None and current_password is None:
|
if new_password is None and current_password is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pass_result = await self(functions.account.GetPasswordRequest())
|
if email and not callable(email_code_callback):
|
||||||
if isinstance(
|
raise ValueError('email present without email_code_callback')
|
||||||
pass_result, types.account.NoPassword) and current_password:
|
|
||||||
|
pwd = await self(functions.account.GetPasswordRequest())
|
||||||
|
pwd.new_algo.salt1 += os.urandom(32)
|
||||||
|
assert isinstance(pwd, types.account.Password)
|
||||||
|
if not pwd.has_password and current_password:
|
||||||
current_password = None
|
current_password = None
|
||||||
|
|
||||||
salt_random = os.urandom(8)
|
if current_password:
|
||||||
salt = pass_result.new_salt + salt_random
|
password = pwd_mod.compute_check(pwd, current_password)
|
||||||
if not current_password:
|
|
||||||
current_password_hash = salt
|
|
||||||
else:
|
else:
|
||||||
current_password = (
|
password = types.InputCheckPasswordEmpty()
|
||||||
pass_result.current_salt
|
|
||||||
+ current_password.encode()
|
|
||||||
+ pass_result.current_salt
|
|
||||||
)
|
|
||||||
current_password_hash = hashlib.sha256(current_password).digest()
|
|
||||||
|
|
||||||
if new_password: # Setting new password
|
if new_password:
|
||||||
new_password = salt + new_password.encode('utf-8') + salt
|
new_password_hash = pwd_mod.compute_digest(
|
||||||
new_password_hash = hashlib.sha256(new_password).digest()
|
pwd.new_algo, new_password)
|
||||||
new_settings = types.account.PasswordInputSettings(
|
else:
|
||||||
new_salt=salt,
|
new_password_hash = b''
|
||||||
new_password_hash=new_password_hash,
|
|
||||||
hint=hint
|
try:
|
||||||
)
|
await self(functions.account.UpdatePasswordSettingsRequest(
|
||||||
if email: # If enabling 2FA or changing email
|
password=password,
|
||||||
new_settings.email = email # TG counts empty string as None
|
|
||||||
return await self(functions.account.UpdatePasswordSettingsRequest(
|
|
||||||
current_password_hash, new_settings=new_settings
|
|
||||||
))
|
|
||||||
else: # Removing existing password
|
|
||||||
return await self(functions.account.UpdatePasswordSettingsRequest(
|
|
||||||
current_password_hash,
|
|
||||||
new_settings=types.account.PasswordInputSettings(
|
new_settings=types.account.PasswordInputSettings(
|
||||||
new_salt=bytes(),
|
new_algo=pwd.new_algo,
|
||||||
new_password_hash=bytes(),
|
new_password_hash=new_password_hash,
|
||||||
hint=hint
|
hint=hint,
|
||||||
|
email=email,
|
||||||
|
new_secure_settings=None
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
except errors.EmailUnconfirmedError as e:
|
||||||
|
code = email_code_callback(e.code_length)
|
||||||
|
if inspect.isawaitable(code):
|
||||||
|
code = await code
|
||||||
|
|
||||||
|
code = str(code)
|
||||||
|
await self(functions.account.ConfirmPasswordEmailRequest(code))
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region with blocks
|
# region with blocks
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self.start()
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
def __exit__(self, *args):
|
|
||||||
if self._loop.is_running():
|
|
||||||
self._loop.create_task(self.disconnect())
|
|
||||||
elif inspect.iscoroutinefunction(self.disconnect):
|
|
||||||
self._loop.run_until_complete(self.disconnect())
|
|
||||||
else:
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
async def __aexit__(self, *args):
|
async def __aexit__(self, *args):
|
||||||
await self.disconnect()
|
await self.disconnect()
|
||||||
|
|
||||||
|
__enter__ = helpers._sync_enter
|
||||||
|
__exit__ = helpers._sync_exit
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
|
@ -1,23 +1,40 @@
|
||||||
from .users import UserMethods
|
import typing
|
||||||
|
|
||||||
|
from .. import hints
|
||||||
from ..tl import types, functions, custom
|
from ..tl import types, functions, custom
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
from .telegramclient import TelegramClient
|
||||||
|
|
||||||
class BotMethods(UserMethods):
|
|
||||||
async def inline_query(self, bot, query, *, offset=None, geo_point=None):
|
class BotMethods:
|
||||||
|
async def inline_query(
|
||||||
|
self: 'TelegramClient',
|
||||||
|
bot: 'hints.EntityLike',
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
entity: 'hints.EntityLike' = None,
|
||||||
|
offset: str = None,
|
||||||
|
geo_point: 'types.GeoPoint' = None) -> custom.InlineResults:
|
||||||
"""
|
"""
|
||||||
Makes the given inline query to the specified bot
|
Makes an inline query to the specified bot (``@vote New Poll``).
|
||||||
i.e. ``@vote My New Poll`` would be as follows:
|
|
||||||
|
|
||||||
>>> client = ...
|
Arguments
|
||||||
>>> client.inline_query('vote', 'My New Poll')
|
|
||||||
|
|
||||||
Args:
|
|
||||||
bot (`entity`):
|
bot (`entity`):
|
||||||
The bot entity to which the inline query should be made.
|
The bot entity to which the inline query should be made.
|
||||||
|
|
||||||
query (`str`):
|
query (`str`):
|
||||||
The query that should be made to the bot.
|
The query that should be made to the bot.
|
||||||
|
|
||||||
|
entity (`entity`, optional):
|
||||||
|
The entity where the inline query is being made from. Certain
|
||||||
|
bots use this to display different results depending on where
|
||||||
|
it's used, such as private chats, groups or channels.
|
||||||
|
|
||||||
|
If specified, it will also be the default entity where the
|
||||||
|
message will be sent after clicked. Otherwise, the "empty
|
||||||
|
peer" will be used, which some bots may not handle correctly.
|
||||||
|
|
||||||
offset (`str`, optional):
|
offset (`str`, optional):
|
||||||
The string offset to use for the bot.
|
The string offset to use for the bot.
|
||||||
|
|
||||||
|
@ -25,17 +42,31 @@ class BotMethods(UserMethods):
|
||||||
The geo point location information to send to the bot
|
The geo point location information to send to the bot
|
||||||
for localised results. Available under some bots.
|
for localised results. Available under some bots.
|
||||||
|
|
||||||
Returns:
|
Returns
|
||||||
A list of `custom.InlineResult
|
A list of `custom.InlineResult
|
||||||
<telethon.tl.custom.inlineresult.InlineResult>`.
|
<telethon.tl.custom.inlineresult.InlineResult>`.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# Make an inline query to @like
|
||||||
|
results = await client.inline_query('like', 'Do you like Telethon?')
|
||||||
|
|
||||||
|
# Send the first result to some chat
|
||||||
|
message = await results[0].click('TelethonOffTopic')
|
||||||
"""
|
"""
|
||||||
bot = await self.get_input_entity(bot)
|
bot = await self.get_input_entity(bot)
|
||||||
|
if entity:
|
||||||
|
peer = await self.get_input_entity(entity)
|
||||||
|
else:
|
||||||
|
peer = types.InputPeerEmpty()
|
||||||
|
|
||||||
result = await self(functions.messages.GetInlineBotResultsRequest(
|
result = await self(functions.messages.GetInlineBotResultsRequest(
|
||||||
bot=bot,
|
bot=bot,
|
||||||
peer=types.InputPeerEmpty(),
|
peer=peer,
|
||||||
query=query,
|
query=query,
|
||||||
offset=offset or '',
|
offset=offset or '',
|
||||||
geo_point=geo_point
|
geo_point=geo_point
|
||||||
))
|
))
|
||||||
|
|
||||||
return custom.InlineResults(self, result)
|
return custom.InlineResults(self, result, entity=peer if entity else None)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user