python-telegram 2.0.0__py3-none-macosx_11_0_arm64.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-2.0.0.dist-info/METADATA +156 -0
- python_telegram-2.0.0.dist-info/RECORD +12 -0
- python_telegram-2.0.0.dist-info/WHEEL +4 -0
- python_telegram-2.0.0.dist-info/licenses/LICENSE +21 -0
- telegram/__init__.py +3 -0
- telegram/client.py +1101 -0
- telegram/lib/libtdjson.dylib +0 -0
- telegram/py.typed +0 -0
- telegram/tdjson.py +185 -0
- telegram/text.py +46 -0
- telegram/utils.py +69 -0
- telegram/worker.py +51 -0
|
Binary file
|
telegram/py.typed
ADDED
|
File without changes
|
telegram/tdjson.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes.util
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from ctypes import CDLL, CFUNCTYPE, c_char_p, c_double, c_int, c_longlong, c_void_p
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
TDLIB_PATH_ENV_VAR = "PYTHON_TELEGRAM_TDLIB_PATH"
|
|
16
|
+
|
|
17
|
+
_OVERRIDES_HINT = (
|
|
18
|
+
"Only the linux x86_64, linux aarch64, macOS arm64 and macOS x86_64 wheels bundle a "
|
|
19
|
+
"libtdjson; the sdist never does. Install tdlib system-wide so ctypes can find it, or "
|
|
20
|
+
f"point python-telegram at your own build with the {TDLIB_PATH_ENV_VAR} environment "
|
|
21
|
+
"variable or the TDJson(library_path=...) argument."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClientDestroyedError(RuntimeError):
|
|
26
|
+
"""Raised when a TDJson client is used after it has been stopped"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TDLibNotFoundError(OSError):
|
|
30
|
+
"""
|
|
31
|
+
Raised when no libtdjson can be found or loaded.
|
|
32
|
+
|
|
33
|
+
Subclasses OSError because that is what ctypes.CDLL raises, so callers that
|
|
34
|
+
already handle a failed load keep working.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _bundled_lib_path() -> Path:
|
|
39
|
+
name = "libtdjson.dylib" if platform.system().lower() == "darwin" else "libtdjson.so"
|
|
40
|
+
|
|
41
|
+
return Path(str(importlib.resources.files("telegram").joinpath(f"lib/{name}")))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_tdjson_library() -> tuple[str, str]:
|
|
45
|
+
"""Returns the path of the libtdjson to load and the name of the source it came from."""
|
|
46
|
+
env_path = os.environ.get(TDLIB_PATH_ENV_VAR)
|
|
47
|
+
|
|
48
|
+
if env_path:
|
|
49
|
+
return env_path, "env"
|
|
50
|
+
|
|
51
|
+
system_library = ctypes.util.find_library("tdjson")
|
|
52
|
+
|
|
53
|
+
if system_library is not None:
|
|
54
|
+
return system_library, "system"
|
|
55
|
+
|
|
56
|
+
bundled = _bundled_lib_path()
|
|
57
|
+
|
|
58
|
+
if bundled.is_file():
|
|
59
|
+
return str(bundled), "bundled"
|
|
60
|
+
|
|
61
|
+
raise TDLibNotFoundError(
|
|
62
|
+
f"No libtdjson found for {platform.system()} {platform.machine()}. "
|
|
63
|
+
f"{TDLIB_PATH_ENV_VAR} is not set, ctypes.util.find_library('tdjson') found nothing, "
|
|
64
|
+
f"and there is no bundled binary at {bundled}. {_OVERRIDES_HINT}"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TDJson:
|
|
69
|
+
def __init__(self, library_path: str | None = None, verbosity: int = 2) -> None:
|
|
70
|
+
if library_path is None:
|
|
71
|
+
library_path, source = _resolve_tdjson_library()
|
|
72
|
+
else:
|
|
73
|
+
source = "library_path"
|
|
74
|
+
|
|
75
|
+
logger.info('Using shared library "%s" (found via: %s)', library_path, source)
|
|
76
|
+
|
|
77
|
+
self._build_client(library_path, verbosity)
|
|
78
|
+
|
|
79
|
+
def __del__(self) -> None:
|
|
80
|
+
if hasattr(self, "_td_json_client_destroy"):
|
|
81
|
+
self.stop()
|
|
82
|
+
|
|
83
|
+
def _build_client(self, library_path: str, verbosity: int) -> None:
|
|
84
|
+
try:
|
|
85
|
+
self._tdjson = CDLL(library_path)
|
|
86
|
+
except OSError as error:
|
|
87
|
+
raise TDLibNotFoundError(
|
|
88
|
+
f"Failed to load libtdjson from {library_path} on "
|
|
89
|
+
f"{platform.system()} {platform.machine()}: {error}. {_OVERRIDES_HINT}"
|
|
90
|
+
) from error
|
|
91
|
+
|
|
92
|
+
# load TDLib functions from shared library
|
|
93
|
+
self._td_json_client_create = self._tdjson.td_json_client_create
|
|
94
|
+
self._td_json_client_create.restype = c_void_p
|
|
95
|
+
self._td_json_client_create.argtypes = []
|
|
96
|
+
|
|
97
|
+
self.td_json_client: int | None = self._td_json_client_create()
|
|
98
|
+
|
|
99
|
+
self._td_json_client_receive = self._tdjson.td_json_client_receive
|
|
100
|
+
self._td_json_client_receive.restype = c_char_p
|
|
101
|
+
self._td_json_client_receive.argtypes = [c_void_p, c_double]
|
|
102
|
+
|
|
103
|
+
self._td_json_client_send = self._tdjson.td_json_client_send
|
|
104
|
+
self._td_json_client_send.restype = None
|
|
105
|
+
self._td_json_client_send.argtypes = [c_void_p, c_char_p]
|
|
106
|
+
|
|
107
|
+
self._td_json_client_execute = self._tdjson.td_json_client_execute
|
|
108
|
+
self._td_json_client_execute.restype = c_char_p
|
|
109
|
+
self._td_json_client_execute.argtypes = [c_void_p, c_char_p]
|
|
110
|
+
|
|
111
|
+
self._td_json_client_destroy = self._tdjson.td_json_client_destroy
|
|
112
|
+
self._td_json_client_destroy.restype = None
|
|
113
|
+
self._td_json_client_destroy.argtypes = [c_void_p]
|
|
114
|
+
|
|
115
|
+
self._td_set_log_file_path = self._tdjson.td_set_log_file_path
|
|
116
|
+
self._td_set_log_file_path.restype = c_int
|
|
117
|
+
self._td_set_log_file_path.argtypes = [c_char_p]
|
|
118
|
+
|
|
119
|
+
self._td_set_log_max_file_size = self._tdjson.td_set_log_max_file_size
|
|
120
|
+
self._td_set_log_max_file_size.restype = None
|
|
121
|
+
self._td_set_log_max_file_size.argtypes = [c_longlong]
|
|
122
|
+
|
|
123
|
+
self._td_set_log_verbosity_level = self._tdjson.td_set_log_verbosity_level
|
|
124
|
+
self._td_set_log_verbosity_level.restype = None
|
|
125
|
+
self._td_set_log_verbosity_level.argtypes = [c_int]
|
|
126
|
+
|
|
127
|
+
self._td_set_log_verbosity_level(verbosity)
|
|
128
|
+
|
|
129
|
+
fatal_error_callback_type = CFUNCTYPE(None, c_char_p)
|
|
130
|
+
|
|
131
|
+
self._td_set_log_fatal_error_callback = self._tdjson.td_set_log_fatal_error_callback
|
|
132
|
+
self._td_set_log_fatal_error_callback.restype = None
|
|
133
|
+
self._td_set_log_fatal_error_callback.argtypes = [fatal_error_callback_type]
|
|
134
|
+
|
|
135
|
+
# initialize TDLib log with desired parameters
|
|
136
|
+
def on_fatal_error_callback(error_message: str) -> None:
|
|
137
|
+
logger.error("TDLib fatal error: %s", error_message)
|
|
138
|
+
|
|
139
|
+
self._c_on_fatal_error_callback = fatal_error_callback_type(on_fatal_error_callback)
|
|
140
|
+
self._td_set_log_fatal_error_callback(self._c_on_fatal_error_callback)
|
|
141
|
+
|
|
142
|
+
def _get_client(self) -> int:
|
|
143
|
+
"""
|
|
144
|
+
Returns the client handle, or raises if the client has been destroyed.
|
|
145
|
+
|
|
146
|
+
tdlib dereferences this handle, so passing a destroyed (NULL) one
|
|
147
|
+
crashes the whole process instead of raising.
|
|
148
|
+
"""
|
|
149
|
+
if self.td_json_client is None:
|
|
150
|
+
raise ClientDestroyedError("The tdlib client is stopped and cannot be used anymore")
|
|
151
|
+
|
|
152
|
+
return self.td_json_client
|
|
153
|
+
|
|
154
|
+
def send(self, query: dict[Any, Any]) -> None:
|
|
155
|
+
dumped_query = json.dumps(query).encode("utf-8")
|
|
156
|
+
self._td_json_client_send(self._get_client(), dumped_query)
|
|
157
|
+
logger.debug("[me ==>] Sent %s", dumped_query)
|
|
158
|
+
|
|
159
|
+
def receive(self) -> None | dict[Any, Any]:
|
|
160
|
+
result_str = self._td_json_client_receive(self._get_client(), 1.0)
|
|
161
|
+
|
|
162
|
+
if result_str:
|
|
163
|
+
result: dict[Any, Any] = json.loads(result_str.decode("utf-8"))
|
|
164
|
+
logger.debug("[me <==] Received %s", result)
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def td_execute(self, query: dict[Any, Any]) -> dict[Any, Any] | Any:
|
|
171
|
+
dumped_query = json.dumps(query).encode("utf-8")
|
|
172
|
+
result_str = self._td_json_client_execute(self._get_client(), dumped_query)
|
|
173
|
+
|
|
174
|
+
if result_str:
|
|
175
|
+
result: dict[Any, Any] = json.loads(result_str.decode("utf-8"))
|
|
176
|
+
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def stop(self) -> None:
|
|
182
|
+
if self.td_json_client is None:
|
|
183
|
+
return
|
|
184
|
+
self._td_json_client_destroy(self.td_json_client)
|
|
185
|
+
self.td_json_client = None
|
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
|
+
Text,
|
|
20
|
+
TOMLSection,
|
|
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,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from telegram.client import Telegram
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AsyncResult:
|
|
16
|
+
"""
|
|
17
|
+
tdlib is asynchronous, and this class helps you get results back.
|
|
18
|
+
After each API call, you receive AsyncResult object, which you can use to get results back.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, client: Telegram, result_id: str | None = None) -> None:
|
|
22
|
+
self.client = client
|
|
23
|
+
|
|
24
|
+
if result_id:
|
|
25
|
+
self.id = result_id
|
|
26
|
+
else:
|
|
27
|
+
self.id = uuid.uuid4().hex
|
|
28
|
+
|
|
29
|
+
self.request: dict[Any, Any] | None = None
|
|
30
|
+
self.ok_received = False
|
|
31
|
+
self.error = False
|
|
32
|
+
self.error_info: dict[Any, Any] | None = None
|
|
33
|
+
self.update: dict[Any, Any] | None = None
|
|
34
|
+
self._ready = threading.Event()
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return f"AsyncResult <{self.id}>"
|
|
38
|
+
|
|
39
|
+
def wait(self, timeout: float | None = None, raise_exc: bool = False) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Blocking method to wait for the result
|
|
42
|
+
"""
|
|
43
|
+
result = self._ready.wait(timeout=timeout)
|
|
44
|
+
if result is False:
|
|
45
|
+
raise TimeoutError()
|
|
46
|
+
if raise_exc and self.error:
|
|
47
|
+
raise RuntimeError(f"Telegram error: {self.error_info}")
|
|
48
|
+
|
|
49
|
+
def parse_update(self, update: dict[Any, Any]) -> bool:
|
|
50
|
+
update_type = update.get("@type")
|
|
51
|
+
|
|
52
|
+
logger.debug("update id=%s type=%s received", self.id, update_type)
|
|
53
|
+
|
|
54
|
+
if update_type == "ok":
|
|
55
|
+
self.ok_received = True
|
|
56
|
+
if self.id == "updateAuthorizationState":
|
|
57
|
+
# For updateAuthorizationState commands tdlib sends
|
|
58
|
+
# @type: ok responses
|
|
59
|
+
# but we want to wait longer to receive the new authorization state
|
|
60
|
+
return False
|
|
61
|
+
elif update_type == "error":
|
|
62
|
+
self.error = True
|
|
63
|
+
self.error_info = update
|
|
64
|
+
else:
|
|
65
|
+
self.update = update
|
|
66
|
+
|
|
67
|
+
self._ready.set()
|
|
68
|
+
|
|
69
|
+
return True
|
telegram/worker.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from queue import Empty, Queue
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseWorker:
|
|
9
|
+
"""
|
|
10
|
+
Base worker class.
|
|
11
|
+
Each worker must implement the run method to start listening to the queue
|
|
12
|
+
and calling handler functions
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, queue: Queue):
|
|
16
|
+
self._is_enabled = True
|
|
17
|
+
self._queue = queue
|
|
18
|
+
|
|
19
|
+
def run(self) -> None:
|
|
20
|
+
raise NotImplementedError()
|
|
21
|
+
|
|
22
|
+
def stop(self) -> None:
|
|
23
|
+
raise NotImplementedError()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimpleWorker(BaseWorker):
|
|
27
|
+
"""Simple one-thread worker"""
|
|
28
|
+
|
|
29
|
+
def run(self) -> None:
|
|
30
|
+
self._thread = threading.Thread(target=self._run_thread)
|
|
31
|
+
self._thread.daemon = True
|
|
32
|
+
self._thread.start()
|
|
33
|
+
|
|
34
|
+
def _run_thread(self) -> None:
|
|
35
|
+
logger.info("[SimpleWorker] started")
|
|
36
|
+
|
|
37
|
+
while self._is_enabled:
|
|
38
|
+
try:
|
|
39
|
+
handler, update = self._queue.get(timeout=0.5)
|
|
40
|
+
except Empty:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
handler(update)
|
|
45
|
+
except Exception:
|
|
46
|
+
logger.exception("Error in update handler %s", handler)
|
|
47
|
+
self._queue.task_done()
|
|
48
|
+
|
|
49
|
+
def stop(self) -> None:
|
|
50
|
+
self._is_enabled = False
|
|
51
|
+
self._thread.join()
|