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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-trueconf-bot
3
- Version: 1.2.3.dev1
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.2.3.dev1.dist-info/licenses/LICENSE,sha256=ftDuEzhr-yY_dsfpEQ2SKAP_dEAO-iPt96MBZq_K48o,1684
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=GU697UbDRMobw369MOjK5OiWpHCOfOcCJPfxrNBaeMw,533
4
- trueconf/exceptions.py,sha256=oBDcWPd1O7059fKrPbdP29VdsWcom6GTfVUeM93OvUE,4436
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=1GNhQBh65W5GKQdtEVWqGkTRt5ZcIbBFb5PPisBmL1E,71343
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=yE8sl3f9Jin9IH4ntebCddYAVyEWCLuUM8_TUTwDDpo,23228
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/_token.py,sha256=quI_OgdnLcfapBYZc3ejAX8AozlpQ9gGAZ5SdNtELDw,2307
140
- trueconf/utils/_version_checker.py,sha256=WmVnPLoxfAqRyemNuaE-wQE6sQMcXxgyvTQ2F6kdSgQ,1709
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
- python_trueconf_bot-1.2.3.dev1.dist-info/METADATA,sha256=Em79RYDbhZrYf_VPdwFmXk9Lrtu07xGGBSRXA-9YrgM,5679
143
- python_trueconf_bot-1.2.3.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
144
- python_trueconf_bot-1.2.3.dev1.dist-info/top_level.txt,sha256=GjgYdn_osTUuU-H8f59VhIS7MY44xLV7-D7veywJaeY,9
145
- python_trueconf_bot-1.2.3.dev1.dist-info/RECORD,,
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.2.3.dev1'
22
- __version_tuple__ = version_tuple = (1, 2, 3, 'dev1')
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: bool = True,
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): Whether to verify the server's SSL certificate. Defaults to True.
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.verify_ssl = verify_ssl
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={verify_ssl}, ws_max_retries={ws_max_retries}, ws_max_delay={ws_max_delay}, "
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.verify_ssl, timeout=5) as client:
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.verify_ssl, timeout=5) as client:
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 me(self) -> str:
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: bool = True,
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): Whether to verify the server's SSL certificate. Defaults to True.
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
- token = _get_auth_token(server, username, password, verify=verify_ssl)
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=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.verify_ssl, timeout=httpx.Timeout(timeout)) as client:
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
- if not self.verify_ssl:
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
- if self.verify_ssl:
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(f"⏸️ Bot running, waiting for shutdown signal")
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