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.
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/METADATA +2 -1
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/RECORD +30 -14
- trueconf/__init__.py +2 -0
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +193 -37
- trueconf/dispatcher/dispatcher.py +126 -40
- trueconf/dispatcher/router.py +156 -38
- trueconf/exceptions.py +107 -10
- trueconf/fsm/__init__.py +20 -0
- trueconf/fsm/context.py +57 -0
- trueconf/fsm/filters.py +31 -0
- trueconf/fsm/key_builder.py +39 -0
- trueconf/fsm/manager.py +36 -0
- trueconf/fsm/middleware.py +24 -0
- trueconf/fsm/state.py +135 -0
- trueconf/fsm/storage/__init__.py +4 -0
- trueconf/fsm/storage/base.py +36 -0
- trueconf/fsm/storage/memory.py +57 -0
- trueconf/fsm/strategy.py +36 -0
- trueconf/middleware.py +78 -0
- 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.4.0.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.3.dev1.dist-info → python_trueconf_bot-1.4.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.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
|
-
trueconf/__init__.py,sha256=
|
|
3
|
-
trueconf/_version.py,sha256=
|
|
4
|
-
trueconf/exceptions.py,sha256=
|
|
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=
|
|
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=
|
|
13
|
-
trueconf/dispatcher/router.py,sha256=
|
|
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=
|
|
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/
|
|
140
|
-
trueconf/utils/
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
python_trueconf_bot-1.
|
|
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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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={
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
341
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|