python-trueconf-bot 1.2.3.dev1__py3-none-any.whl → 1.4.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.4.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,16 +1,17 @@
1
- python_trueconf_bot-1.2.3.dev1.dist-info/licenses/LICENSE,sha256=ftDuEzhr-yY_dsfpEQ2SKAP_dEAO-iPt96MBZq_K48o,1684
2
- trueconf/__init__.py,sha256=JRFC0ca_JhwlDuzcnZ1r9bflnAmQyMYhJr_hBRYcdpw,394
3
- trueconf/_version.py,sha256=GU697UbDRMobw369MOjK5OiWpHCOfOcCJPfxrNBaeMw,533
4
- trueconf/exceptions.py,sha256=oBDcWPd1O7059fKrPbdP29VdsWcom6GTfVUeM93OvUE,4436
1
+ python_trueconf_bot-1.4.0.dist-info/licenses/LICENSE,sha256=ftDuEzhr-yY_dsfpEQ2SKAP_dEAO-iPt96MBZq_K48o,1684
2
+ trueconf/__init__.py,sha256=Ka9VLhsdu-jZPndOQI1QpUXWldpu8xTC2BfgW51Px5U,481
3
+ trueconf/_version.py,sha256=u2oUj6dCZIqQ271Pm6INHvXKeiux4BSapl6g0c_tllg,520
4
+ trueconf/exceptions.py,sha256=JlnOybf6XBVKGYPPN3STlruupdfQXrLdeZ_FbtUuMIs,7653
5
5
  trueconf/loggers.py,sha256=4dnq14qFo6fJ8SKZJCRZd0iJDYRqmcuX-ccv5FezxlQ,594
6
+ trueconf/middleware.py,sha256=7yaS4UjZSrPb-lv5STmX8qGg1OamYbO5Nd5-IjPtbU4,2281
6
7
  trueconf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
8
  trueconf/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- trueconf/client/bot.py,sha256=1GNhQBh65W5GKQdtEVWqGkTRt5ZcIbBFb5PPisBmL1E,71343
9
+ trueconf/client/bot.py,sha256=vfIPb5ok5RUuVqvhoQzf5v0P_9Zz72sF_DiqJcl20wI,78735
9
10
  trueconf/client/context_controller.py,sha256=_aItNHblyFbEqRsnbOM3ESgPqg2Pkji7xidTx0gb6zk,660
10
11
  trueconf/client/session.py,sha256=CvHjxQXbkUyyS1r7YCFVL8al37zOYtnhw5ACNLJJDOc,1751
11
12
  trueconf/dispatcher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- trueconf/dispatcher/dispatcher.py,sha256=3l0CVXWj5pPIzN2jd4O6aClZ4LS0Gm0E55sM8P_WMOs,2114
13
- trueconf/dispatcher/router.py,sha256=Oqwfq854565a0kVSsa_0QkdTIR3g0T8YzSkqnAae-oQ,15843
13
+ trueconf/dispatcher/dispatcher.py,sha256=v2TRq90NZ5j85gJAANfVp1HPFB6Pt_AueFQCbmRCCt4,5802
14
+ trueconf/dispatcher/router.py,sha256=WFduzQCUVuCDtGA-4RxNcEipZt73y7_4n9iy11YO8KE,20642
14
15
  trueconf/enums/__init__.py,sha256=YxjyW5UbyBAB8NWYqpxeSpuC6b_D7tghq0S4XWCySms,645
15
16
  trueconf/enums/aouth_error.py,sha256=Ru5oZxam-2SnhSPZyapH79IlNrU_3t1l6RxsUJ4f5b8,462
16
17
  trueconf/enums/chat_participant_role.py,sha256=2W-1nQdpvOWd8hDDgksPtykKdzewj6nwXWrvvHrQxGU,434
@@ -28,6 +29,17 @@ trueconf/filters/command.py,sha256=UWGn0yyFA5we2pRnL0BeUiJBjvN5slu_5GderrmQl8I,3
28
29
  trueconf/filters/instance_of.py,sha256=O5s2AfoGvsXdsZLdk0OnAqUss-PeUQG7-giDpiJle6s,287
29
30
  trueconf/filters/message.py,sha256=-4mSEokWo_Bqnk2K7ci3dI1niJw3X9CVFHo5xo1Qtis,480
30
31
  trueconf/filters/method.py,sha256=v2cB3N8fzKWqz1AGJaUjsswen8d58pafOt4eB0qJtgM,367
32
+ trueconf/fsm/__init__.py,sha256=ewkpwMGTf0tFRXOFGrSCEZpyh5j0EXMNfbGZ5yHEK-w,554
33
+ trueconf/fsm/context.py,sha256=Eo__H5RJi7S3l1KQBggc1evg8dPFqkTZ0LSWfmy91x8,1726
34
+ trueconf/fsm/filters.py,sha256=v8RC_uSHL9N_xyu5_qSLbMN_aGIlk8W9dh_oYVk0Lps,939
35
+ trueconf/fsm/key_builder.py,sha256=7LVrpgWxS8_5uuCW_XSep72aZJ4cB-wsZCOSBe9bcTs,1228
36
+ trueconf/fsm/manager.py,sha256=ybcdv3id4lz4cCj85ZokpLmuZIJwJ70oRpntO0-6P20,1199
37
+ trueconf/fsm/middleware.py,sha256=F5MMVg5CbsFqpegr6_hpgvZ0xU8oxLQ0QM1FdPiAHkc,663
38
+ trueconf/fsm/state.py,sha256=MmF6QMekC-3alOBU0pByA7wZ_h3_SOuut_-X74-8pyc,4538
39
+ trueconf/fsm/strategy.py,sha256=m5--QhJf_NSq8rTN4xkxp2Zx76w5D8Jb8PIkGXvVjac,951
40
+ trueconf/fsm/storage/__init__.py,sha256=cDVH7QNbdIvUFDsEg0RIyxfvmYo_haALvqsBY8apFoM,148
41
+ trueconf/fsm/storage/base.py,sha256=f5vhdNt7mSXrXkqOTKCtj3CpJ24VqryWVKzLoOKuz5c,984
42
+ trueconf/fsm/storage/memory.py,sha256=Etib3uqMXfuD6hrsYZCxSp0awsM61c2NqjzWl5sFpmg,1781
31
43
  trueconf/methods/__init__.py,sha256=fapNvuoZ16q6YUaJJjRWljoSXAOmyqq3PyUK8yyCh9Q,1911
32
44
  trueconf/methods/add_participant_to_chat.py,sha256=uT-NBkHXFDfmHbi3f_hc3HJtS_jRS69yBuIQ87HczVQ,677
33
45
  trueconf/methods/auth.py,sha256=ZEYoJpEKqUybDyqW8YJMPxA8QK2cAYXhipYr8V3nbj0,783
@@ -67,7 +79,7 @@ trueconf/types/author_box.py,sha256=G_9AxpBGIbZyWEHCfKg_DkEbte9f9FWt-JQ05gro3bU,
67
79
  trueconf/types/chat_participant.py,sha256=08sxIkiLK0IFZdHF_yApWHroNZTd-9tj9dVBOvZcI1E,328
68
80
  trueconf/types/input_file.py,sha256=mkr0dqnlTUOZjoFrOcX1_GsecqJ7trfQ1vSWquC4MYc,16806
69
81
  trueconf/types/last_message.py,sha256=87-mPWYEIi7RAYB1os7iqnPE7fvtQhZhnloMAkybLKU,1541
70
- trueconf/types/message.py,sha256=yE8sl3f9Jin9IH4ntebCddYAVyEWCLuUM8_TUTwDDpo,23228
82
+ trueconf/types/message.py,sha256=eZu4sjDJNDfDxddtMefDka_b-rZ1usiMnCGVsfdOsJI,24871
71
83
  trueconf/types/parser.py,sha256=R_xV4EjJQMX15LQwbr6KrJd94qaZZ6h2Yt7VsbNJ9O8,4814
72
84
  trueconf/types/update.py,sha256=FS4d_NKjTHWtUiB6-ShPNkOll7hYFTl71gilLPRv45g,287
73
85
  trueconf/types/content/__init__.py,sha256=9wpEMRUWc8Vj-kVY8GFSFUlmrjKhdlL2V1QlZvcC49E,645
@@ -136,10 +148,14 @@ trueconf/types/responses/unsubscribe_file_progress_response.py,sha256=_5C-LmO7Pz
136
148
  trueconf/types/responses/upload_file_response.py,sha256=CmhLmby4MVny8KmfQ1aHW6UDnpcsW_BqL9wgAitUyxM,244
137
149
  trueconf/utils/__init__.py,sha256=O1ZS6uwnlZpSH19FEodZQ2fOLyua0SJYsIgEO70UIRg,39
138
150
  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
151
+ trueconf/utils/_ssl.py,sha256=pMEGtCQtnaeo5zD9cS15Sry2hBnUkDx8I0BVov9PV1c,1419
152
+ trueconf/utils/_token.py,sha256=vbZp8hwHu59DzqMpE0MqeQ7EM55txLkKNDFV031Up6Q,2336
153
+ trueconf/utils/_version_checker.py,sha256=U1AB0KgASTCuI68r7SiLGnWeeSy3T1yFqLNg8h3fjM0,2029
141
154
  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,,
155
+ trueconf/utils/formatting/__init__.py,sha256=Ii9rEamQdy8R0pVbmDf3xbdB4TtmgYyu2-RWRTCXukw,261
156
+ trueconf/utils/formatting/decorations.py,sha256=iQzCdFBELn16llCgTTPk43K5-BzY6BGPc1M3V2Q0tPQ,2175
157
+ trueconf/utils/formatting/text.py,sha256=pOW07TIV5vfvtWmGa2i2l_Q0UjFM6QMuowmsAjE9nyo,6867
158
+ python_trueconf_bot-1.4.0.dist-info/METADATA,sha256=Zp74Ghw3JmfBFYW7OPok0WFUAOi5GFLt64RmNDk76js,5700
159
+ python_trueconf_bot-1.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
160
+ python_trueconf_bot-1.4.0.dist-info/top_level.txt,sha256=GjgYdn_osTUuU-H8f59VhIS7MY44xLV7-D7veywJaeY,9
161
+ python_trueconf_bot-1.4.0.dist-info/RECORD,,
trueconf/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from trueconf.client.bot import Bot
2
2
  from trueconf.dispatcher.dispatcher import Dispatcher
3
3
  from trueconf.dispatcher.router import Router
4
+ from trueconf.middleware import BaseMiddleware, SkipSelfMessages
4
5
  from magic_filter import F
5
6
  from trueconf.types.message import Message
6
7
  from trueconf.types import requests
@@ -11,6 +12,7 @@ __all__ = (
11
12
  "Bot",
12
13
  "Dispatcher",
13
14
  "Router",
15
+ "BaseMiddleware",
14
16
  "F",
15
17
  "Message",
16
18
  "requests",
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.4.0'
22
+ __version_tuple__ = version_tuple = (1, 4, 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,14 @@ 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
+ skip_self_messages: bool = True,
129
+ verify_ssl: SSLVerify = True,
126
130
  web_port: int | None = None,
127
131
  https: bool = True,
128
132
  ws_max_retries: int = 5,
129
133
  ws_max_delay: int = 60,
130
134
  debug: bool = False,
135
+ on_health_check: HealthCheckCallback | None = None,
131
136
  ):
132
137
  """
133
138
  Initializes a TrueConf chatbot instance with WebSocket connection and configuration options.
@@ -143,12 +148,21 @@ class Bot:
143
148
  receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
144
149
  receive_system_messages (bool, optional): Whether to receive system messages, such as user additions
145
150
  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.
151
+ skip_self_messages (bool, optional): If True, the bot will ignore messages sent by itself.
152
+ This prevents echo loops when multiple bot sessions are running. Defaults to True.
153
+ verify_ssl (bool | str | ssl.SSLContext, optional): SSL verification mode. If True, verifies
154
+ the server certificate using the system trust store when available. If False, disables
155
+ certificate verification. If a string is provided, it must be a path to a CA bundle file.
156
+ A custom ssl.SSLContext can also be passed. Defaults to True.
147
157
  web_port (int, optional): WebSocket connection port. Defaults to 443.
148
158
  https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
149
159
  ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
150
160
  ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
151
161
  debug (bool, optional): Enables debug mode. Defaults to False.
162
+ on_health_check (HealthCheckCallback | None, optional): Async callback called when the bot
163
+ connection health changes. The callback receives a dictionary with the current status,
164
+ WebSocket state, authorization state, server, port, protocol, timestamp, and optional
165
+ error details. Defaults to None.
152
166
 
153
167
  Note:
154
168
  Alternatively, you can authorize using a username and password via the `from_credentials()` class method.
@@ -161,7 +175,7 @@ class Bot:
161
175
  self.dp = dispatcher or Dispatcher()
162
176
  self.receive_unread_messages = receive_unread_messages
163
177
  self.receive_system_messages = receive_system_messages
164
- self.verify_ssl = verify_ssl
178
+ self.ssl_context = _build_ssl_context(verify_ssl)
165
179
  self.https = https
166
180
  self._ws_max_retries = ws_max_retries
167
181
  self._ws_max_delay = ws_max_delay
@@ -186,11 +200,17 @@ class Bot:
186
200
  self.max_file_size: int | None = None
187
201
  self.file_extension_filter_mode: str | None = None
188
202
  self.file_extensions_list: set | None = None
203
+ self._me_id: str
204
+ self._on_health_check = on_health_check
205
+
206
+ if skip_self_messages:
207
+ from trueconf.middleware import SkipSelfMessages
208
+ self.dp.outer_middleware(SkipSelfMessages())
189
209
 
190
210
  loggers.chatbot.info(
191
211
  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}, "
193
- f"receive_unread={receive_unread_messages}"
212
+ f"verify_ssl={_describe_ssl_context(self.ssl_context)}, ws_max_retries={ws_max_retries}, ws_max_delay={ws_max_delay}, "
213
+ f"receive_unread={receive_unread_messages}, skip_self={skip_self_messages}"
194
214
  )
195
215
 
196
216
  async def __call__(self, method: TrueConfMethod[T]) -> T:
@@ -209,7 +229,7 @@ class Bot:
209
229
 
210
230
  async def __get_domain_name(self):
211
231
  try:
212
- async with httpx.AsyncClient(verify=self.verify_ssl, timeout=5) as client:
232
+ async with httpx.AsyncClient(verify=self.ssl_context, timeout=5) as client:
213
233
  response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
214
234
  domain_name = response.json().get("product").get("display_name")
215
235
  loggers.chatbot.info(f"🌐 Server domain_name resolved: {domain_name}")
@@ -220,7 +240,7 @@ class Bot:
220
240
 
221
241
  async def __get_server_version(self):
222
242
  try:
223
- async with httpx.AsyncClient(verify=self.verify_ssl, timeout=5) as client:
243
+ async with httpx.AsyncClient(verify=self.ssl_context, timeout=5) as client:
224
244
  response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
225
245
  version = response.json().get("product").get("version")
226
246
  loggers.chatbot.info(f"📦 Server version resolved: {version}")
@@ -266,8 +286,12 @@ class Bot:
266
286
  current_version = await self.server_version
267
287
  _VersionChecker.check(current_version)
268
288
 
289
+ @property
290
+ def me_id(self) -> str:
291
+ return self._me_id
292
+
269
293
  @async_cached_property
270
- async def me(self) -> str:
294
+ async def me_chat(self) -> str:
271
295
  """
272
296
  Returns the identifier of the bot's personal (Favorites) chat.
273
297
 
@@ -281,6 +305,20 @@ class Bot:
281
305
  r = await self.create_favorites_chat()
282
306
  return r.chat_id
283
307
 
308
+ @async_cached_property
309
+ @deprecated("bot.me is deprecated, use bot.me_chat instead", stacklevel=2)
310
+ async def me(self) -> str:
311
+ """
312
+ Returns the identifier of the bot's personal (Favorites) chat.
313
+
314
+ If the chat does not exist yet, it will be created automatically.
315
+ The result is cached to prevent redundant API calls.
316
+
317
+ Returns:
318
+ str: Chat ID of the bot's personal Favorites chat.
319
+ """
320
+ return await self.me_chat
321
+
284
322
  @classmethod
285
323
  def from_credentials(
286
324
  cls,
@@ -291,11 +329,14 @@ class Bot:
291
329
  dispatcher: Dispatcher | None = None,
292
330
  receive_unread_messages: bool = False,
293
331
  receive_system_messages: bool = False,
294
- verify_ssl: bool = True,
332
+ skip_self_messages: bool = True,
333
+ verify_ssl: SSLVerify = True,
295
334
  web_port: int | None = None,
296
335
  https: bool = True,
297
336
  ws_max_retries: int = 5,
298
337
  ws_max_delay: int = 60,
338
+ debug: bool = False,
339
+ on_health_check: HealthCheckCallback | None = None,
299
340
  ) -> Self:
300
341
  """
301
342
  Creates a bot instance using username and password authentication.
@@ -312,11 +353,16 @@ class Bot:
312
353
  receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
313
354
  receive_system_messages (bool, optional): Whether to receive system messages, such as user additions
314
355
  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.
356
+ skip_self_messages (bool, optional): If True, the bot will ignore messages sent by itself.
357
+ Defaults to True.
358
+ verify_ssl (bool | str | ssl.SSLContext, optional): SSL verification mode. Defaults to True.
316
359
  web_port (int, optional): WebSocket connection port. Defaults to 443.
317
360
  https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
318
361
  ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
319
362
  ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
363
+ debug (bool, optional): Enables debug mode. Defaults to False.
364
+ on_health_check (HealthCheckCallback | None, optional): Async callback called when the bot
365
+ connection health changes. Defaults to None.
320
366
 
321
367
  Returns:
322
368
  Bot: An authorized bot instance.
@@ -326,7 +372,8 @@ class Bot:
326
372
  """
327
373
 
328
374
  loggers.chatbot.info(f"🔑 Obtaining auth token for user={username} @ {server}")
329
- token = _get_auth_token(server, username, password, verify=verify_ssl)
375
+ ssl_context = _build_ssl_context(verify_ssl)
376
+ token = _get_auth_token(server, username, password, ssl_context=ssl_context)
330
377
  if not token:
331
378
  loggers.chatbot.error(f"❌ Failed to obtain token for user={username} @ {server}")
332
379
  raise RuntimeError("Failed to obtain token")
@@ -337,10 +384,13 @@ class Bot:
337
384
  https=https,
338
385
  dispatcher=dispatcher,
339
386
  receive_unread_messages=receive_unread_messages,
340
- receive_system_messages = receive_system_messages,
341
- verify_ssl=verify_ssl,
387
+ receive_system_messages=receive_system_messages,
388
+ skip_self_messages=skip_self_messages,
389
+ verify_ssl=ssl_context,
342
390
  ws_max_delay=ws_max_delay,
343
- ws_max_retries=ws_max_retries
391
+ ws_max_retries=ws_max_retries,
392
+ debug=debug,
393
+ on_health_check=on_health_check,
344
394
  )
345
395
 
346
396
  async def __wait_upload_complete(
@@ -402,7 +452,7 @@ class Bot:
402
452
  if dest_path:
403
453
  loggers.chatbot.info(f"⬇️ Destination: {dest_path}")
404
454
  try:
405
- async with httpx.AsyncClient(verify=self.verify_ssl, timeout=httpx.Timeout(timeout)) as client:
455
+ async with httpx.AsyncClient(verify=self.ssl_context, timeout=httpx.Timeout(timeout)) as client:
406
456
  async with client.stream("GET", url) as resp:
407
457
  resp.raise_for_status()
408
458
 
@@ -477,13 +527,7 @@ class Bot:
477
527
  "Upload-Task-Id": upload_task_id,
478
528
  }
479
529
 
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()
530
+ connector = TCPConnector(ssl=self.ssl_context)
487
531
 
488
532
  try:
489
533
  async with ClientSession(connector=connector, timeout=ClientTimeout(total=timeout)) as session:
@@ -532,10 +576,7 @@ class Bot:
532
576
 
533
577
  if self.https:
534
578
  ws_protocol = "wss"
535
- if self.verify_ssl:
536
- ssl_context = ssl.create_default_context()
537
- else:
538
- ssl_context = ssl._create_unverified_context()
579
+ ssl_context = self.ssl_context
539
580
  else:
540
581
  ws_protocol = "ws"
541
582
 
@@ -546,7 +587,7 @@ class Bot:
546
587
  loggers.chatbot.info("⏳ Attempting WebSocket connection...")
547
588
  async with websockets.connect(
548
589
  uri=f"{ws_protocol}://{self.server}:{self.port}/websocket/chat_bot",
549
- ssl=ssl_context,
590
+ ssl = ssl_context,
550
591
  ping_interval=30,
551
592
  ping_timeout=10
552
593
  ) as ws:
@@ -561,14 +602,34 @@ class Bot:
561
602
 
562
603
  self.connected_event.set()
563
604
  self.authorized_event.clear()
605
+ await self._call_health_check({
606
+ "status": "connected",
607
+ "websocket_connected": True,
608
+ "authorized": False,
609
+ "server": self.server,
610
+ "port": self.port,
611
+ "protocol": self._protocol,
612
+ "timestamp": datetime.now(timezone.utc).isoformat(),
613
+ })
564
614
 
565
615
  try:
566
- await self.__authorize()
616
+ user_id = await self.__authorize()
617
+ self.authorized_event.set()
618
+
619
+ await self._call_health_check({
620
+ "status": "authorized",
621
+ "websocket_connected": True,
622
+ "authorized": True,
623
+ "user_id": user_id,
624
+ "server": self.server,
625
+ "port": self.port,
626
+ "protocol": self._protocol,
627
+ "timestamp": datetime.now(timezone.utc).isoformat(),
628
+ })
567
629
  except ApiErrorException as e:
568
630
  loggers.chatbot.error(f"❌ Authorization failed: {e}")
569
631
  raise
570
632
 
571
- self.authorized_event.set()
572
633
  await ws.wait_closed()
573
634
 
574
635
  except asyncio.CancelledError:
@@ -576,6 +637,7 @@ class Bot:
576
637
  raise
577
638
 
578
639
  except (websockets.exceptions.ConnectionClosed, websockets.exceptions.InvalidStatus, OSError) as e:
640
+ was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
579
641
  self.connected_event.clear()
580
642
  self.authorized_event.clear()
581
643
 
@@ -587,6 +649,18 @@ class Bot:
587
649
  else:
588
650
  reason = str(e)
589
651
 
652
+ if was_connected:
653
+ await self._call_health_check({
654
+ "status": "disconnected",
655
+ "websocket_connected": False,
656
+ "authorized": False,
657
+ "error": reason,
658
+ "server": self.server,
659
+ "port": self.port,
660
+ "protocol": self._protocol,
661
+ "timestamp": datetime.now(timezone.utc).isoformat(),
662
+ })
663
+
590
664
  delay = min(delay * 2, self._ws_max_delay) + random.uniform(0, 1)
591
665
  msg = f"🔌 Connection issue: {reason}. Retrying in {delay:.1f}s..."
592
666
  loggers.chatbot.warning(msg)
@@ -594,8 +668,22 @@ class Bot:
594
668
  continue
595
669
 
596
670
  except websockets.exceptions.InvalidURI as e:
671
+ was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
597
672
  retry_count += 1
598
673
  self.connected_event.clear()
674
+ self.authorized_event.clear()
675
+
676
+ if was_connected:
677
+ await self._call_health_check({
678
+ "status": "disconnected",
679
+ "websocket_connected": False,
680
+ "authorized": False,
681
+ "error": str(e),
682
+ "server": self.server,
683
+ "port": self.port,
684
+ "protocol": self._protocol,
685
+ "timestamp": datetime.now(timezone.utc).isoformat(),
686
+ })
599
687
 
600
688
  if retry_count > self._ws_max_retries:
601
689
  loggers.chatbot.error(f"❌ Critical network error: {e}. Check your URI/IP. Giving up.")
@@ -608,8 +696,21 @@ class Bot:
608
696
  continue
609
697
 
610
698
  finally:
699
+ was_connected = self.connected_event.is_set() or self.authorized_event.is_set()
611
700
  self.connected_event.clear()
612
701
  self.authorized_event.clear()
702
+
703
+ if was_connected:
704
+ await self._call_health_check({
705
+ "status": "disconnected",
706
+ "websocket_connected": False,
707
+ "authorized": False,
708
+ "server": self.server,
709
+ "port": self.port,
710
+ "protocol": self._protocol,
711
+ "timestamp": datetime.now(timezone.utc).isoformat(),
712
+ })
713
+
613
714
  if self._session:
614
715
  with contextlib.suppress(Exception):
615
716
  await self._session.detach()
@@ -625,7 +726,7 @@ class Bot:
625
726
  if future and not future.done():
626
727
  future.set_result(message)
627
728
 
628
- async def __authorize(self):
729
+ async def __authorize(self) -> str:
629
730
  loggers.chatbot.info("🚀 Starting authorization")
630
731
 
631
732
  call = AuthMethod(
@@ -635,7 +736,9 @@ class Bot:
635
736
  )
636
737
  loggers.chatbot.info(f"🛠 Created AuthMethod with id={call.id}")
637
738
  result = await self(call)
739
+ self._me_id = result.user_id.split("/", maxsplit=1)[0]
638
740
  loggers.chatbot.info(f"🔐 Authenticated as {result.user_id}")
741
+ return result.user_id
639
742
 
640
743
  async def __process_message(self, data: dict):
641
744
  data = parse_update(data)
@@ -661,7 +764,7 @@ class Bot:
661
764
  if hasattr(payload, "bind"):
662
765
  payload.bind(self)
663
766
 
664
- await self.dp._feed_update(data)
767
+ await self.dp._feed_update(data, {"bot": self})
665
768
 
666
769
  async def __on_raw_message(self, raw: str):
667
770
  try:
@@ -703,6 +806,59 @@ class Bot:
703
806
  )
704
807
  loggers.chatbot.info(f"✅ File passed limitations check: {file.file_name}")
705
808
 
809
+ async def _call_health_check(self, status: Dict[str, Any]) -> None:
810
+ """
811
+ Calls the registered health-check callback with the provided status payload.
812
+
813
+ The callback is optional. If it is not registered, the method returns immediately.
814
+ Exceptions raised inside the callback are caught and logged so that user-defined
815
+ monitoring logic cannot interrupt the bot connection loop.
816
+
817
+ Args:
818
+ status (Dict[str, Any]): Current bot health status payload. Common fields include
819
+ ``status``, ``websocket_connected``, ``authorized``, ``server``, ``port``,
820
+ ``protocol``, ``timestamp``, and optionally ``user_id`` or ``error``.
821
+
822
+ Returns:
823
+ None
824
+ """
825
+ if self._on_health_check is None:
826
+ return
827
+
828
+ try:
829
+ await self._on_health_check(status)
830
+ except Exception as e:
831
+ loggers.chatbot.error(f"Health check callback error: {e}")
832
+
833
+ def health_check(self) -> Dict[str, Any]:
834
+ """
835
+ Returns the current bot health status.
836
+
837
+ This method can be used by external monitoring systems, HTTP health endpoints,
838
+ or application code that needs to check the current WebSocket and authorization
839
+ state without waiting for a callback event.
840
+
841
+ Returns:
842
+ Dict[str, Any]: Dictionary containing the current status, WebSocket state,
843
+ authorization state, server, port, protocol, and UTC timestamp.
844
+ """
845
+ return {
846
+ "status": (
847
+ "authorized"
848
+ if self.authorized_event.is_set()
849
+ else "connected"
850
+ if self.connected_event.is_set()
851
+ else "disconnected"
852
+ ),
853
+ "websocket_connected": self.connected_event.is_set(),
854
+ "authorized": self.authorized_event.is_set(),
855
+ "server": self.server,
856
+ "port": self.port,
857
+ "protocol": self._protocol,
858
+ "timestamp": datetime.now(timezone.utc).isoformat(),
859
+ }
860
+
861
+
706
862
  async def add_participant_to_chat(
707
863
  self,
708
864
  chat_id: str,
@@ -1477,7 +1633,7 @@ class Bot:
1477
1633
  pass
1478
1634
 
1479
1635
  await self.start()
1480
- loggers.chatbot.info(f"⏸️ Bot running, waiting for shutdown signal")
1636
+ loggers.chatbot.info("⏸️ Bot running, waiting for shutdown signal")
1481
1637
  if self._connect_task:
1482
1638
  try:
1483
1639
  await self._connect_task