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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-trueconf-bot
3
- Version: 1.2.0b1
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.0b1.dist-info/licenses/LICENSE,sha256=ftDuEzhr-yY_dsfpEQ2SKAP_dEAO-iPt96MBZq_K48o,1684
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=e-_7z50FAHqdTfPkm-_QSv76KW6AuWHjM_bkQ956deE,528
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=gP6Dm1gmP-NO1poAyPLmuSiz_OhqdTbuH4mSlFGbAhY,58022
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=LCxlTxsBm-AZ0mtTyVtt_siWZ1J-HBPkL0opStcuyls,651
56
- trueconf/methods/send_file.py,sha256=7HXD0S3s4EReAxzbbvobYh4X7cuYwdemufXosmXbj6E,932
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.0b1.dist-info/METADATA,sha256=Q-n7e6YNo-uBlEpFp57E5kfryhWH2jumxrzot3Rp2zg,5126
132
- python_trueconf_bot-1.2.0b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
133
- python_trueconf_bot-1.2.0b1.dist-info/top_level.txt,sha256=GjgYdn_osTUuU-H8f59VhIS7MY44xLV7-D7veywJaeY,9
134
- python_trueconf_bot-1.2.0b1.dist-info/RECORD,,
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.0b1'
22
- __version_tuple__ = version_tuple = (1, 2, 0, 'b1')
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
- web_port: int = 443,
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
- receive_system_messages: bool = False,
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.web_port = web_port
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
- self.port = 443 if self.https else self.web_port
154
- self.receive_unread_messages = receive_unread_messages
155
- self.receive_system_messages = receive_system_messages
156
- self._url_for_upload_files = (
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(url)
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
- **token_opts: Unpack[TokenOpts],
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
- **token_opts: Additional options passed to the token request, such as `web_port` and `https`.
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=server,
259
- token=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
- web_port=token_opts.get("web_port", 443),
265
- https=token_opts.get("https", True),
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 by URL and save it to disk.
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
- Path | None: Path to saved file, or None on error.
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
- else:
332
- dest = Path(dest_path) / file_name
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
- async with aiofiles.open(dest, "wb") as f:
336
- async for chunk in resp.aiter_bytes(chunk_size):
337
- await f.write(chunk)
338
- return None
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
- with contextlib.suppress(Exception):
342
- if dest.exists():
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: int = 60,
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 (int, optional): Upload timeout in seconds. Defaults to 60.
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(self._url_for_upload_files, headers=headers, data=data) as response:
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
- uri = f"wss://{self.server}:{self.web_port}/websocket/chat_bot" if self.https else f"ws://{self.server}:{self.web_port}/websocket/chat_bot"
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(uri, ssl=ssl_context, ping_interval=30, ping_timeout=10) as ws:
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
- await asyncio.sleep(0.5)
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 already in the READY state, it will be downloaded immediately.
752
- If the file is in the NOT_AVAILABLE state, the method will exit without downloading.
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: Path to the downloaded file, or None if the download failed.
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
- ok = await self.__wait_upload_complete(file_id, expected_size=info.size, timeout=None)
779
- if not ok:
780
- loggers.chatbot.error(f"Wait upload complete failed for {file_id}")
781
- return None
782
-
783
- for _ in range(20):
784
- info = await self.get_file_info(file_id)
785
- if info.ready_state == FileReadyState.READY:
786
- break
787
- await asyncio.sleep(1)
788
- else:
789
- loggers.chatbot.warning(f"File {file_id} didn’t reach READY in time")
790
- return None
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, chat_id: str, user_id: str
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
  }
@@ -20,14 +20,20 @@ class SendFile(TrueConfMethod[SendFileResponse]):
20
20
  super().__init__()
21
21
 
22
22
  def payload(self):
23
- return {
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