mirror of
https://github.com/LonamiWebs/Telethon.git
synced 2025-03-13 16:05:49 +03:00
Compare commits
1241 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 |
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
115
.gitignore
vendored
|
@ -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 *
|
|
11
README.rst
11
README.rst
|
@ -5,12 +5,15 @@ 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_ **Python 3**
|
|logo| **Telethon** is an asyncio_ **Python 3**
|
||||||
MTProto_ library to interact with Telegram_'s API.
|
MTProto_ library to interact with Telegram_'s API
|
||||||
|
as a user or through a bot account (bot API alternative).
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
If you have code using Telethon before its 1.0 version, you must
|
If you have code using Telethon before its 1.0 version, you must
|
||||||
read `Compatibility and Convenience`_ to learn how to migrate.
|
read `Compatibility and Convenience`_ to learn how to migrate.
|
||||||
|
As with any third-party library for Telegram, be careful not to
|
||||||
|
break `Telegram's ToS`_ or `Telegram can ban the account`_.
|
||||||
|
|
||||||
What is this?
|
What is this?
|
||||||
-------------
|
-------------
|
||||||
|
@ -74,8 +77,10 @@ useful information.
|
||||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||||
.. _MTProto: https://core.telegram.org/mtproto
|
.. _MTProto: https://core.telegram.org/mtproto
|
||||||
.. _Telegram: https://telegram.org
|
.. _Telegram: https://telegram.org
|
||||||
.. _Compatibility and Convenience: https://telethon.readthedocs.io/en/latest/extra/basic/compatibility-and-convenience.html
|
.. _Compatibility and Convenience: https://docs.telethon.dev/en/stable/misc/compatibility-and-convenience.html
|
||||||
.. _Read The Docs: https://telethon.readthedocs.io
|
.. _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.
|
|
@ -8,23 +8,23 @@ Mastering asyncio
|
||||||
|
|
||||||
|
|
||||||
What's asyncio?
|
What's asyncio?
|
||||||
***************
|
===============
|
||||||
|
|
||||||
asyncio_ is a Python 3's built-in library. This means it's already installed if
|
`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
|
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.
|
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
|
`asyncio` stands for *Asynchronous Input Output*. This is a very powerful
|
||||||
concept to use whenever you work IO. Interacting with the web or external
|
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.
|
APIs such as Telegram's makes a lot of sense this way.
|
||||||
|
|
||||||
|
|
||||||
Why asyncio?
|
Why asyncio?
|
||||||
************
|
============
|
||||||
|
|
||||||
Asynchronous IO makes a lot of sense in a library like Telethon.
|
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
|
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.
|
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
|
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
|
other code can run while the response arrives. That is *a lot* more
|
||||||
|
@ -38,28 +38,28 @@ because tasks are smaller than threads, which are smaller than processes.
|
||||||
|
|
||||||
|
|
||||||
What are asyncio basics?
|
What are asyncio basics?
|
||||||
************************
|
========================
|
||||||
|
|
||||||
|
The code samples below assume that you have Python 3.7 or greater installed.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# First we need the asyncio library
|
# First we need the asyncio library
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
# Then we need a loop to work with
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
# We also need something to run
|
# We also need something to run
|
||||||
async def main():
|
async def main():
|
||||||
for char in 'Hello, world!\n':
|
for char in 'Hello, world!\n':
|
||||||
print(char, end='', flush=True)
|
print(char, end='', flush=True)
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
# Then, we need to run the loop with a task
|
# Then, we can create a new asyncio loop and use it to run our coroutine.
|
||||||
loop.run_until_complete(main())
|
# The creation and tear-down of the loop is hidden away from us.
|
||||||
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
What does telethon.sync do?
|
What does telethon.sync do?
|
||||||
***************************
|
===========================
|
||||||
|
|
||||||
The moment you import any of these:
|
The moment you import any of these:
|
||||||
|
|
||||||
|
@ -96,8 +96,12 @@ Instead of this:
|
||||||
|
|
||||||
.. code-block:: python
|
.. 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
|
import asyncio
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop() # == client.loop
|
||||||
me = loop.run_until_complete(client.get_me())
|
me = loop.run_until_complete(client.get_me())
|
||||||
print(me.username)
|
print(me.username)
|
||||||
|
|
||||||
|
@ -133,7 +137,7 @@ running, and if the loop is running, you must ``await`` things yourself:
|
||||||
|
|
||||||
|
|
||||||
What are async, await and coroutines?
|
What are async, await and coroutines?
|
||||||
*************************************
|
=====================================
|
||||||
|
|
||||||
The ``async`` keyword lets you define asynchronous functions,
|
The ``async`` keyword lets you define asynchronous functions,
|
||||||
also known as coroutines, and also iterate over asynchronous
|
also known as coroutines, and also iterate over asynchronous
|
||||||
|
@ -154,13 +158,10 @@ loops or use ``async with``:
|
||||||
|
|
||||||
print(message.sender.username)
|
print(message.sender.username)
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
asyncio.run(main())
|
||||||
# ^ this assigns the default event loop from the main thread to a variable
|
# ^ 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.
|
||||||
loop.run_until_complete(main())
|
# You should only use this function if there is no other loop running.
|
||||||
# ^ this runs the *entire* loop until the main() function finishes.
|
|
||||||
# While the main() function does not finish, the loop will be running.
|
|
||||||
# While the loop is running, you can't run it again.
|
|
||||||
|
|
||||||
|
|
||||||
The ``await`` keyword blocks the *current* task, and the loop can run
|
The ``await`` keyword blocks the *current* task, and the loop can run
|
||||||
|
@ -180,14 +181,14 @@ concurrently:
|
||||||
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
await asyncio.sleep(delay) # await tells the loop this task is "busy"
|
||||||
print('world') # eventually the loop finishes all tasks
|
print('world') # eventually the loop finishes all tasks
|
||||||
|
|
||||||
loop = asyncio.get_event_loop() # get the default loop for the main thread
|
async def main():
|
||||||
loop.create_task(world(2)) # create the world task, passing 2 as delay
|
asyncio.create_task(world(2)) # create the world task, passing 2 as delay
|
||||||
loop.create_task(hello(delay=1)) # another task, but with delay 1
|
asyncio.create_task(hello(delay=1)) # another task, but with delay 1
|
||||||
|
await asyncio.sleep(3) # wait for three seconds before exiting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# run the event loop forever; ctrl+c to stop it
|
# create a new temporary asyncio loop and use it to run main
|
||||||
# we could also run the loop for three seconds:
|
asyncio.run(main())
|
||||||
# loop.run_until_complete(asyncio.sleep(3))
|
|
||||||
loop.run_forever()
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -205,36 +206,43 @@ The same example, but without the comment noise:
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
print('world')
|
print('world')
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
async def main():
|
||||||
loop.create_task(world(2))
|
asyncio.create_task(world(2))
|
||||||
loop.create_task(hello(1))
|
asyncio.create_task(hello(delay=1))
|
||||||
loop.run_until_complete(asyncio.sleep(3))
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
Can I use threads?
|
Can I use threads?
|
||||||
******************
|
==================
|
||||||
|
|
||||||
Yes, you can, but you must understand that the loops themselves are
|
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. You
|
not thread safe. and you must be sure to know what is happening. The
|
||||||
may want to create a loop in a new thread and make sure to pass it to
|
easiest and cleanest option is to use `asyncio.run` to create and manage
|
||||||
the client:
|
the new event loop for you:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
def go():
|
async def actual_work():
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
client = TelegramClient(..., loop=loop)
|
client = TelegramClient(..., loop=loop)
|
||||||
...
|
... # can use `await` here
|
||||||
|
|
||||||
|
def go():
|
||||||
|
asyncio.run(actual_work())
|
||||||
|
|
||||||
threading.Thread(target=go).start()
|
threading.Thread(target=go).start()
|
||||||
|
|
||||||
|
|
||||||
Generally, **you don't need threads** unless you know what you're doing.
|
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
|
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``
|
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
|
whenever you use the client, or enable the compatible mode. For that, see
|
||||||
:ref:`compatibility-and-convenience`.
|
:ref:`compatibility-and-convenience`.
|
||||||
|
|
||||||
|
@ -244,17 +252,17 @@ You may have seen this error:
|
||||||
|
|
||||||
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
RuntimeError: There is no current event loop in thread 'Thread-1'.
|
||||||
|
|
||||||
It just means you didn't create a loop for that thread, and if you don't
|
It just means you didn't create a loop for that thread. Please refer to
|
||||||
pass a loop when creating the client, it uses ``asyncio.get_event_loop()``,
|
the ``asyncio`` documentation to correctly learn how to set the event loop
|
||||||
which only works in the main thread.
|
for non-main threads.
|
||||||
|
|
||||||
|
|
||||||
client.run_until_disconnected() blocks!
|
client.run_until_disconnected() blocks!
|
||||||
***************************************
|
=======================================
|
||||||
|
|
||||||
All of what `client.run_until_disconnected()
|
All of what `client.run_until_disconnected()
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>` does is
|
||||||
run the asyncio_'s event loop until the client is disconnected. That means
|
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
|
*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:
|
in it. So if you want to run *other* code, create tasks for it:
|
||||||
|
|
||||||
|
@ -274,18 +282,19 @@ in it. So if you want to run *other* code, create tasks for it:
|
||||||
This creates a task for a clock that prints the time every second.
|
This creates a task for a clock that prints the time every second.
|
||||||
You don't need to use `client.run_until_disconnected()
|
You don't need to use `client.run_until_disconnected()
|
||||||
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
|
<telethon.client.updates.UpdateMethods.run_until_disconnected>` either!
|
||||||
You just need to make the loop is running, somehow. ``asyncio.run_forever``
|
You just need to make the loop is running, somehow. `loop.run_forever()
|
||||||
and ``asyncio.run_until_complete`` can also be used to run the loop, and
|
<asyncio.loop.run_forever()>` and `loop.run_until_complete()
|
||||||
Telethon will be happy with any approach.
|
<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.
|
Of course, there are better tools to run code hourly or daily, see below.
|
||||||
|
|
||||||
|
|
||||||
What else can asyncio do?
|
What else can asyncio do?
|
||||||
*************************
|
=========================
|
||||||
|
|
||||||
Asynchronous IO is a really powerful tool, as we've seen. There are plenty
|
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
|
of other useful libraries that also use `asyncio` and that you can integrate
|
||||||
with Telethon.
|
with Telethon.
|
||||||
|
|
||||||
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
|
* `aiohttp <https://github.com/aio-libs/aiohttp>`_ is like the infamous
|
||||||
|
@ -303,27 +312,26 @@ you can run requests in parallel:
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
last, sent, download_path = await asyncio.gather(
|
last, sent, download_path = await asyncio.gather(
|
||||||
client.get_messages('TelethonChat', 10),
|
client.get_messages('telegram', 10),
|
||||||
client.send_message('TelethonOfftopic', 'Hey guys!'),
|
client.send_message('me', 'Using asyncio!'),
|
||||||
client.download_profile_photo('TelethonChat')
|
client.download_profile_photo('telegram')
|
||||||
)
|
)
|
||||||
|
|
||||||
loop.run_until_complete(main())
|
loop.run_until_complete(main())
|
||||||
|
|
||||||
|
|
||||||
This code will get the 10 last messages from `@TelethonChat
|
This code will get the 10 last messages from `@telegram
|
||||||
<https://t.me/TelethonChat>`_, send one to `@TelethonOfftopic
|
<https://t.me/telegram>`_, send one to the chat with yourself, and also
|
||||||
<https://t.me/TelethonOfftopic>`_, and also download the profile
|
download the profile photo of the channel. `asyncio` will run all these
|
||||||
photo of the main group. asyncio_ will run all these three tasks
|
three tasks at the same time. You can run all the tasks you want this way.
|
||||||
at the same time. You can run all the tasks you want this way.
|
|
||||||
|
|
||||||
A different way would be:
|
A different way would be:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
loop.create_task(client.get_messages('TelethonChat', 10))
|
loop.create_task(client.get_messages('telegram', 10))
|
||||||
loop.create_task(client.send_message('TelethonOfftopic', 'Hey guys!'))
|
loop.create_task(client.send_message('me', 'Using asyncio!'))
|
||||||
loop.create_task(client.download_profile_photo('TelethonChat'))
|
loop.create_task(client.download_profile_photo('telegram'))
|
||||||
|
|
||||||
They will run in the background as long as the loop is running too.
|
They will run in the background as long as the loop is running too.
|
||||||
|
|
||||||
|
@ -339,7 +347,7 @@ combine all the libraries you want. People seem to forget this simple fact!
|
||||||
|
|
||||||
|
|
||||||
Why does client.start() work outside async?
|
Why does client.start() work outside async?
|
||||||
*******************************************
|
===========================================
|
||||||
|
|
||||||
Because it's so common that it's really convenient to offer said
|
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
|
functionality by default. This means you can set up all your event
|
||||||
|
@ -352,11 +360,9 @@ Using the client in a ``with`` block, `start
|
||||||
all support this.
|
all support this.
|
||||||
|
|
||||||
Where can I read more?
|
Where can I read more?
|
||||||
**********************
|
======================
|
||||||
|
|
||||||
`Check out my blog post
|
`Check out my blog post
|
||||||
<https://lonamiwebs.github.io/blog/asyncio/>`_ about asyncio_, which
|
<https://lonami.dev/blog/asyncio/>`_ about `asyncio`, which
|
||||||
has some more examples and pictures to help you understand what happens
|
has some more examples and pictures to help you understand what happens
|
||||||
when the loop runs.
|
when the loop runs.
|
||||||
|
|
||||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
|
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
|
|
@ -6,8 +6,11 @@ Session Files
|
||||||
|
|
||||||
.. contents::
|
.. contents::
|
||||||
|
|
||||||
What are sessions?
|
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
|
||||||
|
@ -43,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
|
||||||
|
@ -54,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
|
||||||
|
|
||||||
|
@ -91,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:
|
||||||
|
|
||||||
|
@ -129,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.
|
||||||
|
@ -143,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,13 +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::
|
.. 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`:
|
||||||
|
@ -21,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`:
|
||||||
|
@ -41,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`:
|
||||||
|
|
||||||
|
@ -55,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`:
|
||||||
|
@ -69,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,230 +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:`compatibility-and-convenience` 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,73 +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())
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
|
||||||
...
|
|
|
@ -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,213 +0,0 @@
|
||||||
.. _entities:
|
|
||||||
|
|
||||||
=========================
|
|
||||||
Users, Chats and Channels
|
|
||||||
=========================
|
|
||||||
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
with client:
|
|
||||||
# Does it have an username? Use it!
|
|
||||||
entity = client.get_entity(username)
|
|
||||||
|
|
||||||
# Do you have a conversation open with them? Get dialogs.
|
|
||||||
client.get_dialogs()
|
|
||||||
|
|
||||||
# Are they participant of some group? Get them.
|
|
||||||
client.get_participants('TelethonChat')
|
|
||||||
|
|
||||||
# Is the entity the original sender of a forwarded message? Get it.
|
|
||||||
client.get_messages('TelethonChat', 100)
|
|
||||||
|
|
||||||
# NOW you can use the ID, anywhere!
|
|
||||||
entity = client.get_entity(123456)
|
|
||||||
client.send_message(123456, 'Hi!')
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
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,95 +0,0 @@
|
||||||
.. _getting-started:
|
|
||||||
|
|
||||||
|
|
||||||
===============
|
|
||||||
Getting Started
|
|
||||||
===============
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
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,99 +0,0 @@
|
||||||
.. _installation:
|
|
||||||
|
|
||||||
============
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
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,104 +0,0 @@
|
||||||
.. _telegram-client:
|
|
||||||
|
|
||||||
==============
|
|
||||||
TelegramClient
|
|
||||||
==============
|
|
||||||
|
|
||||||
.. 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/tree/master/telethon/client
|
|
|
@ -1,333 +0,0 @@
|
||||||
.. _working-with-updates:
|
|
||||||
|
|
||||||
====================
|
|
||||||
Working with Updates
|
|
||||||
====================
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Coming from Telethon before it reached its version 1.0?
|
|
||||||
Make sure to read :ref:`compatibility-and-convenience`!
|
|
||||||
Otherwise, you can ignore this note and just follow along.
|
|
||||||
|
|
||||||
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,73 +0,0 @@
|
||||||
====
|
|
||||||
Bots
|
|
||||||
====
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
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,327 +0,0 @@
|
||||||
===============================
|
|
||||||
Working with Chats and Channels
|
|
||||||
===============================
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-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
|
|
||||||
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,645 +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.
|
|
||||||
|
|
||||||
It can look a bit strange at first, but you can make inline queries in no
|
|
||||||
chat in particular, and then click a *result* to send it to some chat.
|
|
||||||
|
|
||||||
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')``.
|
|
||||||
|
|
||||||
Answering Inline Queries
|
|
||||||
************************
|
|
||||||
|
|
||||||
As a bot, you can answer to inline queries with `events.InlineQuery
|
|
||||||
<telethon.events.inlinequery.InlineQuery>`. You should make use of the
|
|
||||||
`builder <telethon.tl.custom.inlinebuilder.InlineBuilder>` property
|
|
||||||
to conveniently build the list of results to show to the user. Remember
|
|
||||||
to check the properties of the `InlineQuery.Event
|
|
||||||
<telethon.events.inlinequery.InlineQuery.Event>`:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@bot.on(events.InlineQuery)
|
|
||||||
async def handler(event):
|
|
||||||
builder = event.builder
|
|
||||||
|
|
||||||
rev_text = event.text[::-1]
|
|
||||||
await event.answer([
|
|
||||||
builder.article('Reverse text', text=rev_text),
|
|
||||||
builder.photo('/path/to/photo.jpg')
|
|
||||||
])
|
|
||||||
|
|
||||||
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 Lonami!
|
|
||||||
|
|
||||||
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,140 +0,0 @@
|
||||||
=====================
|
|
||||||
Working with messages
|
|
||||||
=====================
|
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
These examples assume you have read :ref:`accessing-the-full-api`.
|
|
||||||
|
|
||||||
.. contents::
|
|
||||||
|
|
||||||
|
|
||||||
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,28 +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::
|
|
||||||
|
|
||||||
* Are you new here? Jump straight into :ref:`getting-started`!
|
|
||||||
* Looking for available friendly methods? See :ref:`telethon-client`.
|
|
||||||
* Used Telethon before v1.0? See :ref:`compatibility-and-convenience`.
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -30,94 +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/working-with-updates
|
|
||||||
extra/basic/compatibility-and-convenience
|
|
||||||
|
|
||||||
|
|
||||||
.. _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
|
||||||
extra/advanced-usage/mastering-asyncio
|
|
||||||
|
|
||||||
|
|
||||||
.. _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
|
@ -4,7 +4,7 @@
|
||||||
Compatibility and Convenience
|
Compatibility and Convenience
|
||||||
=============================
|
=============================
|
||||||
|
|
||||||
Telethon is an ``asyncio`` library. Compatibility is an important concern,
|
Telethon is an `asyncio` library. Compatibility is an important concern,
|
||||||
and while it can't always be kept and mistakes happens, the :ref:`changelog`
|
and while it can't always be kept and mistakes happens, the :ref:`changelog`
|
||||||
is there to tell you when these important changes happen.
|
is there to tell you when these important changes happen.
|
||||||
|
|
||||||
|
@ -12,11 +12,11 @@ is there to tell you when these important changes happen.
|
||||||
|
|
||||||
|
|
||||||
Compatibility
|
Compatibility
|
||||||
*************
|
=============
|
||||||
|
|
||||||
Some decisions when developing will inevitable be proven wrong in the future.
|
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
|
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
|
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.
|
like Telethon to make a good use of it.
|
||||||
|
|
||||||
If you have old code, **just use old versions** of the library! There is
|
If you have old code, **just use old versions** of the library! There is
|
||||||
|
@ -34,7 +34,7 @@ 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
|
Sometimes, other small decisions are made. These all will be reflected in the
|
||||||
:ref:`changelog` which you should read when upgrading.
|
:ref:`changelog` which you should read when upgrading.
|
||||||
|
|
||||||
If you want to jump the ``asyncio`` boat, here are some of the things you will
|
If you want to jump the `asyncio` boat, here are some of the things you will
|
||||||
need to start migrating really old code:
|
need to start migrating really old code:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -76,7 +76,7 @@ the chat or sender. If you don't use updates, you're done!
|
||||||
|
|
||||||
|
|
||||||
Convenience
|
Convenience
|
||||||
***********
|
===========
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ Convenience
|
||||||
This makes the examples shorter and easier to think about.
|
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
|
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
|
forget about `asyncio` and just work with sequential code. This can prove
|
||||||
to be a powerful hybrid for running under the Python REPL too.
|
to be a powerful hybrid for running under the Python REPL too.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
@ -134,7 +134,7 @@ This keeps the best of both worlds as a sane default.
|
||||||
them too. Otherwise, there is no need to do so with this mode.
|
them too. Otherwise, there is no need to do so with this mode.
|
||||||
|
|
||||||
Speed
|
Speed
|
||||||
*****
|
=====
|
||||||
|
|
||||||
When you're ready to micro-optimize your application, or if you simply
|
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,
|
don't need to call any non-basic methods from a synchronous context,
|
||||||
|
@ -161,27 +161,25 @@ just get rid of ``telethon.sync`` and work inside an ``async def``:
|
||||||
|
|
||||||
await client.run_until_disconnected()
|
await client.run_until_disconnected()
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
asyncio.run(main())
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
|
|
||||||
The ``telethon.sync`` magic module simply wraps every method behind:
|
The ``telethon.sync`` magic module essentially wraps every method behind:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
asyncio.run(main())
|
||||||
loop.run_until_complete(main())
|
|
||||||
|
|
||||||
So that you don't have to write it yourself every time. That's the
|
With some other tricks, so that you don't have to write it yourself every time.
|
||||||
overhead you pay if you import it, and what you save if you don't.
|
That's the overhead you pay if you import it, and what you save if you don't.
|
||||||
|
|
||||||
Learning
|
Learning
|
||||||
********
|
========
|
||||||
|
|
||||||
You know the library uses ``asyncio`` everywhere, and you want to learn
|
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
|
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
|
documentation wants you to learn how to use Telethon correctly, and for
|
||||||
that, you need to use ``asyncio`` correctly too. For this reason, there
|
that, you need to use `asyncio` correctly too. For this reason, there
|
||||||
is a section called :ref:`mastering-asyncio` that will introduce you to
|
is a section called :ref:`mastering-asyncio` that will introduce you to
|
||||||
the ``asyncio`` world, with links to more resources for learning how 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.
|
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,37 +1,52 @@
|
||||||
.. _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
|
.. automodule:: telethon.client.telegrambaseclient
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
: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:
|
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,27 +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:
|
|
|
@ -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,99 +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\.inlinebuilder module
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.inlinebuilder
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.inlineresult module
|
|
||||||
-----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.inlineresult
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
telethon\.tl\.custom\.inlineresults module
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: telethon.tl.custom.inlineresults
|
|
||||||
: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
|
|
||||||
|
|
153
setup.py
153
setup.py
|
@ -15,66 +15,75 @@ import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from sys import argv
|
import sys
|
||||||
|
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 = os.path.join(GENERATOR_DIR, 'data', 'errors.csv')
|
GENERATOR_DIR = Path('telethon_generator')
|
||||||
ERRORS_OUT = os.path.join(LIBRARY_DIR, 'errors', 'rpcerrorlist.py')
|
LIBRARY_DIR = Path('telethon')
|
||||||
|
|
||||||
METHODS_IN = os.path.join(GENERATOR_DIR, 'data', 'methods.csv')
|
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\
|
from telethon_generator.parsers import\
|
||||||
parse_errors, parse_methods, parse_tl, find_layer
|
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
|
||||||
|
|
||||||
layer = find_layer(TLOBJECT_IN_TL)
|
layer = next(filter(None, map(find_layer, TLOBJECT_IN_TLS)))
|
||||||
errors = list(parse_errors(ERRORS_IN))
|
errors = list(parse_errors(ERRORS_IN))
|
||||||
methods = list(parse_methods(METHODS_IN, {e.str_code: e for e in errors}))
|
methods = list(parse_methods(METHODS_IN, FRIENDLY_IN, {e.str_code: e for e in errors}))
|
||||||
|
|
||||||
tlobjects = list(itertools.chain(
|
tlobjects = list(itertools.chain(*(
|
||||||
parse_tl(TLOBJECT_IN_CORE_TL, layer, methods),
|
parse_tl(file, layer, methods) for file in TLOBJECT_IN_TLS)))
|
||||||
parse_tl(TLOBJECT_IN_TL, layer, 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')
|
||||||
|
@ -94,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, methods, 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
|
||||||
|
@ -214,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']
|
||||||
}
|
}
|
||||||
|
@ -229,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,8 +183,7 @@ 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()
|
||||||
|
@ -159,19 +196,12 @@ class AuthMethods(MessageParseMethods, UserMethods):
|
||||||
if not value:
|
if not value:
|
||||||
raise errors.PhoneCodeEmptyError(request=None)
|
raise errors.PhoneCodeEmptyError(request=None)
|
||||||
|
|
||||||
if sign_up:
|
# Raises SessionPasswordNeededError if 2FA enabled
|
||||||
me = await self.sign_up(value, first_name, last_name)
|
me = await self.sign_in(phone, code=value)
|
||||||
else:
|
|
||||||
# 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,
|
||||||
|
@ -205,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(None)
|
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
|
||||||
|
@ -241,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:
|
||||||
|
@ -262,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:
|
||||||
return result.user
|
self._phone_code_hash.pop(phone, None)
|
||||||
|
raise
|
||||||
|
|
||||||
async def sign_up(self, code, first_name, last_name=''):
|
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':
|
||||||
"""
|
"""
|
||||||
Signs up to Telegram if you don't have an account yet.
|
This method can no longer be used, and will immediately raise a ``ValueError``.
|
||||||
You must call .send_code_request(phone) first.
|
See `issue #4050 <https://github.com/LonamiWebs/Telethon/issues/4050>`_ for context.
|
||||||
|
|
||||||
**By using this method you're agreeing to Telegram's
|
|
||||||
Terms of Service. This is required and your account
|
|
||||||
will be banned otherwise.** See https://telegram.org/tos
|
|
||||||
and https://core.telegram.org/api/terms.
|
|
||||||
|
|
||||||
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()
|
raise ValueError('Third-party applications cannot sign up for Telegram. See https://github.com/LonamiWebs/Telethon/issues/4050 for details')
|
||||||
if me:
|
|
||||||
return me
|
|
||||||
|
|
||||||
if self._tos and self._tos.text:
|
async def _on_login(self, user):
|
||||||
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
|
|
||||||
)
|
|
||||||
return result.user
|
|
||||||
|
|
||||||
async def send_code_request(self, phone, *, force_sms=False):
|
|
||||||
"""
|
"""
|
||||||
Sends a code request to the specified phone number.
|
Callback called whenever the login or sign up process completes.
|
||||||
|
|
||||||
Args:
|
Returns the input user parameter.
|
||||||
|
"""
|
||||||
|
self._mb_entity_cache.set_self_user(user.id, user.bot, user.access_hash)
|
||||||
|
self._authorized = True
|
||||||
|
|
||||||
|
state = await self(functions.updates.GetStateRequest())
|
||||||
|
# the server may send an old qts in getState
|
||||||
|
difference = await self(functions.updates.GetDifferenceRequest(pts=state.pts, date=state.date, qts=state.qts))
|
||||||
|
|
||||||
|
if isinstance(difference, types.updates.Difference):
|
||||||
|
state = difference.state
|
||||||
|
elif isinstance(difference, types.updates.DifferenceSlice):
|
||||||
|
state = difference.intermediate_state
|
||||||
|
elif isinstance(difference, types.updates.DifferenceTooLong):
|
||||||
|
state.pts = difference.pts
|
||||||
|
|
||||||
|
self._message_box.load(SessionState(0, 0, 0, state.pts, state.qts, int(state.date.timestamp()), state.seq, 0), [])
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
async def send_code_request(
|
||||||
|
self: 'TelegramClient',
|
||||||
|
phone: str,
|
||||||
|
*,
|
||||||
|
force_sms: bool = False,
|
||||||
|
_retry_count: int = 0) -> 'types.auth.SentCode':
|
||||||
|
"""
|
||||||
|
Sends the Telegram code needed to login to the given phone number.
|
||||||
|
|
||||||
|
Arguments
|
||||||
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._self_input_peer = None
|
self._mb_entity_cache.set_self_user(None, None, None)
|
||||||
self._state.pts = -1
|
self._authorized = False
|
||||||
self.disconnect()
|
|
||||||
|
await self.disconnect()
|
||||||
self.session.delete()
|
self.session.delete()
|
||||||
|
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):
|
|
||||||
if self._loop.is_running():
|
|
||||||
raise RuntimeError(
|
|
||||||
'You must use "async with" if the event loop '
|
|
||||||
'is running (i.e. you are inside an "async def")'
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.start()
|
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
|
||||||
def __exit__(self, *args):
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
async def __aexit__(self, *args):
|
async def __aexit__(self, *args):
|
||||||
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)
|
||||||
|
|
|
@ -1,20 +1,42 @@
|
||||||
from .updates import UpdateMethods
|
import typing
|
||||||
|
|
||||||
|
from .. import utils, hints
|
||||||
from ..tl import types, custom
|
from ..tl import types, custom
|
||||||
from .. import utils, events
|
|
||||||
|
|
||||||
|
|
||||||
class ButtonMethods(UpdateMethods):
|
class ButtonMethods:
|
||||||
def build_reply_markup(self, buttons, inline_only=False):
|
@staticmethod
|
||||||
|
def build_reply_markup(
|
||||||
|
buttons: 'typing.Optional[hints.MarkupLike]',
|
||||||
|
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]':
|
||||||
"""
|
"""
|
||||||
Builds a :tl`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
|
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
|
||||||
the given buttons, or does nothing if either no buttons are
|
the given buttons.
|
||||||
provided or the provided argument is already a reply markup.
|
|
||||||
|
|
||||||
This will add any event handlers defined in the
|
Does nothing if either no buttons are provided or the provided
|
||||||
buttons and delete old ones not to call them twice,
|
argument is already a reply markup.
|
||||||
so you should probably call this method manually for
|
|
||||||
serious bots instead re-adding handlers every time you
|
You should consider using this method if you are going to reuse
|
||||||
send a message. Magic can only go so far.
|
the markup very often. Otherwise, it is not necessary.
|
||||||
|
|
||||||
|
This method is **not** asynchronous (don't use ``await`` on it).
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
buttons (`hints.MarkupLike`):
|
||||||
|
The button, list of buttons, array of buttons or markup
|
||||||
|
to convert into a markup.
|
||||||
|
|
||||||
|
inline_only (`bool`, optional):
|
||||||
|
Whether the buttons **must** be inline buttons only or not.
|
||||||
|
|
||||||
|
Example
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from telethon import Button
|
||||||
|
|
||||||
|
markup = client.build_reply_markup(Button.inline('hi'))
|
||||||
|
# later
|
||||||
|
await client.send_message(chat, 'click me', buttons=markup)
|
||||||
"""
|
"""
|
||||||
if buttons is None:
|
if buttons is None:
|
||||||
return None
|
return None
|
||||||
|
@ -27,17 +49,29 @@ class ButtonMethods(UpdateMethods):
|
||||||
|
|
||||||
if not utils.is_list_like(buttons):
|
if not utils.is_list_like(buttons):
|
||||||
buttons = [[buttons]]
|
buttons = [[buttons]]
|
||||||
elif not utils.is_list_like(buttons[0]):
|
elif not buttons or not utils.is_list_like(buttons[0]):
|
||||||
buttons = [buttons]
|
buttons = [buttons]
|
||||||
|
|
||||||
is_inline = False
|
is_inline = False
|
||||||
is_normal = False
|
is_normal = False
|
||||||
|
resize = None
|
||||||
|
single_use = None
|
||||||
|
selective = None
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for row in buttons:
|
for row in buttons:
|
||||||
current = []
|
current = []
|
||||||
for button in row:
|
for button in row:
|
||||||
if isinstance(button, custom.MessageButton):
|
if isinstance(button, custom.Button):
|
||||||
|
if button.resize is not None:
|
||||||
|
resize = button.resize
|
||||||
|
if button.single_use is not None:
|
||||||
|
single_use = button.single_use
|
||||||
|
if button.selective is not None:
|
||||||
|
selective = button.selective
|
||||||
|
|
||||||
|
button = button.button
|
||||||
|
elif isinstance(button, custom.MessageButton):
|
||||||
button = button.button
|
button = button.button
|
||||||
|
|
||||||
inline = custom.Button._is_inline(button)
|
inline = custom.Button._is_inline(button)
|
||||||
|
@ -57,5 +91,6 @@ class ButtonMethods(UpdateMethods):
|
||||||
raise ValueError('You cannot mix inline with normal buttons')
|
raise ValueError('You cannot mix inline with normal buttons')
|
||||||
elif is_inline:
|
elif is_inline:
|
||||||
return types.ReplyInlineMarkup(rows)
|
return types.ReplyInlineMarkup(rows)
|
||||||
elif is_normal:
|
# elif is_normal:
|
||||||
return types.ReplyKeyboardMarkup(rows)
|
return types.ReplyKeyboardMarkup(
|
||||||
|
rows, resize=resize, single_use=single_use, selective=selective)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user