python-trueconf-bot 1.2.0b1__py3-none-any.whl → 1.2.0b2__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.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/METADATA +18 -1
- {python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/RECORD +9 -9
- trueconf/_version.py +2 -2
- trueconf/client/bot.py +179 -102
- trueconf/methods/remove_participant_from_chat.py +3 -1
- trueconf/methods/send_file.py +13 -7
- {python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/WHEEL +0 -0
- {python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/licenses/LICENSE +0 -0
- {python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.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.2.
|
|
3
|
+
Version: 1.2.0b2
|
|
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
|
|
@@ -77,6 +77,23 @@ Dynamic: license-file
|
|
|
77
77
|
>
|
|
78
78
|
> We were inspired by the popular [aiogram](https://github.com/aiogram/aiogram/) library, so, the transition will be **simple** for developers already familiar with this library.
|
|
79
79
|
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 📦 Version Compatibility
|
|
83
|
+
|
|
84
|
+
| TrueConf Server Version | Required Library Version |
|
|
85
|
+
|:------------------------|:-------------------------|
|
|
86
|
+
| < 5.5.0 | ❌ Not supported |
|
|
87
|
+
| 5.5.0 — 5.5.2 | v1.1.x |
|
|
88
|
+
| >= 5.5.3 | **v1.2.0** (or higher) |
|
|
89
|
+
|
|
90
|
+
> ⚠️ IMPORTANT
|
|
91
|
+
>
|
|
92
|
+
> If you are using server version **5.5.3** or higher, you must upgrade the library. If the stable release is not yet available, install the latest beta:
|
|
93
|
+
> ```bash
|
|
94
|
+
> uv pip install --pre "python-trueconf-bot>=1.2.0b0"
|
|
95
|
+
> ```
|
|
96
|
+
|
|
80
97
|
## 📌 Key Features
|
|
81
98
|
|
|
82
99
|
* Easy integration with the [TrueConf Server Chatbot API](https://trueconf.com/docs/chatbot-connector/en/overview/)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
python_trueconf_bot-1.2.
|
|
1
|
+
python_trueconf_bot-1.2.0b2.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=
|
|
3
|
+
trueconf/_version.py,sha256=ZjrumRj9rT0zIxBYOWkwYCDiswIhQcklxX-StaUjgb0,528
|
|
4
4
|
trueconf/exceptions.py,sha256=dtY54xAdatBxmQlUy3moGpeMVz2NOKHIKPIOrX7MyHQ,500
|
|
5
5
|
trueconf/loggers.py,sha256=dOAwgGLPcrmD6aZI5jA3J9eMunbbOAvPRguk6zWMcNo,125
|
|
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=Baoz3F1mgmhYxM_-WYLuo9YIMXThV1I5IHvgvTXMuOU,61401
|
|
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
|
|
@@ -52,8 +52,8 @@ trueconf/methods/get_user_display_name.py,sha256=36-vbHICcofZYpLgurSFoE8IOP51yWJ
|
|
|
52
52
|
trueconf/methods/has_chat_participant.py,sha256=BsDV_9GfLLBDuA-pmQdFXykOcXf3PLFSqgxhlDC1nV4,598
|
|
53
53
|
trueconf/methods/remove_chat.py,sha256=sEPVf645wOey6U5M66-nTwsWvYcOmAHjFAz7TzXsO4A,497
|
|
54
54
|
trueconf/methods/remove_message.py,sha256=mWhpF5INT6LHxnAyIqTKpinKB_8whj-H8EXXzuMpJHY,612
|
|
55
|
-
trueconf/methods/remove_participant_from_chat.py,sha256=
|
|
56
|
-
trueconf/methods/send_file.py,sha256=
|
|
55
|
+
trueconf/methods/remove_participant_from_chat.py,sha256=KwBUD52Hulh2FqDIkdcZ-wRknzDxxxw2aTO5MvbpE_4,723
|
|
56
|
+
trueconf/methods/send_file.py,sha256=6BM1zdPWkKVFFT_gTCqHZUk0qHowz3VkJrYlRCPGKT0,1018
|
|
57
57
|
trueconf/methods/send_message.py,sha256=MYjKUQvqD3li_QyuyTQnC3YOg5050gE2qXxDVc_pvuk,816
|
|
58
58
|
trueconf/methods/send_survey.py,sha256=HI6gCICQofkPyfZGsd2idX1ZK5-txvCHa6VZgFaOBPY,1275
|
|
59
59
|
trueconf/methods/subscribe_file_progress.py,sha256=3zVqz13Z9KNcPMm13fcX6VpRXKxiIm0-b8x_C_BDxTg,598
|
|
@@ -128,7 +128,7 @@ trueconf/types/responses/upload_file_response.py,sha256=CmhLmby4MVny8KmfQ1aHW6UD
|
|
|
128
128
|
trueconf/utils/__init__.py,sha256=6CrEznKfUTDKL0IH-CNJJOOW-Vx22aokR7tmHORyUUU,135
|
|
129
129
|
trueconf/utils/generate_secret_for_survey.py,sha256=_rJttL9lsY3zeVxAzV5iSnHLRFslIBvYzj_qn03xMh8,241
|
|
130
130
|
trueconf/utils/token.py,sha256=WZWiYH3mQsltPoKgF9szVb1R-rxPZYqs3-GPS1WXb0Q,2305
|
|
131
|
-
python_trueconf_bot-1.2.
|
|
132
|
-
python_trueconf_bot-1.2.
|
|
133
|
-
python_trueconf_bot-1.2.
|
|
134
|
-
python_trueconf_bot-1.2.
|
|
131
|
+
python_trueconf_bot-1.2.0b2.dist-info/METADATA,sha256=I7si5S2Jqumkhr1LGlvC1ychLv0aKZoXCZbj0omTlSY,5688
|
|
132
|
+
python_trueconf_bot-1.2.0b2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
133
|
+
python_trueconf_bot-1.2.0b2.dist-info/top_level.txt,sha256=GjgYdn_osTUuU-H8f59VhIS7MY44xLV7-D7veywJaeY,9
|
|
134
|
+
python_trueconf_bot-1.2.0b2.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.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 2, 0, '
|
|
21
|
+
__version__ = version = '1.2.0b2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0, 'b2')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
trueconf/client/bot.py
CHANGED
|
@@ -5,9 +5,9 @@ import httpx
|
|
|
5
5
|
import json
|
|
6
6
|
import signal
|
|
7
7
|
import ssl
|
|
8
|
-
import tempfile
|
|
9
8
|
import websockets
|
|
10
9
|
import warnings
|
|
10
|
+
import random
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from aiohttp import ClientSession, ClientTimeout, TCPConnector, FormData
|
|
13
13
|
from async_property import async_cached_property
|
|
@@ -97,23 +97,20 @@ from trueconf.utils import validate_token
|
|
|
97
97
|
|
|
98
98
|
T = TypeVar("T")
|
|
99
99
|
|
|
100
|
-
class TokenOpts(TypedDict, total=False):
|
|
101
|
-
web_port: int
|
|
102
|
-
https: bool
|
|
103
|
-
|
|
104
|
-
|
|
105
100
|
class Bot:
|
|
106
101
|
def __init__(
|
|
107
102
|
self,
|
|
108
103
|
server: str,
|
|
109
104
|
token: str,
|
|
110
|
-
|
|
111
|
-
https: bool = True,
|
|
112
|
-
debug: bool = False,
|
|
113
|
-
verify_ssl: bool = True,
|
|
105
|
+
*,
|
|
114
106
|
dispatcher: Dispatcher | None = None,
|
|
115
107
|
receive_unread_messages: bool = False,
|
|
116
|
-
|
|
108
|
+
verify_ssl: bool = True,
|
|
109
|
+
web_port: int | None = None,
|
|
110
|
+
https: bool = True,
|
|
111
|
+
ws_max_retries: int = 5,
|
|
112
|
+
ws_max_delay: int = 60,
|
|
113
|
+
debug: bool = False,
|
|
117
114
|
):
|
|
118
115
|
"""
|
|
119
116
|
Initializes a TrueConf chatbot instance with WebSocket connection and configuration options.
|
|
@@ -124,13 +121,15 @@ class Bot:
|
|
|
124
121
|
Args:
|
|
125
122
|
server (str): Address of the TrueConf server.
|
|
126
123
|
token (str): Bot authorization token.
|
|
124
|
+
* : All following arguments must be passed by name (keyword-only).
|
|
125
|
+
dispatcher (Dispatcher | None, optional): Dispatcher instance for registering handlers.
|
|
126
|
+
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
127
|
+
verify_ssl (bool, optional): Whether to verify the server's SSL certificate. Defaults to True.
|
|
127
128
|
web_port (int, optional): WebSocket connection port. Defaults to 443.
|
|
128
129
|
https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
|
|
130
|
+
ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
|
|
131
|
+
ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
|
|
129
132
|
debug (bool, optional): Enables debug mode. Defaults to False.
|
|
130
|
-
verify_ssl (bool, optional): Whether to verify the server's SSL certificate. Defaults to True.
|
|
131
|
-
dispatcher (Dispatcher | None, optional): Dispatcher instance for registering handlers.
|
|
132
|
-
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
133
|
-
receive_system_messages (bool, optional): Whether to receive system messages on connection. Defaults to False.
|
|
134
133
|
|
|
135
134
|
Note:
|
|
136
135
|
Alternatively, you can authorize using a username and password via the `from_credentials()` class method.
|
|
@@ -140,28 +139,27 @@ class Bot:
|
|
|
140
139
|
|
|
141
140
|
self.server = server
|
|
142
141
|
self.__token = token
|
|
143
|
-
self.
|
|
142
|
+
self.dp = dispatcher or Dispatcher()
|
|
143
|
+
self.receive_unread_messages = receive_unread_messages
|
|
144
|
+
self.verify_ssl = verify_ssl
|
|
144
145
|
self.https = https
|
|
146
|
+
self._ws_max_retries = ws_max_retries
|
|
147
|
+
self._ws_max_delay = ws_max_delay
|
|
145
148
|
self.debug = debug
|
|
149
|
+
|
|
146
150
|
self.connected_event = asyncio.Event()
|
|
147
151
|
self.authorized_event = asyncio.Event()
|
|
148
152
|
self._session: WebSocketSession | None = None
|
|
149
153
|
self._connect_task: asyncio.Task | None = None
|
|
150
154
|
self.stopped_event = asyncio.Event()
|
|
151
|
-
self.dp = dispatcher or Dispatcher()
|
|
152
155
|
self._protocol = "https" if self.https else "http"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
f"{self._protocol}://{self.server}:{self.port}/bridge/api/client/v1/files"
|
|
158
|
-
)
|
|
159
|
-
self.verify_ssl = verify_ssl
|
|
156
|
+
if web_port is None:
|
|
157
|
+
self.port = 443 if self.https else 4309
|
|
158
|
+
else:
|
|
159
|
+
self.port = web_port
|
|
160
160
|
self._progress_queues: Dict[str, asyncio.Queue] = {}
|
|
161
|
-
|
|
162
161
|
self._futures: Dict[int, asyncio.Future] = {}
|
|
163
162
|
self._handlers: List[Tuple[dict, Callable[[dict], Awaitable]]] = []
|
|
164
|
-
|
|
165
163
|
self._stop = False
|
|
166
164
|
self._ws = None
|
|
167
165
|
|
|
@@ -169,16 +167,23 @@ class Bot:
|
|
|
169
167
|
return await method(self)
|
|
170
168
|
|
|
171
169
|
async def __get_domain_name(self):
|
|
172
|
-
url = f"{self._protocol}://{self.server}:{self.port}/api/v4/server"
|
|
173
|
-
|
|
174
170
|
try:
|
|
175
171
|
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=5) as client:
|
|
176
|
-
response = await client.get(
|
|
172
|
+
response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
|
|
177
173
|
return response.json().get("product").get("display_name")
|
|
178
174
|
except Exception as e:
|
|
179
175
|
loggers.chatbot.error(f"Failed to get server domain_name: {e}")
|
|
180
176
|
return None
|
|
181
177
|
|
|
178
|
+
async def __get_server_version(self):
|
|
179
|
+
try:
|
|
180
|
+
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=5) as client:
|
|
181
|
+
response = await client.get(f"{self._protocol}://{self.server}:{self.port}/api/v4/server")
|
|
182
|
+
return response.json().get("product").get("version")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
loggers.chatbot.error(f"Failed to get server version: {e}")
|
|
185
|
+
return None
|
|
186
|
+
|
|
182
187
|
@property
|
|
183
188
|
def token(self) -> str:
|
|
184
189
|
"""
|
|
@@ -201,6 +206,34 @@ class Bot:
|
|
|
201
206
|
|
|
202
207
|
return await self.__get_domain_name()
|
|
203
208
|
|
|
209
|
+
@async_cached_property
|
|
210
|
+
async def server_version(self) -> str:
|
|
211
|
+
"""
|
|
212
|
+
Returns the domain name of the TrueConf server.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
str: Domain name of the connected server.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
return await self.__get_server_version()
|
|
219
|
+
|
|
220
|
+
async def check_version(self):
|
|
221
|
+
current_version = await self.server_version
|
|
222
|
+
v = tuple(map(int, current_version.split('.')[:3]))
|
|
223
|
+
|
|
224
|
+
if v < (5, 5, 0):
|
|
225
|
+
raise RuntimeError(
|
|
226
|
+
f"Error: Server version {current_version} is too old. "
|
|
227
|
+
"Chatbots are not supported (version 5.5.0+ required)."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if (5, 5, 0) <= v <= (5, 5, 2):
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
f"\n[!] Server version {current_version} is incompatible with the current library.\n"
|
|
233
|
+
"Please install version v1.1.x using the following command:\n"
|
|
234
|
+
'uv pip install "python-trueconf-bot>=1.1.0,<1.2.0"'
|
|
235
|
+
)
|
|
236
|
+
|
|
204
237
|
@async_cached_property
|
|
205
238
|
async def me(self) -> str:
|
|
206
239
|
"""
|
|
@@ -222,11 +255,14 @@ class Bot:
|
|
|
222
255
|
server: str,
|
|
223
256
|
username: str,
|
|
224
257
|
password: str,
|
|
258
|
+
*,
|
|
225
259
|
dispatcher: Dispatcher | None = None,
|
|
226
260
|
receive_unread_messages: bool = False,
|
|
227
|
-
receive_system_messages: bool = False,
|
|
228
261
|
verify_ssl: bool = True,
|
|
229
|
-
|
|
262
|
+
web_port: int | None = None,
|
|
263
|
+
https: bool = True,
|
|
264
|
+
ws_max_retries: int = 5,
|
|
265
|
+
ws_max_delay: int = 60,
|
|
230
266
|
) -> Self:
|
|
231
267
|
"""
|
|
232
268
|
Creates a bot instance using username and password authentication.
|
|
@@ -238,11 +274,14 @@ class Bot:
|
|
|
238
274
|
server (str): Address of the TrueConf server.
|
|
239
275
|
username (str): Username for authentication.
|
|
240
276
|
password (str): Password for authentication.
|
|
277
|
+
* : All following arguments must be passed by name (keyword-only).
|
|
241
278
|
dispatcher (Dispatcher | None, optional): Dispatcher instance for registering handlers.
|
|
242
279
|
receive_unread_messages (bool, optional): Whether to receive unread messages on connection. Defaults to False.
|
|
243
|
-
receive_system_messages (bool, optional): Whether to receive system messages on connection. Defaults to False.
|
|
244
280
|
verify_ssl (bool, optional): Whether to verify the server's SSL certificate. Defaults to True.
|
|
245
|
-
|
|
281
|
+
web_port (int, optional): WebSocket connection port. Defaults to 443.
|
|
282
|
+
https (bool, optional): Whether to use HTTPS protocol. Defaults to True.
|
|
283
|
+
ws_max_retries (int, optional): Max connection attempts on network/IP errors before giving up. Defaults to 5.
|
|
284
|
+
ws_max_delay (int, optional): Maximum delay between reconnection attempts (in seconds). Defaults to 60.
|
|
246
285
|
|
|
247
286
|
Returns:
|
|
248
287
|
Bot: An authorized bot instance.
|
|
@@ -255,14 +294,15 @@ class Bot:
|
|
|
255
294
|
if not token:
|
|
256
295
|
raise RuntimeError("Failed to obtain token")
|
|
257
296
|
return cls(
|
|
258
|
-
server
|
|
259
|
-
token
|
|
297
|
+
server,
|
|
298
|
+
token,
|
|
299
|
+
web_port=web_port,
|
|
300
|
+
https=https,
|
|
260
301
|
dispatcher=dispatcher,
|
|
261
302
|
receive_unread_messages=receive_unread_messages,
|
|
262
|
-
receive_system_messages=receive_system_messages,
|
|
263
303
|
verify_ssl=verify_ssl,
|
|
264
|
-
|
|
265
|
-
|
|
304
|
+
ws_max_delay=ws_max_delay,
|
|
305
|
+
ws_max_retries=ws_max_retries
|
|
266
306
|
)
|
|
267
307
|
|
|
268
308
|
async def __wait_upload_complete(
|
|
@@ -300,46 +340,48 @@ class Bot:
|
|
|
300
340
|
dest_path: str | Path | None = None,
|
|
301
341
|
timeout: int = 60,
|
|
302
342
|
chunk_size: int = 64 * 1024,
|
|
303
|
-
) -> bytes | None:
|
|
343
|
+
) -> bytes | Path | None:
|
|
304
344
|
|
|
305
345
|
"""
|
|
306
|
-
Asynchronously download file
|
|
307
|
-
|
|
308
|
-
If `dest_path` isn't provided, a temporary file will be created (similar to aiogram.File.download()).
|
|
309
|
-
|
|
310
|
-
Args:
|
|
311
|
-
url: Direct download URL.
|
|
312
|
-
dest_path: Destination path; if None, a NamedTemporaryFile will be created and returned.
|
|
313
|
-
timeout: Request timeout (seconds).
|
|
314
|
-
chunk_size: Stream chunk size in bytes.
|
|
346
|
+
Asynchronously download a file from the server by URL.
|
|
315
347
|
|
|
348
|
+
Args:
|
|
349
|
+
url (str): Direct download URL.
|
|
350
|
+
file_name (str): Name of the file to be saved.
|
|
351
|
+
dest_path (str | Path | None): Destination path on disk.
|
|
352
|
+
If None, the file will be downloaded into memory. Defaults to None.
|
|
353
|
+
timeout (int): Request timeout in seconds. Defaults to 60.
|
|
354
|
+
chunk_size (int): Stream chunk size in bytes. Defaults to 65536 (64 KB).
|
|
316
355
|
|
|
317
356
|
Returns:
|
|
318
|
-
|
|
357
|
+
bytes | Path | None:
|
|
358
|
+
- bytes: if `dest_path` is None (file in memory).
|
|
359
|
+
- Path: if the file was successfully saved to disk.
|
|
360
|
+
- None: if an error occurred during download or saving.
|
|
319
361
|
"""
|
|
320
|
-
|
|
362
|
+
dest = None
|
|
321
363
|
try:
|
|
322
364
|
async with httpx.AsyncClient(verify=self.verify_ssl, timeout=httpx.Timeout(timeout)) as client:
|
|
323
365
|
async with client.stream("GET", url) as resp:
|
|
324
366
|
resp.raise_for_status()
|
|
367
|
+
|
|
325
368
|
if dest_path is None:
|
|
326
369
|
chunks = []
|
|
327
370
|
async for chunk in resp.aiter_bytes(chunk_size):
|
|
328
371
|
chunks.append(chunk)
|
|
329
372
|
return b"".join(chunks)
|
|
330
373
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
dest = Path(dest_path) / file_name
|
|
375
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
334
376
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
377
|
+
async with aiofiles.open(dest, "wb") as f:
|
|
378
|
+
async for chunk in resp.aiter_bytes(chunk_size):
|
|
379
|
+
await f.write(chunk)
|
|
380
|
+
return dest
|
|
339
381
|
except Exception as e:
|
|
340
382
|
loggers.chatbot.error(f"Failed to download file from {url}: {e}")
|
|
341
|
-
|
|
342
|
-
|
|
383
|
+
if dest and dest.exists():
|
|
384
|
+
with contextlib.suppress(Exception):
|
|
343
385
|
dest.unlink()
|
|
344
386
|
return None
|
|
345
387
|
|
|
@@ -348,7 +390,7 @@ class Bot:
|
|
|
348
390
|
file: InputFile,
|
|
349
391
|
preview: InputFile | None = None,
|
|
350
392
|
is_sticker: bool = False,
|
|
351
|
-
timeout:
|
|
393
|
+
timeout: float = 60.0,
|
|
352
394
|
) -> str:
|
|
353
395
|
"""
|
|
354
396
|
Uploads a file to the server and returns a temporary file identifier (temporalFileId).
|
|
@@ -371,7 +413,7 @@ class Bot:
|
|
|
371
413
|
file (InputFile): The primary file to upload.
|
|
372
414
|
preview (InputFile | None, optional): Optional preview file (default is None).
|
|
373
415
|
is_sticker (bool, optional): Whether the uploaded file is a sticker (affects MIME type). Defaults to False.
|
|
374
|
-
timeout (
|
|
416
|
+
timeout (float, optional): Upload timeout in seconds. Defaults to 60.
|
|
375
417
|
|
|
376
418
|
Returns:
|
|
377
419
|
str | None: A temporary file ID (`temporalFileId`) on success, or None if the upload failed.
|
|
@@ -390,8 +432,6 @@ class Bot:
|
|
|
390
432
|
|
|
391
433
|
}
|
|
392
434
|
|
|
393
|
-
timeout = ClientTimeout(total=timeout)
|
|
394
|
-
|
|
395
435
|
if not self.verify_ssl:
|
|
396
436
|
ssl_context = ssl.create_default_context()
|
|
397
437
|
ssl_context.check_hostname = False
|
|
@@ -401,7 +441,7 @@ class Bot:
|
|
|
401
441
|
connector = TCPConnector()
|
|
402
442
|
|
|
403
443
|
try:
|
|
404
|
-
async with ClientSession(connector=connector, timeout=timeout) as session:
|
|
444
|
+
async with ClientSession(connector=connector, timeout=ClientTimeout(total=timeout)) as session:
|
|
405
445
|
data = FormData(quote_fields=False)
|
|
406
446
|
data.add_field(
|
|
407
447
|
name="file",
|
|
@@ -418,7 +458,11 @@ class Bot:
|
|
|
418
458
|
content_type=preview.mime_type
|
|
419
459
|
)
|
|
420
460
|
|
|
421
|
-
async with session.post(
|
|
461
|
+
async with session.post(
|
|
462
|
+
url=f"{self._protocol}://{self.server}:{self.port}/bridge/api/client/v1/files",
|
|
463
|
+
headers=headers,
|
|
464
|
+
data=data
|
|
465
|
+
) as response:
|
|
422
466
|
result = await response.json()
|
|
423
467
|
return result.get("temporalFileId")
|
|
424
468
|
except Exception as e:
|
|
@@ -438,18 +482,31 @@ class Bot:
|
|
|
438
482
|
|
|
439
483
|
async def __connect_and_listen(self):
|
|
440
484
|
ssl_context = None
|
|
485
|
+
delay = 1
|
|
486
|
+
retry_count = 0
|
|
487
|
+
|
|
441
488
|
if self.https:
|
|
489
|
+
ws_protocol = "wss"
|
|
442
490
|
if self.verify_ssl:
|
|
443
491
|
ssl_context = ssl.create_default_context()
|
|
444
492
|
else:
|
|
445
493
|
ssl_context = ssl._create_unverified_context()
|
|
446
|
-
|
|
447
|
-
|
|
494
|
+
else:
|
|
495
|
+
ws_protocol = "ws"
|
|
448
496
|
|
|
449
497
|
while not self._stop:
|
|
450
498
|
try:
|
|
499
|
+
if delay > 1 or retry_count > 0:
|
|
500
|
+
await asyncio.sleep(delay)
|
|
451
501
|
loggers.chatbot.info("⏳ Attempting WebSocket connection...")
|
|
452
|
-
async with websockets.connect(
|
|
502
|
+
async with websockets.connect(
|
|
503
|
+
uri=f"{ws_protocol}://{self.server}:{self.port}/websocket/chat_bot",
|
|
504
|
+
ssl=ssl_context,
|
|
505
|
+
ping_interval=30,
|
|
506
|
+
ping_timeout=10
|
|
507
|
+
) as ws:
|
|
508
|
+
delay = 1
|
|
509
|
+
retry_count = 0
|
|
453
510
|
self._ws = ws
|
|
454
511
|
loggers.chatbot.info("✅ WebSocket connected")
|
|
455
512
|
|
|
@@ -473,12 +530,36 @@ class Bot:
|
|
|
473
530
|
loggers.chatbot.info("🛑 Connect loop cancelled, performing cleanup.")
|
|
474
531
|
raise
|
|
475
532
|
|
|
476
|
-
except websockets.exceptions.ConnectionClosed as e:
|
|
477
|
-
loggers.chatbot.warning(
|
|
478
|
-
f"🔌 Connection closed: {getattr(e, 'code', '?')} – {getattr(e, 'reason', '?')}. Retrying...")
|
|
533
|
+
except (websockets.exceptions.ConnectionClosed, websockets.exceptions.InvalidStatus, OSError) as e:
|
|
479
534
|
self.connected_event.clear()
|
|
480
535
|
self.authorized_event.clear()
|
|
481
|
-
|
|
536
|
+
|
|
537
|
+
if isinstance(e, websockets.exceptions.ConnectionClosed):
|
|
538
|
+
close_info = e.rcvd or e.sent
|
|
539
|
+
code = close_info.code if close_info else "?"
|
|
540
|
+
reason = close_info.reason if close_info else "no reason"
|
|
541
|
+
reason = f"Code: {code} ({reason})"
|
|
542
|
+
else:
|
|
543
|
+
reason = str(e)
|
|
544
|
+
|
|
545
|
+
delay = min(delay * 2, self._ws_max_delay) + random.uniform(0, 1)
|
|
546
|
+
msg = f"🔌 Connection issue: {reason}. Retrying in {delay:.1f}s..."
|
|
547
|
+
loggers.chatbot.warning(msg)
|
|
548
|
+
print(msg)
|
|
549
|
+
continue
|
|
550
|
+
|
|
551
|
+
except websockets.exceptions.InvalidURI as e:
|
|
552
|
+
retry_count += 1
|
|
553
|
+
self.connected_event.clear()
|
|
554
|
+
|
|
555
|
+
if retry_count > self._ws_max_retries:
|
|
556
|
+
loggers.chatbot.error(f"❌ Critical network error: {e}. Check your URI/IP. Giving up.")
|
|
557
|
+
raise ConnectionError(f"Failed to connect after {self._ws_max_retries} attempts: {e}") from e
|
|
558
|
+
|
|
559
|
+
delay = 5
|
|
560
|
+
msg = f"⚠️ Cant reach server: {e}. Attempt {retry_count}/{self._ws_max_retries}. Retrying in {delay}s..."
|
|
561
|
+
loggers.chatbot.warning(msg)
|
|
562
|
+
print(msg)
|
|
482
563
|
continue
|
|
483
564
|
|
|
484
565
|
finally:
|
|
@@ -744,50 +825,41 @@ class Bot:
|
|
|
744
825
|
call = RemoveChat(chat_id=chat_id)
|
|
745
826
|
return await self(call)
|
|
746
827
|
|
|
747
|
-
async def download_file_by_id(self, file_id, dest_path: str = None) -> Path | None:
|
|
828
|
+
async def download_file_by_id(self, file_id, dest_path: str | Path | None = None) -> bytes | Path | None:
|
|
748
829
|
"""
|
|
749
830
|
Downloads a file by its ID, waiting for the upload to complete if necessary.
|
|
750
831
|
|
|
751
|
-
If the file is
|
|
752
|
-
If
|
|
753
|
-
In other cases, the bot will wait for the upload to finish and then attempt to download the file.
|
|
832
|
+
If `dest_path` is provided, the file is saved to disk and the Path is returned.
|
|
833
|
+
If `dest_path` is None, the file content is returned as bytes.
|
|
754
834
|
|
|
755
835
|
Args:
|
|
756
836
|
file_id (str): Unique identifier of the file on the server.
|
|
757
|
-
dest_path (str, optional): Path where the file should be saved.
|
|
758
|
-
If not specified, a temporary file will be created using `NamedTemporaryFile`
|
|
759
|
-
(with prefix `tc_dl_`, suffix set to the original file name, and `delete=False` to keep the file on disk).
|
|
837
|
+
dest_path (str | Path, optional): Path where the file should be saved.
|
|
760
838
|
|
|
761
839
|
Returns:
|
|
762
|
-
Path | None:
|
|
840
|
+
bytes | Path | None: File content (bytes), path to file (Path), or None if failed.
|
|
763
841
|
"""
|
|
764
842
|
|
|
765
843
|
info = await self.get_file_info(file_id)
|
|
766
844
|
|
|
767
|
-
if info.ready_state == FileReadyState.READY:
|
|
768
|
-
return await self.__download_file_from_server(
|
|
769
|
-
url=info.download_url,
|
|
770
|
-
file_name=info.name,
|
|
771
|
-
dest_path=dest_path,
|
|
772
|
-
)
|
|
773
|
-
|
|
774
845
|
if info.ready_state == FileReadyState.NOT_AVAILABLE:
|
|
775
846
|
loggers.chatbot.warning(f"File {file_id} is NOT_AVAILABLE")
|
|
776
847
|
return None
|
|
777
848
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
849
|
+
if info.ready_state != FileReadyState.READY:
|
|
850
|
+
ok = await self.__wait_upload_complete(file_id, expected_size=info.size, timeout=None)
|
|
851
|
+
if not ok:
|
|
852
|
+
loggers.chatbot.error(f"Wait upload complete failed for {file_id}")
|
|
853
|
+
return None
|
|
854
|
+
|
|
855
|
+
for _ in range(20):
|
|
856
|
+
info = await self.get_file_info(file_id)
|
|
857
|
+
if info.ready_state == FileReadyState.READY:
|
|
858
|
+
break
|
|
859
|
+
await asyncio.sleep(1)
|
|
860
|
+
else:
|
|
861
|
+
loggers.chatbot.warning(f"File {file_id} didn’t reach READY in time")
|
|
862
|
+
return None
|
|
791
863
|
|
|
792
864
|
return await self.__download_file_from_server(
|
|
793
865
|
url=info.download_url,
|
|
@@ -1133,7 +1205,10 @@ class Bot:
|
|
|
1133
1205
|
return await self(call)
|
|
1134
1206
|
|
|
1135
1207
|
async def remove_participant_from_chat(
|
|
1136
|
-
self,
|
|
1208
|
+
self,
|
|
1209
|
+
chat_id: str,
|
|
1210
|
+
user_id: str,
|
|
1211
|
+
clear_history: bool = False
|
|
1137
1212
|
) -> RemoveChatParticipantResponse:
|
|
1138
1213
|
"""
|
|
1139
1214
|
Removes a participant from the specified chat.
|
|
@@ -1144,6 +1219,7 @@ class Bot:
|
|
|
1144
1219
|
Args:
|
|
1145
1220
|
chat_id (str): Identifier of the chat to remove the participant from.
|
|
1146
1221
|
user_id (str): Identifier of the user to be removed.
|
|
1222
|
+
clear_history (bool, optional): If True, the chat history will be cleared for the removed participant. Defaults to False.
|
|
1147
1223
|
|
|
1148
1224
|
Returns:
|
|
1149
1225
|
RemoveChatParticipantResponse: Object containing the result of the participant removal.
|
|
@@ -1152,7 +1228,7 @@ class Bot:
|
|
|
1152
1228
|
if "@" not in user_id:
|
|
1153
1229
|
user_id = f"{user_id}@{await self.server_name}"
|
|
1154
1230
|
|
|
1155
|
-
call = RemoveChatParticipant(chat_id=chat_id, user_id=user_id)
|
|
1231
|
+
call = RemoveChatParticipant(chat_id=chat_id, user_id=user_id, clear_history=clear_history)
|
|
1156
1232
|
return await self(call)
|
|
1157
1233
|
|
|
1158
1234
|
async def reply_message(
|
|
@@ -1476,6 +1552,7 @@ class Bot:
|
|
|
1476
1552
|
Returns:
|
|
1477
1553
|
None
|
|
1478
1554
|
"""
|
|
1555
|
+
await self.check_version()
|
|
1479
1556
|
if self._connect_task and not self._connect_task.done():
|
|
1480
1557
|
return
|
|
1481
1558
|
self._stop = False
|
|
@@ -12,6 +12,7 @@ class RemoveChatParticipant(TrueConfMethod[RemoveChatParticipantResponse]):
|
|
|
12
12
|
|
|
13
13
|
chat_id: str
|
|
14
14
|
user_id: str
|
|
15
|
+
clear_history: bool
|
|
15
16
|
|
|
16
17
|
def __post_init__(self):
|
|
17
18
|
super().__init__()
|
|
@@ -19,5 +20,6 @@ class RemoveChatParticipant(TrueConfMethod[RemoveChatParticipantResponse]):
|
|
|
19
20
|
def payload(self):
|
|
20
21
|
return {
|
|
21
22
|
"chatId": self.chat_id,
|
|
22
|
-
"userId": self.user_id
|
|
23
|
+
"userId": self.user_id,
|
|
24
|
+
"clearHistory": self.clear_history
|
|
23
25
|
}
|
trueconf/methods/send_file.py
CHANGED
|
@@ -20,14 +20,20 @@ class SendFile(TrueConfMethod[SendFileResponse]):
|
|
|
20
20
|
super().__init__()
|
|
21
21
|
|
|
22
22
|
def payload(self):
|
|
23
|
-
|
|
23
|
+
data = {
|
|
24
24
|
"chatId": self.chat_id,
|
|
25
|
-
"replyMessageId": self.reply_message_id,
|
|
26
25
|
"content": {
|
|
27
|
-
"temporalFileId": self.temporal_file_id
|
|
28
|
-
"caption":{
|
|
29
|
-
"text": self.text,
|
|
30
|
-
"parseMode": self.parse_mode,
|
|
31
|
-
}
|
|
26
|
+
"temporalFileId": self.temporal_file_id
|
|
32
27
|
}
|
|
33
28
|
}
|
|
29
|
+
|
|
30
|
+
if self.reply_message_id:
|
|
31
|
+
data["replyMessageId"] = self.reply_message_id
|
|
32
|
+
|
|
33
|
+
if self.text:
|
|
34
|
+
data["content"]["caption"] = {
|
|
35
|
+
"text": self.text,
|
|
36
|
+
"parseMode": self.parse_mode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return data
|
|
File without changes
|
{python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{python_trueconf_bot-1.2.0b1.dist-info → python_trueconf_bot-1.2.0b2.dist-info}/top_level.txt
RENAMED
|
File without changes
|