python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.3.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_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/METADATA +2 -1
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/RECORD +15 -11
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +184 -34
- trueconf/exceptions.py +107 -10
- trueconf/types/message.py +57 -21
- trueconf/utils/_ssl.py +44 -0
- trueconf/utils/_token.py +4 -4
- trueconf/utils/_version_checker.py +6 -0
- trueconf/utils/formatting/__init__.py +21 -0
- trueconf/utils/formatting/decorations.py +87 -0
- trueconf/utils/formatting/text.py +252 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-trueconf-bot
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Lightweight and powerful framework for the TrueConf Server Chatbot API
|
|
5
5
|
Author-email: TrueConf LLC <info@trueconf.com>, Anton Baadzhi <baadzhianton@gmail.com>
|
|
6
6
|
License-Expression: BSD-3-Clause-Clear
|
|
@@ -35,6 +35,7 @@ Requires-Dist: python-magic>=0.4.27; sys_platform != "win32"
|
|
|
35
35
|
Requires-Dist: python-magic-bin==0.4.14; sys_platform == "win32"
|
|
36
36
|
Requires-Dist: filetype
|
|
37
37
|
Requires-Dist: packaging
|
|
38
|
+
Requires-Dist: truststore
|
|
38
39
|
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
<p align="center">
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
python_trueconf_bot-1.
|
|
1
|
+
python_trueconf_bot-1.3.0.dist-info/licenses/LICENSE,sha256=ftDuEzhr-yY_dsfpEQ2SKAP_dEAO-iPt96MBZq_K48o,1684
|
|
2
2
|
trueconf/__init__.py,sha256=JRFC0ca_JhwlDuzcnZ1r9bflnAmQyMYhJr_hBRYcdpw,394
|
|
3
|
-
trueconf/_version.py,sha256=
|
|
4
|
-
trueconf/exceptions.py,sha256=
|
|
3
|
+
trueconf/_version.py,sha256=kjMXs_RelAfH3nsfXcggy4eqwR8yDdd160wJ9EHB63g,520
|
|
4
|
+
trueconf/exceptions.py,sha256=JlnOybf6XBVKGYPPN3STlruupdfQXrLdeZ_FbtUuMIs,7653
|
|
5
5
|
trueconf/loggers.py,sha256=4dnq14qFo6fJ8SKZJCRZd0iJDYRqmcuX-ccv5FezxlQ,594
|
|
6
6
|
trueconf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
trueconf/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
trueconf/client/bot.py,sha256=
|
|
8
|
+
trueconf/client/bot.py,sha256=CCtWyKrmxQxETBSD31sd4r6I5_L4eWHgh4sNXfNYBUI,78540
|
|
9
9
|
trueconf/client/context_controller.py,sha256=_aItNHblyFbEqRsnbOM3ESgPqg2Pkji7xidTx0gb6zk,660
|
|
10
10
|
trueconf/client/session.py,sha256=CvHjxQXbkUyyS1r7YCFVL8al37zOYtnhw5ACNLJJDOc,1751
|
|
11
11
|
trueconf/dispatcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -67,7 +67,7 @@ trueconf/types/author_box.py,sha256=G_9AxpBGIbZyWEHCfKg_DkEbte9f9FWt-JQ05gro3bU,
|
|
|
67
67
|
trueconf/types/chat_participant.py,sha256=08sxIkiLK0IFZdHF_yApWHroNZTd-9tj9dVBOvZcI1E,328
|
|
68
68
|
trueconf/types/input_file.py,sha256=mkr0dqnlTUOZjoFrOcX1_GsecqJ7trfQ1vSWquC4MYc,16806
|
|
69
69
|
trueconf/types/last_message.py,sha256=87-mPWYEIi7RAYB1os7iqnPE7fvtQhZhnloMAkybLKU,1541
|
|
70
|
-
trueconf/types/message.py,sha256=
|
|
70
|
+
trueconf/types/message.py,sha256=eZu4sjDJNDfDxddtMefDka_b-rZ1usiMnCGVsfdOsJI,24871
|
|
71
71
|
trueconf/types/parser.py,sha256=R_xV4EjJQMX15LQwbr6KrJd94qaZZ6h2Yt7VsbNJ9O8,4814
|
|
72
72
|
trueconf/types/update.py,sha256=FS4d_NKjTHWtUiB6-ShPNkOll7hYFTl71gilLPRv45g,287
|
|
73
73
|
trueconf/types/content/__init__.py,sha256=9wpEMRUWc8Vj-kVY8GFSFUlmrjKhdlL2V1QlZvcC49E,645
|
|
@@ -136,10 +136,14 @@ trueconf/types/responses/unsubscribe_file_progress_response.py,sha256=_5C-LmO7Pz
|
|
|
136
136
|
trueconf/types/responses/upload_file_response.py,sha256=CmhLmby4MVny8KmfQ1aHW6UDnpcsW_BqL9wgAitUyxM,244
|
|
137
137
|
trueconf/utils/__init__.py,sha256=O1ZS6uwnlZpSH19FEodZQ2fOLyua0SJYsIgEO70UIRg,39
|
|
138
138
|
trueconf/utils/_generate_secret_for_survey.py,sha256=QclUSR_T2PU2mkUSCI3LG72HMTBTHAHaYFCm8gkevwI,242
|
|
139
|
-
trueconf/utils/
|
|
140
|
-
trueconf/utils/
|
|
139
|
+
trueconf/utils/_ssl.py,sha256=pMEGtCQtnaeo5zD9cS15Sry2hBnUkDx8I0BVov9PV1c,1419
|
|
140
|
+
trueconf/utils/_token.py,sha256=vbZp8hwHu59DzqMpE0MqeQ7EM55txLkKNDFV031Up6Q,2336
|
|
141
|
+
trueconf/utils/_version_checker.py,sha256=U1AB0KgASTCuI68r7SiLGnWeeSy3T1yFqLNg8h3fjM0,2029
|
|
141
142
|
trueconf/utils/split_text.py,sha256=0HTVv63kLo2gx1r-ImM47ejTR4X_uDkNr5QMxvWdTBo,18807
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
python_trueconf_bot-1.
|
|
143
|
+
trueconf/utils/formatting/__init__.py,sha256=Ii9rEamQdy8R0pVbmDf3xbdB4TtmgYyu2-RWRTCXukw,261
|
|
144
|
+
trueconf/utils/formatting/decorations.py,sha256=iQzCdFBELn16llCgTTPk43K5-BzY6BGPc1M3V2Q0tPQ,2175
|
|
145
|
+
trueconf/utils/formatting/text.py,sha256=pOW07TIV5vfvtWmGa2i2l_Q0UjFM6QMuowmsAjE9nyo,6867
|
|
146
|
+
python_trueconf_bot-1.3.0.dist-info/METADATA,sha256=N8b9bbvbyice_Egb7QWBYOmi_e_fgrkJGdCQli0A8rY,5700
|
|
147
|
+
python_trueconf_bot-1.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
148
|
+
python_trueconf_bot-1.3.0.dist-info/top_level.txt,sha256=GjgYdn_osTUuU-H8f59VhIS7MY44xLV7-D7veywJaeY,9
|
|
149
|
+
python_trueconf_bot-1.3.0.dist-info/RECORD,,
|
trueconf/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 3, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
trueconf/client/bot.py
CHANGED
|
@@ -4,7 +4,6 @@ import contextlib
|
|
|
4
4
|
import httpx
|
|
5
5
|
import json
|
|
6
6
|
import signal
|
|
7
|
-
import ssl
|
|
8
7
|
import websockets
|
|
9
8
|
import warnings
|
|
10
9
|
import random
|
|
@@ -17,10 +16,11 @@ from typing import (
|
|
|
17
16
|
Dict,
|
|
18
17
|
List,
|
|
19
18
|
Tuple,
|
|
20
|
-
TypeVar
|
|
19
|
+
TypeVar,
|
|
20
|
+
Any
|
|
21
21
|
)
|
|
22
|
-
|
|
23
|
-
from typing_extensions import Self
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing_extensions import Self, deprecated
|
|
24
24
|
|
|
25
25
|
from trueconf import loggers
|
|
26
26
|
from trueconf.client.session import WebSocketSession
|
|
@@ -66,7 +66,7 @@ from trueconf.types.input_file import InputFile, URLInputFile
|
|
|
66
66
|
from trueconf.types.parser import parse_update
|
|
67
67
|
from trueconf.types.requests.uploading_progress import UploadingProgress
|
|
68
68
|
from trueconf.types.requests.changed_file_upload_limits import ChangedFileUploadLimits
|
|
69
|
-
from trueconf.types.responses import GetFileUploadLimitsResponse, ApiError
|
|
69
|
+
from trueconf.types.responses import GetFileUploadLimitsResponse, ApiError, AuthResponsePayload
|
|
70
70
|
from trueconf.types.responses.add_chat_participant_response import AddChatParticipantResponse
|
|
71
71
|
from trueconf.types.responses.change_participant_role_response import ChangeParticipantRoleResponse
|
|
72
72
|
from trueconf.types.responses.clear_chat_history_response import ClearChatHistoryResponse
|
|
@@ -99,6 +99,7 @@ from trueconf.types.responses.unsubscribe_file_progress_response import Unsubscr
|
|
|
99
99
|
from trueconf.utils._generate_secret_for_survey import _generate_secret_for_survey
|
|
100
100
|
from trueconf.utils._token import _get_auth_token, _validate_token
|
|
101
101
|
from trueconf.utils._version_checker import _VersionChecker
|
|
102
|
+
from trueconf.utils._ssl import _build_ssl_context, _describe_ssl_context, SSLVerify
|
|
102
103
|
from trueconf.utils.split_text import visible_len
|
|
103
104
|
|
|
104
105
|
from trueconf.exceptions import (
|
|
@@ -113,6 +114,8 @@ from trueconf.exceptions import (
|
|
|
113
114
|
|
|
114
115
|
T = TypeVar("T")
|
|
115
116
|
|
|
117
|
+
HealthCheckCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
|
118
|
+
|
|
116
119
|
class Bot:
|
|
117
120
|
def __init__(
|
|
118
121
|
self,
|
|
@@ -122,12 +125,13 @@ class Bot:
|
|
|
122
125
|
dispatcher: Dispatcher | None = None,
|
|
123
126
|
receive_unread_messages: bool = False,
|
|
124
127
|
receive_system_messages: bool = False,
|
|
125
|
-
verify_ssl:
|
|
128
|
+
verify_ssl: SSLVerify = True,
|
|
126
129
|
web_port: int | None = None,
|
|
127
130
|
https: bool = True,
|
|
128
131
|
ws_max_retries: int = 5,
|
|
129
132
|
ws_max_delay: int = 60,
|
|
130
133
|
debug: bool = False,
|
|
134
|
+
on_health_check: HealthCheckCallback | None = None,
|
|
131
135
|
):
|
|
132
136
|
"""
|
|
133
137
|
Initializes a TrueConf chatbot instance with WebSocket connection and configuration options.
|
|
@@ -143,12 +147,19 @@ class Bot:
|
|
|
143
147
|
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
144
148
|
receive_system_messages (bool, optional): Whether to receive system messages, such as user additions
|
|
145
149
|
to the chat or chat title changes. Defaults to False.
|
|
146
|
-
verify_ssl (bool, optional):
|
|
150
|
+
verify_ssl (bool | str | ssl.SSLContext, optional): SSL verification mode. If True, verifies
|
|
151
|
+
the server certificate using the system trust store when available. If False, disables
|
|
152
|
+
certificate verification. If a string is provided, it must be a path to a CA bundle file.
|
|
153
|
+
A custom ssl.SSLContext can also be passed. Defaults to True.
|
|
147
154
|
web_port (int, optional): WebSocket connection port. Defaults to 443.
|
|
148
155
|
https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
|
|
149
156
|
ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
|
|
150
157
|
ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
|
|
151
158
|
debug (bool, optional): Enables debug mode. Defaults to False.
|
|
159
|
+
on_health_check (HealthCheckCallback | None, optional): Async callback called when the bot
|
|
160
|
+
connection health changes. The callback receives a dictionary with the current status,
|
|
161
|
+
WebSocket state, authorization state, server, port, protocol, timestamp, and optional
|
|
162
|
+
error details. Defaults to None.
|
|
152
163
|
|
|
153
164
|
Note:
|
|
154
165
|
Alternatively, you can authorize using a username and password via the `from_credentials()` class method.
|
|
@@ -161,7 +172,7 @@ class Bot:
|
|
|
161
172
|
self.dp = dispatcher or Dispatcher()
|
|
162
173
|
self.receive_unread_messages = receive_unread_messages
|
|
163
174
|
self.receive_system_messages = receive_system_messages
|
|
164
|
-
self.
|
|
175
|
+
self.ssl_context = _build_ssl_context(verify_ssl)
|
|
165
176
|
self.https = https
|
|
166
177
|
self._ws_max_retries = ws_max_retries
|
|
167
178
|
self._ws_max_delay = ws_max_delay
|
|
@@ -186,10 +197,12 @@ class Bot:
|
|
|
186
197
|
self.max_file_size: int | None = None
|
|
187
198
|
self.file_extension_filter_mode: str | None = None
|
|
188
199
|
self.file_extensions_list: set | None = None
|
|
200
|
+
self._me_id: str
|
|
201
|
+
self._on_health_check = on_health_check
|
|
189
202
|
|
|
190
203
|
loggers.chatbot.info(
|
|
191
204
|
f"Bot initialized: server={server}:{self.port}, protocol={self._protocol}, "
|
|
192
|
-
f"verify_ssl={
|
|
205
|
+
f"verify_ssl={_describe_ssl_context(self.ssl_context)}, ws_max_retries={ws_max_retries}, ws_max_delay={ws_max_delay}, "
|
|
193
206
|
f"receive_unread={receive_unread_messages}"
|
|
194
207
|
)
|
|
195
208
|
|
|
@@ -209,7 +222,7 @@ class Bot:
|
|
|
209
222
|
|
|
210
223
|
async def __get_domain_name(self):
|
|
211
224
|
try:
|
|
212
|
-
async with httpx.AsyncClient(verify=self.
|
|
225
|
+
async with httpx.AsyncClient(verify=self.ssl_context, timeout=5) as client:
|
|
213
226
|
response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
|
|
214
227
|
domain_name = response.json().get("product").get("display_name")
|
|
215
228
|
loggers.chatbot.info(f"🌐 Server domain_name resolved: {domain_name}")
|
|
@@ -220,7 +233,7 @@ class Bot:
|
|
|
220
233
|
|
|
221
234
|
async def __get_server_version(self):
|
|
222
235
|
try:
|
|
223
|
-
async with httpx.AsyncClient(verify=self.
|
|
236
|
+
async with httpx.AsyncClient(verify=self.ssl_context, timeout=5) as client:
|
|
224
237
|
response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
|
|
225
238
|
version = response.json().get("product").get("version")
|
|
226
239
|
loggers.chatbot.info(f"📦 Server version resolved: {version}")
|
|
@@ -266,8 +279,12 @@ class Bot:
|
|
|
266
279
|
current_version = await self.server_version
|
|
267
280
|
_VersionChecker.check(current_version)
|
|
268
281
|
|
|
282
|
+
@property
|
|
283
|
+
def me_id(self) -> str:
|
|
284
|
+
return self._me_id
|
|
285
|
+
|
|
269
286
|
@async_cached_property
|
|
270
|
-
async def
|
|
287
|
+
async def me_chat(self) -> str:
|
|
271
288
|
"""
|
|
272
289
|
Returns the identifier of the bot's personal (Favorites) chat.
|
|
273
290
|
|
|
@@ -281,6 +298,20 @@ class Bot:
|
|
|
281
298
|
r = await self.create_favorites_chat()
|
|
282
299
|
return r.chat_id
|
|
283
300
|
|
|
301
|
+
@async_cached_property
|
|
302
|
+
@deprecated("bot.me is deprecated, use bot.me_chat instead", stacklevel=2)
|
|
303
|
+
async def me(self) -> str:
|
|
304
|
+
"""
|
|
305
|
+
Returns the identifier of the bot's personal (Favorites) chat.
|
|
306
|
+
|
|
307
|
+
If the chat does not exist yet, it will be created automatically.
|
|
308
|
+
The result is cached to prevent redundant API calls.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
str: Chat ID of the bot's personal Favorites chat.
|
|
312
|
+
"""
|
|
313
|
+
return await self.me_chat
|
|
314
|
+
|
|
284
315
|
@classmethod
|
|
285
316
|
def from_credentials(
|
|
286
317
|
cls,
|
|
@@ -291,11 +322,13 @@ class Bot:
|
|
|
291
322
|
dispatcher: Dispatcher | None = None,
|
|
292
323
|
receive_unread_messages: bool = False,
|
|
293
324
|
receive_system_messages: bool = False,
|
|
294
|
-
verify_ssl:
|
|
325
|
+
verify_ssl: SSLVerify = True,
|
|
295
326
|
web_port: int | None = None,
|
|
296
327
|
https: bool = True,
|
|
297
328
|
ws_max_retries: int = 5,
|
|
298
329
|
ws_max_delay: int = 60,
|
|
330
|
+
debug: bool = False,
|
|
331
|
+
on_health_check: HealthCheckCallback | None = None,
|
|
299
332
|
) -> Self:
|
|
300
333
|
"""
|
|
301
334
|
Creates a bot instance using username and password authentication.
|
|
@@ -312,11 +345,19 @@ class Bot:
|
|
|
312
345
|
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
313
346
|
receive_system_messages (bool, optional): Whether to receive system messages, such as user additions
|
|
314
347
|
to the chat or chat title changes. Defaults to False.
|
|
315
|
-
verify_ssl (bool, optional):
|
|
348
|
+
verify_ssl (bool | str | ssl.SSLContext, optional): SSL verification mode. If True, verifies
|
|
349
|
+
the server certificate using the system trust store when available. If False, disables
|
|
350
|
+
certificate verification. If a string is provided, it must be a path to a CA bundle file.
|
|
351
|
+
A custom ssl.SSLContext can also be passed. Defaults to True.
|
|
316
352
|
web_port (int, optional): WebSocket connection port. Defaults to 443.
|
|
317
353
|
https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
|
|
318
354
|
ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
|
|
319
355
|
ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
|
|
356
|
+
debug (bool, optional): Enables debug mode. Defaults to False.
|
|
357
|
+
on_health_check (HealthCheckCallback | None, optional): Async callback called when the bot
|
|
358
|
+
connection health changes. The callback receives a dictionary with the current status,
|
|
359
|
+
WebSocket state, authorization state, server, port, protocol, timestamp, and optional
|
|
360
|
+
error details. Defaults to None.
|
|
320
361
|
|
|
321
362
|
Returns:
|
|
322
363
|
Bot: An authorized bot instance.
|
|
@@ -326,7 +367,8 @@ class Bot:
|
|
|
326
367
|
"""
|
|
327
368
|
|
|
328
369
|
loggers.chatbot.info(f"🔑 Obtaining auth token for user={username} @ {server}")
|
|
329
|
-
|
|
370
|
+
ssl_context = _build_ssl_context(verify_ssl)
|
|
371
|
+
token = _get_auth_token(server, username, password, ssl_context=ssl_context)
|
|
330
372
|
if not token:
|
|
331
373
|
loggers.chatbot.error(f"❌ Failed to obtain token for user={username} @ {server}")
|
|
332
374
|
raise RuntimeError("Failed to obtain token")
|
|
@@ -338,9 +380,11 @@ class Bot:
|
|
|
338
380
|
dispatcher=dispatcher,
|
|
339
381
|
receive_unread_messages=receive_unread_messages,
|
|
340
382
|
receive_system_messages = receive_system_messages,
|
|
341
|
-
verify_ssl=
|
|
383
|
+
verify_ssl=ssl_context,
|
|
342
384
|
ws_max_delay=ws_max_delay,
|
|
343
|
-
ws_max_retries=ws_max_retries
|
|
385
|
+
ws_max_retries=ws_max_retries,
|
|
386
|
+
debug=debug,
|
|
387
|
+
on_health_check=on_health_check,
|
|
344
388
|
)
|
|
345
389
|
|
|
346
390
|
async def __wait_upload_complete(
|
|
@@ -402,7 +446,7 @@ class Bot:
|
|
|
402
446
|
if dest_path:
|
|
403
447
|
loggers.chatbot.info(f"⬇️ Destination: {dest_path}")
|
|
404
448
|
try:
|
|
405
|
-
async with httpx.AsyncClient(verify=self.
|
|
449
|
+
async with httpx.AsyncClient(verify=self.ssl_context, timeout=httpx.Timeout(timeout)) as client:
|
|
406
450
|
async with client.stream("GET", url) as resp:
|
|
407
451
|
resp.raise_for_status()
|
|
408
452
|
|
|
@@ -477,13 +521,7 @@ class Bot:
|
|
|
477
521
|
"Upload-Task-Id": upload_task_id,
|
|
478
522
|
}
|
|
479
523
|
|
|
480
|
-
|
|
481
|
-
ssl_context = ssl.create_default_context()
|
|
482
|
-
ssl_context.check_hostname = False
|
|
483
|
-
ssl_context.verify_mode = ssl.CERT_NONE
|
|
484
|
-
connector = TCPConnector(ssl=ssl_context)
|
|
485
|
-
else:
|
|
486
|
-
connector = TCPConnector()
|
|
524
|
+
connector = TCPConnector(ssl=self.ssl_context)
|
|
487
525
|
|
|
488
526
|
try:
|
|
489
527
|
async with ClientSession(connector=connector, timeout=ClientTimeout(total=timeout)) as session:
|
|
@@ -532,10 +570,7 @@ class Bot:
|
|
|
532
570
|
|
|
533
571
|
if self.https:
|
|
534
572
|
ws_protocol = "wss"
|
|
535
|
-
|
|
536
|
-
ssl_context = ssl.create_default_context()
|
|
537
|
-
else:
|
|
538
|
-
ssl_context = ssl._create_unverified_context()
|
|
573
|
+
ssl_context = self.ssl_context
|
|
539
574
|
else:
|
|
540
575
|
ws_protocol = "ws"
|
|
541
576
|
|
|
@@ -546,7 +581,7 @@ class Bot:
|
|
|
546
581
|
loggers.chatbot.info("⏳ Attempting WebSocket connection...")
|
|
547
582
|
async with websockets.connect(
|
|
548
583
|
uri=f"{ws_protocol}://{self.server}:{self.port}/websocket/chat_bot",
|
|
549
|
-
ssl=ssl_context,
|
|
584
|
+
ssl = ssl_context,
|
|
550
585
|
ping_interval=30,
|
|
551
586
|
ping_timeout=10
|
|
552
587
|
) as ws:
|
|
@@ -561,14 +596,34 @@ class Bot:
|
|
|
561
596
|
|
|
562
597
|
self.connected_event.set()
|
|
563
598
|
self.authorized_event.clear()
|
|
599
|
+
await self._call_health_check({
|
|
600
|
+
"status": "connected",
|
|
601
|
+
"websocket_connected": True,
|
|
602
|
+
"authorized": False,
|
|
603
|
+
"server": self.server,
|
|
604
|
+
"port": self.port,
|
|
605
|
+
"protocol": self._protocol,
|
|
606
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
607
|
+
})
|
|
564
608
|
|
|
565
609
|
try:
|
|
566
|
-
await self.__authorize()
|
|
610
|
+
user_id = await self.__authorize()
|
|
611
|
+
self.authorized_event.set()
|
|
612
|
+
|
|
613
|
+
await self._call_health_check({
|
|
614
|
+
"status": "authorized",
|
|
615
|
+
"websocket_connected": True,
|
|
616
|
+
"authorized": True,
|
|
617
|
+
"user_id": user_id,
|
|
618
|
+
"server": self.server,
|
|
619
|
+
"port": self.port,
|
|
620
|
+
"protocol": self._protocol,
|
|
621
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
622
|
+
})
|
|
567
623
|
except ApiErrorException as e:
|
|
568
624
|
loggers.chatbot.error(f"❌ Authorization failed: {e}")
|
|
569
625
|
raise
|
|
570
626
|
|
|
571
|
-
self.authorized_event.set()
|
|
572
627
|
await ws.wait_closed()
|
|
573
628
|
|
|
574
629
|
except asyncio.CancelledError:
|
|
@@ -576,6 +631,7 @@ class Bot:
|
|
|
576
631
|
raise
|
|
577
632
|
|
|
578
633
|
except (websockets.exceptions.ConnectionClosed, websockets.exceptions.InvalidStatus, OSError) as e:
|
|
634
|
+
was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
|
|
579
635
|
self.connected_event.clear()
|
|
580
636
|
self.authorized_event.clear()
|
|
581
637
|
|
|
@@ -587,6 +643,18 @@ class Bot:
|
|
|
587
643
|
else:
|
|
588
644
|
reason = str(e)
|
|
589
645
|
|
|
646
|
+
if was_connected:
|
|
647
|
+
await self._call_health_check({
|
|
648
|
+
"status": "disconnected",
|
|
649
|
+
"websocket_connected": False,
|
|
650
|
+
"authorized": False,
|
|
651
|
+
"error": reason,
|
|
652
|
+
"server": self.server,
|
|
653
|
+
"port": self.port,
|
|
654
|
+
"protocol": self._protocol,
|
|
655
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
656
|
+
})
|
|
657
|
+
|
|
590
658
|
delay = min(delay * 2, self._ws_max_delay) + random.uniform(0, 1)
|
|
591
659
|
msg = f"🔌 Connection issue: {reason}. Retrying in {delay:.1f}s..."
|
|
592
660
|
loggers.chatbot.warning(msg)
|
|
@@ -594,8 +662,22 @@ class Bot:
|
|
|
594
662
|
continue
|
|
595
663
|
|
|
596
664
|
except websockets.exceptions.InvalidURI as e:
|
|
665
|
+
was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
|
|
597
666
|
retry_count += 1
|
|
598
667
|
self.connected_event.clear()
|
|
668
|
+
self.authorized_event.clear()
|
|
669
|
+
|
|
670
|
+
if was_connected:
|
|
671
|
+
await self._call_health_check({
|
|
672
|
+
"status": "disconnected",
|
|
673
|
+
"websocket_connected": False,
|
|
674
|
+
"authorized": False,
|
|
675
|
+
"error": str(e),
|
|
676
|
+
"server": self.server,
|
|
677
|
+
"port": self.port,
|
|
678
|
+
"protocol": self._protocol,
|
|
679
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
680
|
+
})
|
|
599
681
|
|
|
600
682
|
if retry_count > self._ws_max_retries:
|
|
601
683
|
loggers.chatbot.error(f"❌ Critical network error: {e}. Check your URI/IP. Giving up.")
|
|
@@ -608,8 +690,21 @@ class Bot:
|
|
|
608
690
|
continue
|
|
609
691
|
|
|
610
692
|
finally:
|
|
693
|
+
was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
|
|
611
694
|
self.connected_event.clear()
|
|
612
695
|
self.authorized_event.clear()
|
|
696
|
+
|
|
697
|
+
if was_connected:
|
|
698
|
+
await self._call_health_check({
|
|
699
|
+
"status": "disconnected",
|
|
700
|
+
"websocket_connected": False,
|
|
701
|
+
"authorized": False,
|
|
702
|
+
"server": self.server,
|
|
703
|
+
"port": self.port,
|
|
704
|
+
"protocol": self._protocol,
|
|
705
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
706
|
+
})
|
|
707
|
+
|
|
613
708
|
if self._session:
|
|
614
709
|
with contextlib.suppress(Exception):
|
|
615
710
|
await self._session.detach()
|
|
@@ -625,7 +720,7 @@ class Bot:
|
|
|
625
720
|
if future and not future.done():
|
|
626
721
|
future.set_result(message)
|
|
627
722
|
|
|
628
|
-
async def __authorize(self):
|
|
723
|
+
async def __authorize(self) -> str:
|
|
629
724
|
loggers.chatbot.info("🚀 Starting authorization")
|
|
630
725
|
|
|
631
726
|
call = AuthMethod(
|
|
@@ -635,7 +730,9 @@ class Bot:
|
|
|
635
730
|
)
|
|
636
731
|
loggers.chatbot.info(f"🛠 Created AuthMethod with id={call.id}")
|
|
637
732
|
result = await self(call)
|
|
733
|
+
self._me_id = result.user_id.split("/", maxsplit=1)[0]
|
|
638
734
|
loggers.chatbot.info(f"🔐 Authenticated as {result.user_id}")
|
|
735
|
+
return result.user_id
|
|
639
736
|
|
|
640
737
|
async def __process_message(self, data: dict):
|
|
641
738
|
data = parse_update(data)
|
|
@@ -703,6 +800,59 @@ class Bot:
|
|
|
703
800
|
)
|
|
704
801
|
loggers.chatbot.info(f"✅ File passed limitations check: {file.file_name}")
|
|
705
802
|
|
|
803
|
+
async def _call_health_check(self, status: Dict[str, Any]) -> None:
|
|
804
|
+
"""
|
|
805
|
+
Calls the registered health-check callback with the provided status payload.
|
|
806
|
+
|
|
807
|
+
The callback is optional. If it is not registered, the method returns immediately.
|
|
808
|
+
Exceptions raised inside the callback are caught and logged so that user-defined
|
|
809
|
+
monitoring logic cannot interrupt the bot connection loop.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
status (Dict[str, Any]): Current bot health status payload. Common fields include
|
|
813
|
+
``status``, ``websocket_connected``, ``authorized``, ``server``, ``port``,
|
|
814
|
+
``protocol``, ``timestamp``, and optionally ``user_id`` or ``error``.
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
None
|
|
818
|
+
"""
|
|
819
|
+
if self._on_health_check is None:
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
await self._on_health_check(status)
|
|
824
|
+
except Exception as e:
|
|
825
|
+
loggers.chatbot.error(f"Health check callback error: {e}")
|
|
826
|
+
|
|
827
|
+
def health_check(self) -> Dict[str, Any]:
|
|
828
|
+
"""
|
|
829
|
+
Returns the current bot health status.
|
|
830
|
+
|
|
831
|
+
This method can be used by external monitoring systems, HTTP health endpoints,
|
|
832
|
+
or application code that needs to check the current WebSocket and authorization
|
|
833
|
+
state without waiting for a callback event.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
Dict[str, Any]: Dictionary containing the current status, WebSocket state,
|
|
837
|
+
authorization state, server, port, protocol, and UTC timestamp.
|
|
838
|
+
"""
|
|
839
|
+
return {
|
|
840
|
+
"status": (
|
|
841
|
+
"authorized"
|
|
842
|
+
if self.authorized_event.is_set()
|
|
843
|
+
else "connected"
|
|
844
|
+
if self.connected_event.is_set()
|
|
845
|
+
else "disconnected"
|
|
846
|
+
),
|
|
847
|
+
"websocket_connected": self.connected_event.is_set(),
|
|
848
|
+
"authorized": self.authorized_event.is_set(),
|
|
849
|
+
"server": self.server,
|
|
850
|
+
"port": self.port,
|
|
851
|
+
"protocol": self._protocol,
|
|
852
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
|
|
706
856
|
async def add_participant_to_chat(
|
|
707
857
|
self,
|
|
708
858
|
chat_id: str,
|
|
@@ -1477,7 +1627,7 @@ class Bot:
|
|
|
1477
1627
|
pass
|
|
1478
1628
|
|
|
1479
1629
|
await self.start()
|
|
1480
|
-
loggers.chatbot.info(
|
|
1630
|
+
loggers.chatbot.info("⏸️ Bot running, waiting for shutdown signal")
|
|
1481
1631
|
if self._connect_task:
|
|
1482
1632
|
try:
|
|
1483
1633
|
await self._connect_task
|