python-telegram 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_telegram-0.19.0.dist-info/LICENSE +21 -0
- python_telegram-0.19.0.dist-info/METADATA +118 -0
- python_telegram-0.19.0.dist-info/RECORD +15 -0
- python_telegram-0.19.0.dist-info/WHEEL +5 -0
- python_telegram-0.19.0.dist-info/top_level.txt +1 -0
- telegram/__init__.py +3 -0
- telegram/_version.py +16 -0
- telegram/client.py +891 -0
- telegram/lib/darwin/libtdjson.dylib +0 -0
- telegram/lib/linux/libtdjson.so +0 -0
- telegram/py.typed +0 -0
- telegram/tdjson.py +119 -0
- telegram/text.py +46 -0
- telegram/utils.py +67 -0
- telegram/worker.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Alexander Akhmetov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-telegram
|
|
3
|
+
Version: 0.19.0
|
|
4
|
+
Summary: Python library to help you build your own Telegram clients
|
|
5
|
+
Author-email: Alexander Akhmetov <me@alx.cx>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/alexander-akhmetov/python-telegram
|
|
8
|
+
Project-URL: Documentation, https://python-telegram.readthedocs.io/en/latest/
|
|
9
|
+
Project-URL: Tutorial, https://python-telegram.readthedocs.io/en/latest/tutorial.html
|
|
10
|
+
Project-URL: Changelog, https://python-telegram.readthedocs.io/en/latest/changelog.html
|
|
11
|
+
Keywords: telegram,client,api,tdlib,tdjson,td
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: telegram-text ==0.2.0
|
|
26
|
+
|
|
27
|
+
# python-telegram
|
|
28
|
+
|
|
29
|
+
[](https://github.com/alexander-akhmetov/python-telegram/actions)
|
|
30
|
+
[](https://pypi.python.org/pypi/python-telegram)
|
|
31
|
+
[](https://hub.docker.com/r/akhmetov/python-telegram/)
|
|
32
|
+

|
|
33
|
+
|
|
34
|
+
Python API for the [tdlib](https://github.com/tdlib/td) library.
|
|
35
|
+
It helps you build your own Telegram clients.
|
|
36
|
+
|
|
37
|
+
- [Changelog](https://python-telegram.readthedocs.io/en/latest/changelog.html)
|
|
38
|
+
- [Documentation](http://python-telegram.readthedocs.io)
|
|
39
|
+
- [Tutorial](http://python-telegram.readthedocs.io/en/latest/tutorial.html)
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
This library requires Python 3.9+ and Linux or MacOS. Windows is not supported.
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
pip install python-telegram
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
See [documentation](http://python-telegram.readthedocs.io/en/latest/#installation) for more details.
|
|
50
|
+
|
|
51
|
+
### tdlib
|
|
52
|
+
|
|
53
|
+
`python-telegram` comes with a precompiled `tdlib` library for Linux and MacOS. But it is highly recommended to [compile](https://tdlib.github.io/td/build.html) it yourself.
|
|
54
|
+
The precompiled library may not work on some systems, it is dynamically linked and requires specific versions of additional libraries.
|
|
55
|
+
|
|
56
|
+
```shell
|
|
57
|
+
|
|
58
|
+
### Docker
|
|
59
|
+
|
|
60
|
+
This library has a [docker image](https://hub.docker.com/r/akhmetov/python-telegram/):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
docker run -i -t --rm \
|
|
64
|
+
-v /tmp/docker-python-telegram/:/tmp/ \
|
|
65
|
+
akhmetov/python-telegram \
|
|
66
|
+
python3 /app/examples/send_message.py $(API_ID) $(API_HASH) $(PHONE) $(CHAT_ID) $(TEXT)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## How to use the library
|
|
70
|
+
|
|
71
|
+
Check out the [tutorial](http://python-telegram.readthedocs.io/en/latest/tutorial.html) for more details.
|
|
72
|
+
|
|
73
|
+
Basic example:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from telegram.client import Telegram
|
|
77
|
+
from telegram.text import Spoiler
|
|
78
|
+
|
|
79
|
+
tg = Telegram(
|
|
80
|
+
api_id='api_id',
|
|
81
|
+
api_hash='api_hash',
|
|
82
|
+
phone='+31611111111', # you can pass 'bot_token' instead
|
|
83
|
+
database_encryption_key='changekey123',
|
|
84
|
+
files_directory='/tmp/.tdlib_files/',
|
|
85
|
+
)
|
|
86
|
+
tg.login()
|
|
87
|
+
|
|
88
|
+
# If this is the first run, the library needs to preload all chats.
|
|
89
|
+
# Otherwise, the message will not be sent.
|
|
90
|
+
result = tg.get_chats()
|
|
91
|
+
result.wait()
|
|
92
|
+
|
|
93
|
+
chat_id: int
|
|
94
|
+
result = tg.send_message(chat_id, Spoiler('Hello world!'))
|
|
95
|
+
|
|
96
|
+
# `tdlib` is asynchronous, so `python-telegram` always returns an `AsyncResult` object.
|
|
97
|
+
# You can receive a result with the `wait` method of this object.
|
|
98
|
+
result.wait()
|
|
99
|
+
print(result.update)
|
|
100
|
+
|
|
101
|
+
tg.stop() # You must call `stop` at the end of the script.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
You can also use `call_method` to call any [tdlib method](https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1_function.html):
|
|
105
|
+
|
|
106
|
+
``` python
|
|
107
|
+
tg.call_method('getUser', params={'user_id': user_id})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
More examples can be found in the [/examples/ directory](/examples/).
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
More information is available in the [documentation](http://python-telegram.readthedocs.io).
|
|
115
|
+
|
|
116
|
+
## Development
|
|
117
|
+
|
|
118
|
+
See [CONTRIBUTING.md](/CONTRIBUTING.md).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
telegram/__init__.py,sha256=qAS6-XyOJwuY1CBWqAGqLYIErmDpFtOUFyOzct4LjeA,46
|
|
2
|
+
telegram/_version.py,sha256=JnI7tq4jD4cDGz5sWAnzTPeWXxlVsA9wcpoo7zvk39o,413
|
|
3
|
+
telegram/client.py,sha256=j4EdmlGvTnmRgr_kAuBNRed6-_7t0nl1IG4bviPTvQQ,28585
|
|
4
|
+
telegram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
telegram/tdjson.py,sha256=GJKdFIGHBDbm989khk8q1TaWs6SUHjN-zeRt0OXAu7c,4575
|
|
6
|
+
telegram/text.py,sha256=EG1zaXLTRFHxLPm9VI0-HUeAoDiZ_at6ZV43MhQz7U4,802
|
|
7
|
+
telegram/utils.py,sha256=Ney4fgCBsKt9VA93wftM37vcvLKFjcWYrln3Mj5lwes,2056
|
|
8
|
+
telegram/worker.py,sha256=FiQaGSD4Xv2eOpP8aE-lu-cSx69vhYZNqhjB6Fp25mY,1128
|
|
9
|
+
telegram/lib/darwin/libtdjson.dylib,sha256=IZO0hfze2mgmUnihYCpNyZ42It5DSB659qrjFhNIZhU,22428576
|
|
10
|
+
telegram/lib/linux/libtdjson.so,sha256=1Mh6a-ENrbGmdZDL3bcfJsoTPw1oQdHx6leuL5tpVJc,38488384
|
|
11
|
+
python_telegram-0.19.0.dist-info/LICENSE,sha256=ZiH3WbD6o37C65kBPDH1TScRsXVK7ACPqmB_OXuHB08,1075
|
|
12
|
+
python_telegram-0.19.0.dist-info/METADATA,sha256=dJL-CdOxduUXzw-fpNJBbg7hJWaNbDVJWYhnj93tudg,4318
|
|
13
|
+
python_telegram-0.19.0.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
|
|
14
|
+
python_telegram-0.19.0.dist-info/top_level.txt,sha256=tfrh9q1x_2mn166E6RftggS4sc4T_5C1BYamJkSoWg0,9
|
|
15
|
+
python_telegram-0.19.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
telegram
|
telegram/__init__.py
ADDED
telegram/_version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.19.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 19, 0)
|
telegram/client.py
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import time
|
|
3
|
+
import queue
|
|
4
|
+
import signal
|
|
5
|
+
import typing
|
|
6
|
+
import getpass
|
|
7
|
+
import logging
|
|
8
|
+
import base64
|
|
9
|
+
import threading
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Dict,
|
|
15
|
+
List,
|
|
16
|
+
Type,
|
|
17
|
+
Callable,
|
|
18
|
+
Optional,
|
|
19
|
+
DefaultDict,
|
|
20
|
+
Union,
|
|
21
|
+
Tuple,
|
|
22
|
+
Literal,
|
|
23
|
+
)
|
|
24
|
+
from types import FrameType
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
import enum
|
|
27
|
+
|
|
28
|
+
from telegram import VERSION
|
|
29
|
+
from telegram.utils import AsyncResult
|
|
30
|
+
from telegram.tdjson import TDJson
|
|
31
|
+
from telegram.worker import BaseWorker, SimpleWorker
|
|
32
|
+
from telegram.text import Element
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
MESSAGE_HANDLER_TYPE: str = "updateNewMessage"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AuthorizationState(enum.Enum):
|
|
42
|
+
NONE = None
|
|
43
|
+
WAIT_CODE = "authorizationStateWaitCode"
|
|
44
|
+
WAIT_PASSWORD = "authorizationStateWaitPassword"
|
|
45
|
+
WAIT_TDLIB_PARAMETERS = "authorizationStateWaitTdlibParameters"
|
|
46
|
+
WAIT_ENCRYPTION_KEY = "authorizationStateWaitEncryptionKey"
|
|
47
|
+
WAIT_PHONE_NUMBER = "authorizationStateWaitPhoneNumber"
|
|
48
|
+
WAIT_REGISTRATION = "authorizationStateWaitRegistration"
|
|
49
|
+
READY = "authorizationStateReady"
|
|
50
|
+
CLOSING = "authorizationStateClosing"
|
|
51
|
+
CLOSED = "authorizationStateClosed"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Telegram:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_id: int,
|
|
58
|
+
api_hash: str,
|
|
59
|
+
database_encryption_key: Union[str, bytes],
|
|
60
|
+
phone: Optional[str] = None,
|
|
61
|
+
bot_token: Optional[str] = None,
|
|
62
|
+
library_path: Optional[str] = None,
|
|
63
|
+
worker: Optional[Type[BaseWorker]] = None,
|
|
64
|
+
files_directory: Optional[Union[str, Path]] = None,
|
|
65
|
+
use_test_dc: bool = False,
|
|
66
|
+
use_message_database: bool = True,
|
|
67
|
+
device_model: str = "python-telegram",
|
|
68
|
+
application_version: str = VERSION,
|
|
69
|
+
system_version: str = "unknown",
|
|
70
|
+
system_language_code: str = "en",
|
|
71
|
+
login: bool = False,
|
|
72
|
+
default_workers_queue_size: int = 1000,
|
|
73
|
+
tdlib_verbosity: int = 2,
|
|
74
|
+
proxy_server: str = "",
|
|
75
|
+
proxy_port: int = 0,
|
|
76
|
+
proxy_type: Optional[Dict[str, str]] = None,
|
|
77
|
+
use_secret_chats: bool = True,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Args:
|
|
81
|
+
api_id - ID of your app (https://my.telegram.org/apps/)
|
|
82
|
+
api_hash - api_hash of your app (https://my.telegram.org/apps/)
|
|
83
|
+
phone - your phone number
|
|
84
|
+
library_path - you can change path to the compiled libtdjson library
|
|
85
|
+
worker - worker to process updates
|
|
86
|
+
files_directory - directory for the tdlib's files (database, images, etc.)
|
|
87
|
+
use_test_dc - use test datacenter
|
|
88
|
+
use_message_database
|
|
89
|
+
use_secret_chats
|
|
90
|
+
device_model
|
|
91
|
+
application_version
|
|
92
|
+
system_version
|
|
93
|
+
system_language_code
|
|
94
|
+
"""
|
|
95
|
+
self.api_id = api_id
|
|
96
|
+
self.api_hash = api_hash
|
|
97
|
+
self.library_path = library_path
|
|
98
|
+
self.phone = phone
|
|
99
|
+
self.bot_token = bot_token
|
|
100
|
+
self.use_test_dc = use_test_dc
|
|
101
|
+
self.device_model = device_model
|
|
102
|
+
self.system_version = system_version
|
|
103
|
+
self.system_language_code = system_language_code
|
|
104
|
+
self.application_version = application_version
|
|
105
|
+
self.use_message_database = use_message_database
|
|
106
|
+
self._queue_put_timeout = 10
|
|
107
|
+
self.proxy_server = proxy_server
|
|
108
|
+
self.proxy_port = proxy_port
|
|
109
|
+
self.proxy_type = proxy_type
|
|
110
|
+
self.use_secret_chats = use_secret_chats
|
|
111
|
+
self.authorization_state = AuthorizationState.NONE
|
|
112
|
+
|
|
113
|
+
if not self.bot_token and not self.phone:
|
|
114
|
+
raise ValueError("You must provide bot_token or phone")
|
|
115
|
+
|
|
116
|
+
self._database_encryption_key = database_encryption_key
|
|
117
|
+
if isinstance(self._database_encryption_key, str):
|
|
118
|
+
self._database_encryption_key = self._database_encryption_key.encode()
|
|
119
|
+
|
|
120
|
+
self._database_encryption_key = base64.b64encode(self._database_encryption_key).decode()
|
|
121
|
+
|
|
122
|
+
if not files_directory:
|
|
123
|
+
hasher = hashlib.md5()
|
|
124
|
+
str_to_encode: str = self.phone or self.bot_token # type: ignore
|
|
125
|
+
hasher.update(str_to_encode.encode("utf-8"))
|
|
126
|
+
directory_name = hasher.hexdigest()
|
|
127
|
+
files_directory = Path(tempfile.gettempdir()) / ".tdlib_files" / directory_name
|
|
128
|
+
|
|
129
|
+
self.files_directory = Path(files_directory)
|
|
130
|
+
|
|
131
|
+
self._authorized = False
|
|
132
|
+
self._stopped = threading.Event()
|
|
133
|
+
|
|
134
|
+
# todo: move to worker
|
|
135
|
+
self._workers_queue: queue.Queue = queue.Queue(maxsize=default_workers_queue_size)
|
|
136
|
+
|
|
137
|
+
if not worker:
|
|
138
|
+
worker = SimpleWorker
|
|
139
|
+
self.worker: BaseWorker = worker(queue=self._workers_queue)
|
|
140
|
+
|
|
141
|
+
self._results: Dict[str, AsyncResult] = {}
|
|
142
|
+
self._update_handlers: DefaultDict[str, List[Callable]] = defaultdict(list)
|
|
143
|
+
|
|
144
|
+
self._tdjson = TDJson(library_path=library_path, verbosity=tdlib_verbosity)
|
|
145
|
+
self._run()
|
|
146
|
+
|
|
147
|
+
if login:
|
|
148
|
+
self.login()
|
|
149
|
+
|
|
150
|
+
def stop(self) -> None:
|
|
151
|
+
"""Stops the client"""
|
|
152
|
+
|
|
153
|
+
if self._stopped.is_set():
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
logger.info("Stopping telegram client...")
|
|
157
|
+
|
|
158
|
+
self._close()
|
|
159
|
+
self.worker.stop()
|
|
160
|
+
self._stopped.set()
|
|
161
|
+
|
|
162
|
+
# wait for the tdjson listener to stop
|
|
163
|
+
self._td_listener.join()
|
|
164
|
+
|
|
165
|
+
if hasattr(self, "_tdjson"):
|
|
166
|
+
self._tdjson.stop()
|
|
167
|
+
|
|
168
|
+
def _close(self) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Calls `close` tdlib method and waits until authorization_state becomes CLOSED.
|
|
171
|
+
Blocking.
|
|
172
|
+
"""
|
|
173
|
+
self.call_method("close")
|
|
174
|
+
|
|
175
|
+
while self.authorization_state != AuthorizationState.CLOSED:
|
|
176
|
+
result = self.get_authorization_state()
|
|
177
|
+
self.authorization_state = self._wait_authorization_result(result)
|
|
178
|
+
logger.info("Authorization state: %s", self.authorization_state)
|
|
179
|
+
time.sleep(0.5)
|
|
180
|
+
|
|
181
|
+
def parse_text_entities(self, text: str, parse_mode: Literal["HTML", "Markdown"]) -> AsyncResult:
|
|
182
|
+
"""
|
|
183
|
+
Parses text from 'HTML' and 'Markdown' (not MarkdownV2) into plain
|
|
184
|
+
text and internal telegram style description.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
text
|
|
188
|
+
parse_mode
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
AsyncResult
|
|
192
|
+
The update will be:
|
|
193
|
+
{
|
|
194
|
+
'@type': 'formattedText',
|
|
195
|
+
'text': 'Hello world!',
|
|
196
|
+
'entities': [
|
|
197
|
+
{
|
|
198
|
+
'@type': 'textEntity',
|
|
199
|
+
'offset': 0,
|
|
200
|
+
'length': 12,
|
|
201
|
+
'type': {
|
|
202
|
+
'@type': 'textEntityTypeSpoiler'
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
...
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
parse_mode_types = {
|
|
211
|
+
"HTML": "textParseModeHTML",
|
|
212
|
+
"Markdown": "textParseModeMarkdown",
|
|
213
|
+
}
|
|
214
|
+
data = {
|
|
215
|
+
"@type": "parseTextEntities",
|
|
216
|
+
"text": text,
|
|
217
|
+
"parse_mode": {
|
|
218
|
+
"@type": parse_mode_types[parse_mode],
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return self._send_data(data)
|
|
223
|
+
|
|
224
|
+
def send_message(
|
|
225
|
+
self,
|
|
226
|
+
chat_id: int,
|
|
227
|
+
text: Union[str, Element],
|
|
228
|
+
entities: Union[List[dict], None] = None,
|
|
229
|
+
) -> AsyncResult:
|
|
230
|
+
"""
|
|
231
|
+
Sends a message to a chat. The chat must be in the tdlib's database.
|
|
232
|
+
If there is no chat in the DB, tdlib returns an error.
|
|
233
|
+
Chat is being saved to the database when the client receives a message or when you call the `get_chats` method.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
chat_id
|
|
237
|
+
text
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
AsyncResult
|
|
241
|
+
The update will be:
|
|
242
|
+
{
|
|
243
|
+
'@type': 'message',
|
|
244
|
+
'id': 1,
|
|
245
|
+
'sender_user_id': 2,
|
|
246
|
+
'chat_id': 3,
|
|
247
|
+
...
|
|
248
|
+
}
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
if entities is None:
|
|
252
|
+
entities = []
|
|
253
|
+
|
|
254
|
+
updated_text: str
|
|
255
|
+
if isinstance(text, Element):
|
|
256
|
+
result = self.parse_text_entities(text.to_html(), parse_mode="HTML")
|
|
257
|
+
result.wait()
|
|
258
|
+
assert result.update is not None
|
|
259
|
+
update: dict = result.update
|
|
260
|
+
entities = update["entities"]
|
|
261
|
+
updated_text = update["text"]
|
|
262
|
+
else:
|
|
263
|
+
updated_text = text
|
|
264
|
+
|
|
265
|
+
data = {
|
|
266
|
+
"@type": "sendMessage",
|
|
267
|
+
"chat_id": chat_id,
|
|
268
|
+
"input_message_content": {
|
|
269
|
+
"@type": "inputMessageText",
|
|
270
|
+
"text": {
|
|
271
|
+
"@type": "formattedText",
|
|
272
|
+
"text": updated_text,
|
|
273
|
+
"entities": entities,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return self._send_data(data)
|
|
279
|
+
|
|
280
|
+
def import_contacts(self, contacts: List[Dict[str, str]]) -> AsyncResult:
|
|
281
|
+
"""
|
|
282
|
+
Adds new contacts or edits existing contacts by their phone numbers.
|
|
283
|
+
https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1import_contacts.html
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
contacts
|
|
287
|
+
|
|
288
|
+
contacts is a list of the form
|
|
289
|
+
[
|
|
290
|
+
{
|
|
291
|
+
"phone_number": "+380 12 345 67 89",
|
|
292
|
+
"first_name": "Name",
|
|
293
|
+
"last_name": "Surname"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
"phone_number": "+380 09 876 54 32",
|
|
297
|
+
"first_name": "Name",
|
|
298
|
+
"last_name": "Surname"
|
|
299
|
+
},
|
|
300
|
+
...
|
|
301
|
+
]
|
|
302
|
+
phone format is country-specifc
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
AsyncResult
|
|
306
|
+
The update will be:
|
|
307
|
+
{
|
|
308
|
+
'@type': 'importedContacts',
|
|
309
|
+
'user_ids': [1, 2],
|
|
310
|
+
'importer_count': [3, 4],
|
|
311
|
+
...
|
|
312
|
+
}
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
for contact in contacts:
|
|
316
|
+
contact["@type"] = "contact"
|
|
317
|
+
|
|
318
|
+
data = {
|
|
319
|
+
"@type": "importContacts",
|
|
320
|
+
"contacts": contacts,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return self._send_data(data)
|
|
324
|
+
|
|
325
|
+
def get_chat(self, chat_id: int) -> AsyncResult:
|
|
326
|
+
"""
|
|
327
|
+
This is offline request, if there is no chat in your database it will not be found
|
|
328
|
+
tdlib saves chat to the database when it receives a new message or when you call `get_chats` method.
|
|
329
|
+
"""
|
|
330
|
+
data = {"@type": "getChat", "chat_id": chat_id}
|
|
331
|
+
|
|
332
|
+
return self._send_data(data)
|
|
333
|
+
|
|
334
|
+
def get_me(self) -> AsyncResult:
|
|
335
|
+
"""
|
|
336
|
+
Requests information of the current user (getMe method)
|
|
337
|
+
|
|
338
|
+
https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_me.html
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
return self.call_method("getMe")
|
|
342
|
+
|
|
343
|
+
def get_user(self, user_id: int) -> AsyncResult:
|
|
344
|
+
"""
|
|
345
|
+
Requests information about a user with id = user_id.
|
|
346
|
+
|
|
347
|
+
https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user.html
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
return self.call_method("getUser", params={"user_id": user_id})
|
|
351
|
+
|
|
352
|
+
def get_user_full_info(self, user_id: int) -> AsyncResult:
|
|
353
|
+
"""
|
|
354
|
+
Requests the full information about a user with id = user_id.
|
|
355
|
+
|
|
356
|
+
https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1get_user_full_info.html
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
return self.call_method("getUserFullInfo", params={"user_id": user_id})
|
|
360
|
+
|
|
361
|
+
def get_chats(self, offset_order: int = 0, offset_chat_id: int = 0, limit: int = 100) -> AsyncResult:
|
|
362
|
+
"""
|
|
363
|
+
Returns a list of chats:
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
{
|
|
367
|
+
'@type': 'chats',
|
|
368
|
+
'chat_ids': [...],
|
|
369
|
+
'@extra': {
|
|
370
|
+
'request_id': '...'
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
"""
|
|
374
|
+
data = {
|
|
375
|
+
"@type": "getChats",
|
|
376
|
+
"offset_order": offset_order,
|
|
377
|
+
"offset_chat_id": offset_chat_id,
|
|
378
|
+
"limit": limit,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return self._send_data(data)
|
|
382
|
+
|
|
383
|
+
def get_chat_history(
|
|
384
|
+
self,
|
|
385
|
+
chat_id: int,
|
|
386
|
+
limit: int = 100,
|
|
387
|
+
from_message_id: int = 0,
|
|
388
|
+
offset: int = 0,
|
|
389
|
+
only_local: bool = False,
|
|
390
|
+
) -> AsyncResult:
|
|
391
|
+
"""
|
|
392
|
+
Returns history of a chat
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
chat_id
|
|
396
|
+
limit
|
|
397
|
+
from_message_id
|
|
398
|
+
offset
|
|
399
|
+
only_local
|
|
400
|
+
"""
|
|
401
|
+
data = {
|
|
402
|
+
"@type": "getChatHistory",
|
|
403
|
+
"chat_id": chat_id,
|
|
404
|
+
"limit": limit,
|
|
405
|
+
"from_message_id": from_message_id,
|
|
406
|
+
"offset": offset,
|
|
407
|
+
"only_local": only_local,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return self._send_data(data)
|
|
411
|
+
|
|
412
|
+
def get_message(
|
|
413
|
+
self,
|
|
414
|
+
chat_id: int,
|
|
415
|
+
message_id: int,
|
|
416
|
+
) -> AsyncResult:
|
|
417
|
+
"""
|
|
418
|
+
Return a message via its message_id
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
chat_id
|
|
422
|
+
message_id
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
AsyncResult
|
|
426
|
+
The update will be:
|
|
427
|
+
{
|
|
428
|
+
'@type': 'message',
|
|
429
|
+
'id': 1,
|
|
430
|
+
'sender_user_id': 2,
|
|
431
|
+
'chat_id': 3,
|
|
432
|
+
'content': {...},
|
|
433
|
+
...
|
|
434
|
+
}
|
|
435
|
+
"""
|
|
436
|
+
data = {
|
|
437
|
+
"@type": "getMessage",
|
|
438
|
+
"chat_id": chat_id,
|
|
439
|
+
"message_id": message_id,
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return self._send_data(data)
|
|
443
|
+
|
|
444
|
+
def delete_messages(self, chat_id: int, message_ids: List[int], revoke: bool = True) -> AsyncResult:
|
|
445
|
+
"""
|
|
446
|
+
Delete a list of messages in a chat
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
chat_id
|
|
450
|
+
message_ids
|
|
451
|
+
revoke
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
return self._send_data(
|
|
455
|
+
{
|
|
456
|
+
"@type": "deleteMessages",
|
|
457
|
+
"chat_id": chat_id,
|
|
458
|
+
"message_ids": message_ids,
|
|
459
|
+
"revoke": revoke,
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
def get_supergroup_full_info(self, supergroup_id: int) -> AsyncResult:
|
|
464
|
+
"""
|
|
465
|
+
Get the full info of a supergroup
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
supergroup_id
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
return self._send_data({"@type": "getSupergroupFullInfo", "supergroup_id": supergroup_id})
|
|
472
|
+
|
|
473
|
+
def create_basic_group_chat(self, basic_group_id: int) -> AsyncResult:
|
|
474
|
+
"""
|
|
475
|
+
Create a chat from a basic group
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
basic_group_id
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
return self._send_data({"@type": "createBasicGroupChat", "basic_group_id": basic_group_id})
|
|
482
|
+
|
|
483
|
+
def get_web_page_instant_view(self, url: str, force_full: bool = False) -> AsyncResult:
|
|
484
|
+
"""
|
|
485
|
+
Use this method to request instant preview of a webpage.
|
|
486
|
+
Returns error with 404 if there is no preview for this webpage.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
url: URL of a webpage
|
|
490
|
+
force_full: If true, the full instant view for the web page will be returned
|
|
491
|
+
"""
|
|
492
|
+
data = {"@type": "getWebPageInstantView", "url": url, "force_full": force_full}
|
|
493
|
+
|
|
494
|
+
return self._send_data(data)
|
|
495
|
+
|
|
496
|
+
def call_method(
|
|
497
|
+
self,
|
|
498
|
+
method_name: str,
|
|
499
|
+
params: Optional[Dict[str, Any]] = None,
|
|
500
|
+
block: bool = False,
|
|
501
|
+
) -> AsyncResult:
|
|
502
|
+
"""
|
|
503
|
+
Use this method to call any other method of the tdlib
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
method_name: Name of the method
|
|
507
|
+
params: parameters
|
|
508
|
+
"""
|
|
509
|
+
data = {"@type": method_name}
|
|
510
|
+
|
|
511
|
+
if params:
|
|
512
|
+
data.update(params)
|
|
513
|
+
|
|
514
|
+
return self._send_data(data, block=block)
|
|
515
|
+
|
|
516
|
+
def _run(self) -> None:
|
|
517
|
+
self._td_listener = threading.Thread(target=self._listen_to_td)
|
|
518
|
+
self._td_listener.daemon = True
|
|
519
|
+
self._td_listener.start()
|
|
520
|
+
|
|
521
|
+
self.worker.run()
|
|
522
|
+
|
|
523
|
+
def _listen_to_td(self) -> None:
|
|
524
|
+
logger.info("[Telegram.td_listener] started")
|
|
525
|
+
|
|
526
|
+
while not self._stopped.is_set():
|
|
527
|
+
update = self._tdjson.receive()
|
|
528
|
+
|
|
529
|
+
if update:
|
|
530
|
+
self._update_async_result(update)
|
|
531
|
+
self._run_handlers(update)
|
|
532
|
+
|
|
533
|
+
def _update_async_result(self, update: Dict[Any, Any]) -> typing.Optional[AsyncResult]:
|
|
534
|
+
async_result = None
|
|
535
|
+
|
|
536
|
+
_special_types = ("updateAuthorizationState",) # for authorizationProcess @extra.request_id doesn't work
|
|
537
|
+
|
|
538
|
+
if update.get("@type") in _special_types:
|
|
539
|
+
request_id = update["@type"]
|
|
540
|
+
else:
|
|
541
|
+
request_id = update.get("@extra", {}).get("request_id")
|
|
542
|
+
|
|
543
|
+
if not request_id:
|
|
544
|
+
logger.debug("request_id has not been found in the update")
|
|
545
|
+
else:
|
|
546
|
+
async_result = self._results.get(request_id)
|
|
547
|
+
|
|
548
|
+
if not async_result:
|
|
549
|
+
logger.debug("async_result has not been found in by request_id=%s", request_id)
|
|
550
|
+
else:
|
|
551
|
+
done = async_result.parse_update(update)
|
|
552
|
+
|
|
553
|
+
if done:
|
|
554
|
+
self._results.pop(request_id, None)
|
|
555
|
+
|
|
556
|
+
return async_result
|
|
557
|
+
|
|
558
|
+
def _run_handlers(self, update: Dict[Any, Any]) -> None:
|
|
559
|
+
update_type: str = update.get("@type", "unknown")
|
|
560
|
+
|
|
561
|
+
for handler in self._update_handlers[update_type]:
|
|
562
|
+
self._workers_queue.put((handler, update), timeout=self._queue_put_timeout)
|
|
563
|
+
|
|
564
|
+
def remove_update_handler(self, handler_type: str, func: Callable) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Remove a handler with the specified type
|
|
567
|
+
"""
|
|
568
|
+
try:
|
|
569
|
+
self._update_handlers[handler_type].remove(func)
|
|
570
|
+
except (ValueError, KeyError):
|
|
571
|
+
# not in the list
|
|
572
|
+
pass
|
|
573
|
+
|
|
574
|
+
def add_message_handler(self, func: Callable) -> None:
|
|
575
|
+
self.add_update_handler(MESSAGE_HANDLER_TYPE, func)
|
|
576
|
+
|
|
577
|
+
def add_update_handler(self, handler_type: str, func: Callable) -> None:
|
|
578
|
+
if func not in self._update_handlers[handler_type]:
|
|
579
|
+
self._update_handlers[handler_type].append(func)
|
|
580
|
+
|
|
581
|
+
def _send_data(
|
|
582
|
+
self,
|
|
583
|
+
data: Dict[Any, Any],
|
|
584
|
+
result_id: Optional[str] = None,
|
|
585
|
+
block: bool = False,
|
|
586
|
+
) -> AsyncResult:
|
|
587
|
+
"""
|
|
588
|
+
Sends data to tdlib.
|
|
589
|
+
|
|
590
|
+
If `block`is True, waits for the result
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
if "@extra" not in data:
|
|
594
|
+
data["@extra"] = {}
|
|
595
|
+
|
|
596
|
+
if not result_id and "request_id" in data["@extra"]:
|
|
597
|
+
result_id = data["@extra"]["request_id"]
|
|
598
|
+
|
|
599
|
+
async_result = AsyncResult(client=self, result_id=result_id)
|
|
600
|
+
data["@extra"]["request_id"] = async_result.id
|
|
601
|
+
self._results[async_result.id] = async_result
|
|
602
|
+
self._tdjson.send(data)
|
|
603
|
+
async_result.request = data
|
|
604
|
+
|
|
605
|
+
if block:
|
|
606
|
+
async_result.wait(raise_exc=True)
|
|
607
|
+
|
|
608
|
+
return async_result
|
|
609
|
+
|
|
610
|
+
def idle(
|
|
611
|
+
self,
|
|
612
|
+
stop_signals: Tuple = (
|
|
613
|
+
signal.SIGINT,
|
|
614
|
+
signal.SIGTERM,
|
|
615
|
+
signal.SIGABRT,
|
|
616
|
+
),
|
|
617
|
+
) -> None:
|
|
618
|
+
"""
|
|
619
|
+
Blocks until one of the exit signals is received.
|
|
620
|
+
When a signal is received, calls `stop`.
|
|
621
|
+
"""
|
|
622
|
+
|
|
623
|
+
for sig in stop_signals:
|
|
624
|
+
signal.signal(sig, self._stop_signal_handler)
|
|
625
|
+
|
|
626
|
+
self._stopped.wait()
|
|
627
|
+
|
|
628
|
+
def _stop_signal_handler(self, signum: int, frame: Optional[FrameType] = None) -> None:
|
|
629
|
+
logger.info("Signal %s received!", signum)
|
|
630
|
+
self.stop()
|
|
631
|
+
|
|
632
|
+
def get_authorization_state(self) -> AsyncResult:
|
|
633
|
+
logger.debug("Getting authorization state")
|
|
634
|
+
data = {"@type": "getAuthorizationState"}
|
|
635
|
+
|
|
636
|
+
return self._send_data(data, result_id="getAuthorizationState")
|
|
637
|
+
|
|
638
|
+
def _wait_authorization_result(self, result: AsyncResult) -> AuthorizationState:
|
|
639
|
+
authorization_state = None
|
|
640
|
+
|
|
641
|
+
if result:
|
|
642
|
+
result.wait(raise_exc=True)
|
|
643
|
+
|
|
644
|
+
if result.update is None:
|
|
645
|
+
raise RuntimeError("Something wrong, the result update is None")
|
|
646
|
+
|
|
647
|
+
if result.id == "getAuthorizationState":
|
|
648
|
+
authorization_state = result.update["@type"]
|
|
649
|
+
else:
|
|
650
|
+
authorization_state = result.update["authorization_state"]["@type"]
|
|
651
|
+
|
|
652
|
+
return AuthorizationState(authorization_state)
|
|
653
|
+
|
|
654
|
+
def login(self, blocking: bool = True) -> AuthorizationState:
|
|
655
|
+
"""
|
|
656
|
+
Login process.
|
|
657
|
+
|
|
658
|
+
Must be called before any other call.
|
|
659
|
+
It sends initial params to the tdlib, sets database encryption key, etc.
|
|
660
|
+
|
|
661
|
+
args:
|
|
662
|
+
blocking [bool]: If True, the process is blocking and the client
|
|
663
|
+
expects password and code from stdin.
|
|
664
|
+
If False, `login` call returns next AuthorizationState and
|
|
665
|
+
the login process can be continued (with calling login(blocking=False) again)
|
|
666
|
+
after the necessary action is completed.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
- AuthorizationState.WAIT_CODE if a telegram code is required.
|
|
670
|
+
The caller should ask the telegram code
|
|
671
|
+
to the end user then call send_code(code)
|
|
672
|
+
- AuthorizationState.WAIT_PASSWORD if a telegram password is required.
|
|
673
|
+
The caller should ask the telegram password
|
|
674
|
+
to the end user and then call send_password(password)
|
|
675
|
+
- AuthorizationState.WAIT_REGISTRATION if a the user must finish registration
|
|
676
|
+
The caller should ask the first and last names
|
|
677
|
+
to the end user and then call register_user(first, last)
|
|
678
|
+
- AuthorizationState.READY if the login process succeeded.
|
|
679
|
+
"""
|
|
680
|
+
|
|
681
|
+
if self.proxy_server:
|
|
682
|
+
self._send_add_proxy()
|
|
683
|
+
|
|
684
|
+
actions: Dict[AuthorizationState, Callable[[], AsyncResult]] = {
|
|
685
|
+
AuthorizationState.NONE: self.get_authorization_state,
|
|
686
|
+
AuthorizationState.WAIT_TDLIB_PARAMETERS: self._set_initial_params,
|
|
687
|
+
AuthorizationState.WAIT_ENCRYPTION_KEY: self._send_encryption_key,
|
|
688
|
+
AuthorizationState.WAIT_PHONE_NUMBER: self._send_phone_number_or_bot_token,
|
|
689
|
+
AuthorizationState.WAIT_CODE: self._send_telegram_code,
|
|
690
|
+
AuthorizationState.WAIT_PASSWORD: self._send_password,
|
|
691
|
+
AuthorizationState.WAIT_REGISTRATION: self._register_user,
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
blocking_actions = (
|
|
695
|
+
AuthorizationState.WAIT_CODE,
|
|
696
|
+
AuthorizationState.WAIT_PASSWORD,
|
|
697
|
+
AuthorizationState.WAIT_REGISTRATION,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if self.phone:
|
|
701
|
+
logger.info("[login] Login process has been started with phone")
|
|
702
|
+
else:
|
|
703
|
+
logger.info("[login] Login process has been started with bot token")
|
|
704
|
+
|
|
705
|
+
while self.authorization_state != AuthorizationState.READY:
|
|
706
|
+
logger.info("[login] current authorization state: %s", self.authorization_state)
|
|
707
|
+
|
|
708
|
+
if not blocking and self.authorization_state in blocking_actions:
|
|
709
|
+
return self.authorization_state
|
|
710
|
+
|
|
711
|
+
result = actions[self.authorization_state]()
|
|
712
|
+
|
|
713
|
+
if not isinstance(result, AuthorizationState):
|
|
714
|
+
self.authorization_state = self._wait_authorization_result(result)
|
|
715
|
+
else:
|
|
716
|
+
self.authorization_state = result
|
|
717
|
+
|
|
718
|
+
return self.authorization_state
|
|
719
|
+
|
|
720
|
+
def _set_initial_params(self) -> AsyncResult:
|
|
721
|
+
logger.info(
|
|
722
|
+
"Setting tdlib initial params: files_dir=%s, test_dc=%s",
|
|
723
|
+
self.files_directory,
|
|
724
|
+
self.use_test_dc,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
parameters = {
|
|
728
|
+
"use_test_dc": self.use_test_dc,
|
|
729
|
+
"api_id": self.api_id,
|
|
730
|
+
"api_hash": self.api_hash,
|
|
731
|
+
"device_model": self.device_model,
|
|
732
|
+
"system_version": self.system_version,
|
|
733
|
+
"application_version": self.application_version,
|
|
734
|
+
"system_language_code": self.system_language_code,
|
|
735
|
+
"database_directory": str(self.files_directory / "database"),
|
|
736
|
+
"use_message_database": self.use_message_database,
|
|
737
|
+
"files_directory": str(self.files_directory / "files"),
|
|
738
|
+
"use_secret_chats": self.use_secret_chats,
|
|
739
|
+
}
|
|
740
|
+
data: Dict[str, typing.Any] = {
|
|
741
|
+
"@type": "setTdlibParameters",
|
|
742
|
+
"parameters": parameters,
|
|
743
|
+
# since tdlib 1.8.6
|
|
744
|
+
"database_encryption_key": self._database_encryption_key,
|
|
745
|
+
**parameters,
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
749
|
+
|
|
750
|
+
def _send_encryption_key(self) -> AsyncResult:
|
|
751
|
+
logger.info("Sending encryption key")
|
|
752
|
+
|
|
753
|
+
data = {
|
|
754
|
+
"@type": "checkDatabaseEncryptionKey",
|
|
755
|
+
"encryption_key": self._database_encryption_key,
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
759
|
+
|
|
760
|
+
def _send_phone_number_or_bot_token(self) -> AsyncResult:
|
|
761
|
+
"""Sends phone number or a bot_token"""
|
|
762
|
+
|
|
763
|
+
if self.phone:
|
|
764
|
+
return self._send_phone_number()
|
|
765
|
+
elif self.bot_token:
|
|
766
|
+
return self._send_bot_token()
|
|
767
|
+
else:
|
|
768
|
+
raise RuntimeError("Unknown mode: both bot_token and phone are None")
|
|
769
|
+
|
|
770
|
+
def _send_phone_number(self) -> AsyncResult:
|
|
771
|
+
logger.info("Sending phone number")
|
|
772
|
+
data = {
|
|
773
|
+
"@type": "setAuthenticationPhoneNumber",
|
|
774
|
+
"phone_number": self.phone,
|
|
775
|
+
"allow_flash_call": False,
|
|
776
|
+
"is_current_phone_number": True,
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
780
|
+
|
|
781
|
+
def _send_add_proxy(self) -> AsyncResult:
|
|
782
|
+
logger.info("Sending addProxy")
|
|
783
|
+
data = {
|
|
784
|
+
"@type": "addProxy",
|
|
785
|
+
"server": self.proxy_server,
|
|
786
|
+
"port": self.proxy_port,
|
|
787
|
+
"enable": True,
|
|
788
|
+
"type": self.proxy_type,
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return self._send_data(data, result_id="setProxy")
|
|
792
|
+
|
|
793
|
+
def _send_bot_token(self) -> AsyncResult:
|
|
794
|
+
logger.info("Sending bot token")
|
|
795
|
+
data = {"@type": "checkAuthenticationBotToken", "token": self.bot_token}
|
|
796
|
+
|
|
797
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
798
|
+
|
|
799
|
+
def _send_telegram_code(self, code: Optional[str] = None) -> AsyncResult:
|
|
800
|
+
logger.info("Sending code")
|
|
801
|
+
|
|
802
|
+
if code is None:
|
|
803
|
+
code = input("Enter code:")
|
|
804
|
+
data = {"@type": "checkAuthenticationCode", "code": str(code)}
|
|
805
|
+
|
|
806
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
807
|
+
|
|
808
|
+
def send_code(self, code: str) -> AuthorizationState:
|
|
809
|
+
"""
|
|
810
|
+
Verifies a telegram code and continues the authorization process
|
|
811
|
+
|
|
812
|
+
Args:
|
|
813
|
+
code: the code to be verified. If code is None, it will be asked to the user using the input() function
|
|
814
|
+
|
|
815
|
+
Returns
|
|
816
|
+
- AuthorizationState. The called have to call `login` to continue the login process.
|
|
817
|
+
|
|
818
|
+
Raises:
|
|
819
|
+
- RuntimeError if the login failed
|
|
820
|
+
"""
|
|
821
|
+
result = self._send_telegram_code(code)
|
|
822
|
+
self.authorization_state = self._wait_authorization_result(result)
|
|
823
|
+
|
|
824
|
+
return self.authorization_state
|
|
825
|
+
|
|
826
|
+
def _send_password(self, password: Optional[str] = None) -> AsyncResult:
|
|
827
|
+
logger.info("Sending password")
|
|
828
|
+
|
|
829
|
+
if password is None:
|
|
830
|
+
password = getpass.getpass("Password:")
|
|
831
|
+
data = {"@type": "checkAuthenticationPassword", "password": password}
|
|
832
|
+
|
|
833
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
834
|
+
|
|
835
|
+
def send_password(self, password: str) -> AuthorizationState:
|
|
836
|
+
"""
|
|
837
|
+
Verifies a telegram password and continues the authorization process
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
password the password to be verified.
|
|
841
|
+
If password is None, it will be asked to the user using the getpass.getpass() function
|
|
842
|
+
|
|
843
|
+
Returns
|
|
844
|
+
- AuthorizationState. The called have to call `login` to continue the login process.
|
|
845
|
+
|
|
846
|
+
Raises:
|
|
847
|
+
- RuntimeError if the login failed
|
|
848
|
+
|
|
849
|
+
"""
|
|
850
|
+
result = self._send_password(password)
|
|
851
|
+
self.authorization_state = self._wait_authorization_result(result)
|
|
852
|
+
|
|
853
|
+
return self.authorization_state
|
|
854
|
+
|
|
855
|
+
def _register_user(self, first: Optional[str] = None, last: Optional[str] = None) -> AsyncResult:
|
|
856
|
+
logger.info("Registering user")
|
|
857
|
+
|
|
858
|
+
if first is None:
|
|
859
|
+
first = input("Enter first name: ")
|
|
860
|
+
|
|
861
|
+
if last is None:
|
|
862
|
+
last = input("Enter last name: ")
|
|
863
|
+
|
|
864
|
+
data = {
|
|
865
|
+
"@type": "registerUser",
|
|
866
|
+
"first_name": first,
|
|
867
|
+
"last_name": last,
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
return self._send_data(data, result_id="updateAuthorizationState")
|
|
871
|
+
|
|
872
|
+
def register_user(self, first: str, last: str) -> AuthorizationState:
|
|
873
|
+
"""
|
|
874
|
+
Finishes the new user registration process
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
first the user's first name
|
|
878
|
+
last the user's last name
|
|
879
|
+
If either argument is None, it will be asked to the user using the input() function
|
|
880
|
+
|
|
881
|
+
Returns
|
|
882
|
+
- AuthorizationState. The called have to call `login` to continue the login process.
|
|
883
|
+
|
|
884
|
+
Raises:
|
|
885
|
+
- RuntimeError if the login failed
|
|
886
|
+
|
|
887
|
+
"""
|
|
888
|
+
result = self._register_user(first, last)
|
|
889
|
+
self.authorization_state = self._wait_authorization_result(result)
|
|
890
|
+
|
|
891
|
+
return self.authorization_state
|
|
Binary file
|
|
Binary file
|
telegram/py.typed
ADDED
|
File without changes
|
telegram/tdjson.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import platform
|
|
4
|
+
import ctypes.util
|
|
5
|
+
from ctypes import CDLL, CFUNCTYPE, c_int, c_char_p, c_double, c_void_p, c_longlong
|
|
6
|
+
from typing import Any, Dict, Optional, Union
|
|
7
|
+
import importlib.resources
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_tdjson_lib_path() -> str:
|
|
13
|
+
system_library = ctypes.util.find_library("tdjson")
|
|
14
|
+
|
|
15
|
+
if system_library is not None:
|
|
16
|
+
return system_library
|
|
17
|
+
|
|
18
|
+
if platform.system().lower() == "darwin":
|
|
19
|
+
lib_name = "darwin/libtdjson.dylib"
|
|
20
|
+
else:
|
|
21
|
+
lib_name = "linux/libtdjson.so"
|
|
22
|
+
|
|
23
|
+
return str(importlib.resources.files("telegram").joinpath(f"lib/{lib_name}"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TDJson:
|
|
27
|
+
def __init__(self, library_path: Optional[str] = None, verbosity: int = 2) -> None:
|
|
28
|
+
if library_path is None:
|
|
29
|
+
library_path = _get_tdjson_lib_path()
|
|
30
|
+
logger.info('Using shared library "%s"', library_path)
|
|
31
|
+
|
|
32
|
+
self._build_client(library_path, verbosity)
|
|
33
|
+
|
|
34
|
+
def __del__(self) -> None:
|
|
35
|
+
if hasattr(self, "_tdjson") and hasattr(self._tdjson, "_td_json_client_destroy"):
|
|
36
|
+
self.stop()
|
|
37
|
+
|
|
38
|
+
def _build_client(self, library_path: str, verbosity: int) -> None:
|
|
39
|
+
self._tdjson = CDLL(library_path)
|
|
40
|
+
|
|
41
|
+
# load TDLib functions from shared library
|
|
42
|
+
self._td_json_client_create = self._tdjson.td_json_client_create
|
|
43
|
+
self._td_json_client_create.restype = c_void_p
|
|
44
|
+
self._td_json_client_create.argtypes = []
|
|
45
|
+
|
|
46
|
+
self.td_json_client = self._td_json_client_create()
|
|
47
|
+
|
|
48
|
+
self._td_json_client_receive = self._tdjson.td_json_client_receive
|
|
49
|
+
self._td_json_client_receive.restype = c_char_p
|
|
50
|
+
self._td_json_client_receive.argtypes = [c_void_p, c_double]
|
|
51
|
+
|
|
52
|
+
self._td_json_client_send = self._tdjson.td_json_client_send
|
|
53
|
+
self._td_json_client_send.restype = None
|
|
54
|
+
self._td_json_client_send.argtypes = [c_void_p, c_char_p]
|
|
55
|
+
|
|
56
|
+
self._td_json_client_execute = self._tdjson.td_json_client_execute
|
|
57
|
+
self._td_json_client_execute.restype = c_char_p
|
|
58
|
+
self._td_json_client_execute.argtypes = [c_void_p, c_char_p]
|
|
59
|
+
|
|
60
|
+
self._td_json_client_destroy = self._tdjson.td_json_client_destroy
|
|
61
|
+
self._td_json_client_destroy.restype = None
|
|
62
|
+
self._td_json_client_destroy.argtypes = [c_void_p]
|
|
63
|
+
|
|
64
|
+
self._td_set_log_file_path = self._tdjson.td_set_log_file_path
|
|
65
|
+
self._td_set_log_file_path.restype = c_int
|
|
66
|
+
self._td_set_log_file_path.argtypes = [c_char_p]
|
|
67
|
+
|
|
68
|
+
self._td_set_log_max_file_size = self._tdjson.td_set_log_max_file_size
|
|
69
|
+
self._td_set_log_max_file_size.restype = None
|
|
70
|
+
self._td_set_log_max_file_size.argtypes = [c_longlong]
|
|
71
|
+
|
|
72
|
+
self._td_set_log_verbosity_level = self._tdjson.td_set_log_verbosity_level
|
|
73
|
+
self._td_set_log_verbosity_level.restype = None
|
|
74
|
+
self._td_set_log_verbosity_level.argtypes = [c_int]
|
|
75
|
+
|
|
76
|
+
self._td_set_log_verbosity_level(verbosity)
|
|
77
|
+
|
|
78
|
+
fatal_error_callback_type = CFUNCTYPE(None, c_char_p)
|
|
79
|
+
|
|
80
|
+
self._td_set_log_fatal_error_callback = self._tdjson.td_set_log_fatal_error_callback
|
|
81
|
+
self._td_set_log_fatal_error_callback.restype = None
|
|
82
|
+
self._td_set_log_fatal_error_callback.argtypes = [fatal_error_callback_type]
|
|
83
|
+
|
|
84
|
+
# initialize TDLib log with desired parameters
|
|
85
|
+
def on_fatal_error_callback(error_message: str) -> None:
|
|
86
|
+
logger.error("TDLib fatal error: %s", error_message)
|
|
87
|
+
|
|
88
|
+
c_on_fatal_error_callback = fatal_error_callback_type(on_fatal_error_callback)
|
|
89
|
+
self._td_set_log_fatal_error_callback(c_on_fatal_error_callback)
|
|
90
|
+
|
|
91
|
+
def send(self, query: Dict[Any, Any]) -> None:
|
|
92
|
+
dumped_query = json.dumps(query).encode("utf-8")
|
|
93
|
+
self._td_json_client_send(self.td_json_client, dumped_query)
|
|
94
|
+
logger.debug("[me ==>] Sent %s", dumped_query)
|
|
95
|
+
|
|
96
|
+
def receive(self) -> Union[None, Dict[Any, Any]]:
|
|
97
|
+
result_str = self._td_json_client_receive(self.td_json_client, 1.0)
|
|
98
|
+
|
|
99
|
+
if result_str:
|
|
100
|
+
result: Dict[Any, Any] = json.loads(result_str.decode("utf-8"))
|
|
101
|
+
logger.debug("[me <==] Received %s", result)
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def td_execute(self, query: Dict[Any, Any]) -> Union[Dict[Any, Any], Any]:
|
|
108
|
+
dumped_query = json.dumps(query).encode("utf-8")
|
|
109
|
+
result_str = self._td_json_client_execute(self.td_json_client, dumped_query)
|
|
110
|
+
|
|
111
|
+
if result_str:
|
|
112
|
+
result: Dict[Any, Any] = json.loads(result_str.decode("utf-8"))
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def stop(self) -> None:
|
|
119
|
+
self._td_json_client_destroy(self.td_json_client)
|
telegram/text.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Since telegram.text is based on a third-party module telegram-text,
|
|
2
|
+
you can find more examples of how to use markup components on
|
|
3
|
+
telegram-text.alinsky.tech or github.com/SKY-ALIN/telegram-text
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from telegram_text import (
|
|
7
|
+
Bold,
|
|
8
|
+
Chain,
|
|
9
|
+
Code,
|
|
10
|
+
Hashtag,
|
|
11
|
+
InlineCode,
|
|
12
|
+
InlineUser,
|
|
13
|
+
Italic,
|
|
14
|
+
Link,
|
|
15
|
+
OrderedList,
|
|
16
|
+
PlainText,
|
|
17
|
+
Spoiler,
|
|
18
|
+
Strikethrough,
|
|
19
|
+
TOMLSection,
|
|
20
|
+
Text,
|
|
21
|
+
Underline,
|
|
22
|
+
UnorderedList,
|
|
23
|
+
User,
|
|
24
|
+
)
|
|
25
|
+
from telegram_text.bases import Element
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Bold",
|
|
29
|
+
"Chain",
|
|
30
|
+
"Code",
|
|
31
|
+
"Element",
|
|
32
|
+
"Hashtag",
|
|
33
|
+
"InlineCode",
|
|
34
|
+
"InlineUser",
|
|
35
|
+
"Italic",
|
|
36
|
+
"Link",
|
|
37
|
+
"OrderedList",
|
|
38
|
+
"PlainText",
|
|
39
|
+
"Spoiler",
|
|
40
|
+
"Strikethrough",
|
|
41
|
+
"TOMLSection",
|
|
42
|
+
"Text",
|
|
43
|
+
"Underline",
|
|
44
|
+
"UnorderedList",
|
|
45
|
+
"User",
|
|
46
|
+
]
|
telegram/utils.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import threading
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from telegram.client import Telegram
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncResult:
|
|
14
|
+
"""
|
|
15
|
+
tdlib is asynchronous, and this class helps you get results back.
|
|
16
|
+
After each API call, you receive AsyncResult object, which you can use to get results back.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, client: "Telegram", result_id: Optional[str] = None) -> None:
|
|
20
|
+
self.client = client
|
|
21
|
+
|
|
22
|
+
if result_id:
|
|
23
|
+
self.id = result_id
|
|
24
|
+
else:
|
|
25
|
+
self.id = uuid.uuid4().hex
|
|
26
|
+
|
|
27
|
+
self.request: Optional[Dict[Any, Any]] = None
|
|
28
|
+
self.ok_received = False
|
|
29
|
+
self.error = False
|
|
30
|
+
self.error_info: Optional[Dict[Any, Any]] = None
|
|
31
|
+
self.update: Optional[Dict[Any, Any]] = None
|
|
32
|
+
self._ready = threading.Event()
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return f"AsyncResult <{self.id}>"
|
|
36
|
+
|
|
37
|
+
def wait(self, timeout: Optional[int] = None, raise_exc: bool = False) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Blocking method to wait for the result
|
|
40
|
+
"""
|
|
41
|
+
result = self._ready.wait(timeout=timeout)
|
|
42
|
+
if result is False:
|
|
43
|
+
raise TimeoutError()
|
|
44
|
+
if raise_exc and self.error:
|
|
45
|
+
raise RuntimeError(f"Telegram error: {self.error_info}")
|
|
46
|
+
|
|
47
|
+
def parse_update(self, update: Dict[Any, Any]) -> bool:
|
|
48
|
+
update_type = update.get("@type")
|
|
49
|
+
|
|
50
|
+
logger.debug("update id=%s type=%s received", self.id, update_type)
|
|
51
|
+
|
|
52
|
+
if update_type == "ok":
|
|
53
|
+
self.ok_received = True
|
|
54
|
+
if self.id == "updateAuthorizationState":
|
|
55
|
+
# For updateAuthorizationState commands tdlib sends
|
|
56
|
+
# @type: ok responses
|
|
57
|
+
# but we want to wait longer to receive the new authorization state
|
|
58
|
+
return False
|
|
59
|
+
elif update_type == "error":
|
|
60
|
+
self.error = True
|
|
61
|
+
self.error_info = update
|
|
62
|
+
else:
|
|
63
|
+
self.update = update
|
|
64
|
+
|
|
65
|
+
self._ready.set()
|
|
66
|
+
|
|
67
|
+
return True
|
telegram/worker.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from queue import Queue, Empty
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseWorker:
|
|
10
|
+
"""
|
|
11
|
+
Base worker class.
|
|
12
|
+
Each worker must implement the run method to start listening to the queue
|
|
13
|
+
and calling handler functions
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, queue: Queue):
|
|
17
|
+
self._is_enabled = True
|
|
18
|
+
self._queue = queue
|
|
19
|
+
|
|
20
|
+
def run(self) -> None:
|
|
21
|
+
raise NotImplementedError()
|
|
22
|
+
|
|
23
|
+
def stop(self) -> None:
|
|
24
|
+
raise NotImplementedError()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimpleWorker(BaseWorker):
|
|
28
|
+
"""Simple one-thread worker"""
|
|
29
|
+
|
|
30
|
+
def run(self) -> None:
|
|
31
|
+
self._thread = threading.Thread(target=self._run_thread)
|
|
32
|
+
self._thread.daemon = True
|
|
33
|
+
self._thread.start()
|
|
34
|
+
|
|
35
|
+
def _run_thread(self) -> None:
|
|
36
|
+
logger.info("[SimpleWorker] started")
|
|
37
|
+
|
|
38
|
+
while self._is_enabled:
|
|
39
|
+
try:
|
|
40
|
+
handler, update = self._queue.get(timeout=0.5)
|
|
41
|
+
except Empty:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
handler(update)
|
|
45
|
+
self._queue.task_done()
|
|
46
|
+
|
|
47
|
+
def stop(self) -> None:
|
|
48
|
+
self._is_enabled = False
|
|
49
|
+
self._thread.join()
|