maxapi-python 1.1.20__py3-none-any.whl → 1.2.1__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.
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/METADATA +44 -54
- maxapi_python-1.2.1.dist-info/RECORD +32 -0
- pymax/core.py +79 -156
- pymax/crud.py +3 -7
- pymax/filters.py +158 -41
- pymax/formatter.py +1 -0
- pymax/formatting.py +4 -6
- pymax/interfaces.py +148 -8
- pymax/mixins/__init__.py +3 -0
- pymax/mixins/auth.py +229 -30
- pymax/mixins/channel.py +36 -37
- pymax/mixins/group.py +127 -8
- pymax/mixins/handler.py +163 -39
- pymax/mixins/message.py +251 -97
- pymax/mixins/scheduler.py +28 -0
- pymax/mixins/self.py +79 -40
- pymax/mixins/socket.py +254 -281
- pymax/mixins/user.py +63 -42
- pymax/mixins/websocket.py +145 -145
- pymax/payloads.py +12 -0
- pymax/static/constant.py +4 -2
- pymax/static/enum.py +5 -0
- maxapi_python-1.1.20.dist-info/RECORD +0 -31
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/WHEEL +0 -0
- {maxapi_python-1.1.20.dist-info → maxapi_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/message.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import time
|
|
3
3
|
from http import HTTPStatus
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import aiohttp
|
|
6
|
-
from
|
|
7
|
+
from aiofiles import open as aio_open
|
|
8
|
+
from aiohttp import ClientSession, TCPConnector
|
|
7
9
|
|
|
8
10
|
from pymax.exceptions import Error
|
|
9
11
|
from pymax.files import File, Photo, Video
|
|
@@ -37,15 +39,17 @@ from pymax.types import (
|
|
|
37
39
|
FileRequest,
|
|
38
40
|
Message,
|
|
39
41
|
ReactionInfo,
|
|
40
|
-
VideoAttach,
|
|
41
42
|
VideoRequest,
|
|
42
43
|
)
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
class MessageMixin(ClientProtocol):
|
|
47
|
+
CHUNK_SIZE = 6 * 1024 * 1024
|
|
48
|
+
|
|
46
49
|
async def _upload_file(self, file: File) -> None | Attach:
|
|
47
50
|
try:
|
|
48
51
|
self.logger.info("Uploading file")
|
|
52
|
+
|
|
49
53
|
payload = UploadPayload().model_dump(by_alias=True)
|
|
50
54
|
data = await self._send_and_wait(
|
|
51
55
|
opcode=Opcode.FILE_UPLOAD,
|
|
@@ -60,46 +64,118 @@ class MessageMixin(ClientProtocol):
|
|
|
60
64
|
self.logger.error("No upload URL or file ID received")
|
|
61
65
|
return None
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
self.logger.debug("Got upload URL and file_id=%s", file_id)
|
|
68
|
+
|
|
69
|
+
if file.path:
|
|
70
|
+
file_size = Path(file.path).stat().st_size
|
|
71
|
+
self.logger.info(
|
|
72
|
+
"File size from path: %.2f MB", file_size / (1024 * 1024)
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
file_bytes = await file.read()
|
|
76
|
+
file_size = len(file_bytes)
|
|
77
|
+
self.logger.info(
|
|
78
|
+
"File size from URL: %.2f MB", file_size / (1024 * 1024)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
connector = TCPConnector(limit=0)
|
|
82
|
+
timeout = aiohttp.ClientTimeout(total=None, sock_read=None, sock_connect=30)
|
|
64
83
|
|
|
65
84
|
headers = {
|
|
66
85
|
"Content-Disposition": f"attachment; filename={file.file_name}",
|
|
67
|
-
"Content-
|
|
86
|
+
"Content-Length": str(file_size),
|
|
87
|
+
"Content-Range": f"0-{file_size - 1}/{file_size}",
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
loop = asyncio.get_running_loop()
|
|
71
91
|
fut: asyncio.Future[dict] = loop.create_future()
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
self._file_upload_waiters[int(file_id)] = fut
|
|
93
|
+
|
|
94
|
+
async def file_generator():
|
|
95
|
+
bytes_sent = 0
|
|
96
|
+
chunk_num = 0
|
|
97
|
+
self.logger.debug("Starting file streaming from: %s", file.path)
|
|
98
|
+
async with aio_open(file.path, "rb") as f:
|
|
99
|
+
while True:
|
|
100
|
+
chunk = await f.read(self.CHUNK_SIZE)
|
|
101
|
+
if not chunk:
|
|
102
|
+
self.logger.info(
|
|
103
|
+
"File streaming complete: %d bytes in %d chunks",
|
|
104
|
+
bytes_sent,
|
|
105
|
+
chunk_num,
|
|
106
|
+
)
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
yield chunk
|
|
110
|
+
|
|
111
|
+
bytes_sent += len(chunk)
|
|
112
|
+
chunk_num += 1
|
|
113
|
+
if chunk_num % 10 == 0:
|
|
114
|
+
self.logger.info(
|
|
115
|
+
"Upload progress: %.1f MB in %d chunks",
|
|
116
|
+
bytes_sent / (1024 * 1024),
|
|
117
|
+
chunk_num,
|
|
118
|
+
)
|
|
119
|
+
if chunk_num % 4 == 0:
|
|
120
|
+
await asyncio.sleep(0)
|
|
121
|
+
|
|
122
|
+
async def bytes_generator(b: bytes):
|
|
123
|
+
bytes_sent = 0
|
|
124
|
+
chunk_num = 0
|
|
125
|
+
for i in range(0, len(b), self.CHUNK_SIZE):
|
|
126
|
+
chunk = b[i : i + self.CHUNK_SIZE]
|
|
127
|
+
yield chunk
|
|
128
|
+
bytes_sent += len(chunk)
|
|
129
|
+
chunk_num += 1
|
|
130
|
+
if chunk_num % 10 == 0:
|
|
131
|
+
self.logger.info(
|
|
132
|
+
"Upload progress: %.1f MB in %d chunks",
|
|
133
|
+
bytes_sent / (1024 * 1024),
|
|
134
|
+
chunk_num,
|
|
135
|
+
)
|
|
136
|
+
if chunk_num % 4 == 0:
|
|
137
|
+
await asyncio.sleep(0)
|
|
138
|
+
|
|
139
|
+
if file.path:
|
|
140
|
+
data_to_send = file_generator()
|
|
141
|
+
else:
|
|
142
|
+
data_to_send = bytes_generator(file_bytes)
|
|
143
|
+
|
|
144
|
+
self.logger.info("Starting file upload: %s", file.file_name)
|
|
76
145
|
|
|
77
146
|
async with (
|
|
78
|
-
ClientSession() as session,
|
|
79
|
-
session.post(
|
|
80
|
-
url=url,
|
|
81
|
-
headers=headers,
|
|
82
|
-
data=file_bytes,
|
|
83
|
-
) as response,
|
|
147
|
+
ClientSession(connector=connector, timeout=timeout) as session,
|
|
148
|
+
session.post(url=url, headers=headers, data=data_to_send) as response,
|
|
84
149
|
):
|
|
150
|
+
self.logger.debug("Server response status: %d", response.status)
|
|
85
151
|
if response.status != HTTPStatus.OK:
|
|
86
|
-
self.logger.error(
|
|
152
|
+
self.logger.error("Upload failed with status %s", response.status)
|
|
87
153
|
self._file_upload_waiters.pop(int(file_id), None)
|
|
88
154
|
return None
|
|
89
155
|
|
|
156
|
+
self.logger.debug(
|
|
157
|
+
"File sent successfully. Waiting for server confirmation "
|
|
158
|
+
"(timeout=%d seconds, fileId=%s)",
|
|
159
|
+
DEFAULT_TIMEOUT,
|
|
160
|
+
file_id,
|
|
161
|
+
)
|
|
90
162
|
try:
|
|
91
163
|
await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
|
|
164
|
+
self.logger.info(
|
|
165
|
+
"File upload completed successfully (fileId=%s)", file_id
|
|
166
|
+
)
|
|
92
167
|
return Attach(_type=AttachType.FILE, file_id=file_id)
|
|
93
168
|
except asyncio.TimeoutError:
|
|
94
|
-
self.logger.
|
|
169
|
+
self.logger.warning(
|
|
95
170
|
"Timed out waiting for file processing notification for fileId=%s",
|
|
96
171
|
file_id,
|
|
97
172
|
)
|
|
98
173
|
self._file_upload_waiters.pop(int(file_id), None)
|
|
99
174
|
return None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
175
|
+
|
|
176
|
+
except Exception:
|
|
177
|
+
self.logger.exception("Upload file failed")
|
|
178
|
+
raise
|
|
103
179
|
|
|
104
180
|
async def _upload_video(self, video: Video) -> None | Attach:
|
|
105
181
|
try:
|
|
@@ -127,10 +203,19 @@ class MessageMixin(ClientProtocol):
|
|
|
127
203
|
return None
|
|
128
204
|
|
|
129
205
|
file_bytes = await video.read()
|
|
206
|
+
file_size = len(file_bytes)
|
|
207
|
+
|
|
208
|
+
# Настройки для ClientSession
|
|
209
|
+
connector = TCPConnector(limit=0)
|
|
210
|
+
timeout = aiohttp.ClientTimeout(
|
|
211
|
+
total=900, sock_read=60
|
|
212
|
+
) # 15 минут на видео
|
|
130
213
|
|
|
131
214
|
headers = {
|
|
132
215
|
"Content-Disposition": f"attachment; filename={video.file_name}",
|
|
133
|
-
"Content-Range": f"0-{
|
|
216
|
+
"Content-Range": f"0-{file_size - 1}/{file_size}",
|
|
217
|
+
"Content-Length": str(file_size),
|
|
218
|
+
"Connection": "keep-alive",
|
|
134
219
|
}
|
|
135
220
|
|
|
136
221
|
loop = asyncio.get_running_loop()
|
|
@@ -140,35 +225,45 @@ class MessageMixin(ClientProtocol):
|
|
|
140
225
|
except Exception:
|
|
141
226
|
self.logger.exception("Failed to register file upload waiter")
|
|
142
227
|
|
|
143
|
-
|
|
144
|
-
ClientSession(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
228
|
+
try:
|
|
229
|
+
async with ClientSession(
|
|
230
|
+
connector=connector, timeout=timeout
|
|
231
|
+
) as session:
|
|
232
|
+
async with session.post(
|
|
233
|
+
url=url,
|
|
234
|
+
headers=headers,
|
|
235
|
+
data=file_bytes,
|
|
236
|
+
) as response:
|
|
237
|
+
if response.status != HTTPStatus.OK:
|
|
238
|
+
self.logger.error(
|
|
239
|
+
"Upload failed with status %s", response.status
|
|
240
|
+
)
|
|
241
|
+
self._file_upload_waiters.pop(int(video_id), None)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
|
|
246
|
+
return Attach(
|
|
247
|
+
_type=AttachType.VIDEO, video_id=video_id, token=token
|
|
248
|
+
)
|
|
249
|
+
except asyncio.TimeoutError:
|
|
250
|
+
self.logger.warning(
|
|
251
|
+
"Timed out waiting for video processing notification for videoId=%s",
|
|
252
|
+
video_id,
|
|
253
|
+
)
|
|
254
|
+
self._file_upload_waiters.pop(int(video_id), None)
|
|
255
|
+
return None
|
|
256
|
+
except OSError as e:
|
|
257
|
+
if "malloc failure" in str(e) or "BUF" in str(e):
|
|
258
|
+
self.logger.exception(
|
|
259
|
+
"Memory error during video upload. File too large or insufficient memory. Try uploading smaller files or free up memory."
|
|
165
260
|
)
|
|
166
261
|
self._file_upload_waiters.pop(int(video_id), None)
|
|
167
|
-
|
|
262
|
+
raise
|
|
168
263
|
|
|
169
|
-
except Exception
|
|
170
|
-
self.logger.exception("Upload video failed
|
|
171
|
-
raise
|
|
264
|
+
except Exception:
|
|
265
|
+
self.logger.exception("Upload video failed")
|
|
266
|
+
raise
|
|
172
267
|
|
|
173
268
|
async def _upload_photo(self, photo: Photo) -> None | Attach:
|
|
174
269
|
try:
|
|
@@ -265,7 +360,25 @@ class MessageMixin(ClientProtocol):
|
|
|
265
360
|
use_queue: bool = False,
|
|
266
361
|
) -> Message | None:
|
|
267
362
|
"""
|
|
268
|
-
Отправляет сообщение в
|
|
363
|
+
Отправляет текстовое сообщение в чат с опциональными вложениями.
|
|
364
|
+
|
|
365
|
+
:param text: Текст сообщения.
|
|
366
|
+
:type text: str
|
|
367
|
+
:param chat_id: Идентификатор чата, в который отправляется сообщение.
|
|
368
|
+
:type chat_id: int
|
|
369
|
+
:param notify: Флаг оповещения о новом сообщении. По умолчанию True.
|
|
370
|
+
:type notify: bool
|
|
371
|
+
:param attachment: Одно вложение (фото, файл или видео).
|
|
372
|
+
:type attachment: Photo | File | Video | None
|
|
373
|
+
:param attachments: Список множественных вложений.
|
|
374
|
+
:type attachments: list[Photo | File | Video] | None
|
|
375
|
+
:param reply_to: Идентификатор сообщения для ответа.
|
|
376
|
+
:type reply_to: int | None
|
|
377
|
+
:param use_queue: Использовать очередь для отправки. По умолчанию False.
|
|
378
|
+
:type use_queue: bool
|
|
379
|
+
:return: Объект сообщения или None, если используется очередь.
|
|
380
|
+
:rtype: Message | None
|
|
381
|
+
:raises Error: Если загрузка вложения или отправка сообщения не удалась.
|
|
269
382
|
"""
|
|
270
383
|
|
|
271
384
|
self.logger.info("Sending message to chat_id=%s notify=%s", chat_id, notify)
|
|
@@ -301,9 +414,9 @@ class MessageMixin(ClientProtocol):
|
|
|
301
414
|
|
|
302
415
|
elements = []
|
|
303
416
|
clean_text = None
|
|
304
|
-
raw_elements = Formatting.get_elements_from_markdown(text)
|
|
417
|
+
raw_elements, parsed_text = Formatting.get_elements_from_markdown(text)
|
|
305
418
|
if raw_elements:
|
|
306
|
-
clean_text =
|
|
419
|
+
clean_text = parsed_text
|
|
307
420
|
elements = [
|
|
308
421
|
MessageElement(type=e.type, length=e.length, from_=e.from_)
|
|
309
422
|
for e in raw_elements
|
|
@@ -349,6 +462,25 @@ class MessageMixin(ClientProtocol):
|
|
|
349
462
|
attachments: list[Photo | Video | File] | None = None,
|
|
350
463
|
use_queue: bool = False,
|
|
351
464
|
) -> Message | None:
|
|
465
|
+
"""
|
|
466
|
+
Редактирует текст и/или вложения существующего сообщения.
|
|
467
|
+
|
|
468
|
+
:param chat_id: Идентификатор чата.
|
|
469
|
+
:type chat_id: int
|
|
470
|
+
:param message_id: Идентификатор сообщения для редактирования.
|
|
471
|
+
:type message_id: int
|
|
472
|
+
:param text: Новый текст сообщения.
|
|
473
|
+
:type text: str
|
|
474
|
+
:param attachment: Новое вложение (фото, файл или видео).
|
|
475
|
+
:type attachment: Photo | File | Video | None
|
|
476
|
+
:param attachments: Список новых множественных вложений.
|
|
477
|
+
:type attachments: list[Photo | Video | File] | None
|
|
478
|
+
:param use_queue: Использовать очередь для отправки.
|
|
479
|
+
:type use_queue: bool
|
|
480
|
+
:return: Отредактированное сообщение или None.
|
|
481
|
+
:rtype: Message | None
|
|
482
|
+
:raises Error: Если редактирование не удалось.
|
|
483
|
+
"""
|
|
352
484
|
self.logger.info(
|
|
353
485
|
"Editing message chat_id=%s message_id=%s", chat_id, message_id
|
|
354
486
|
)
|
|
@@ -428,7 +560,18 @@ class MessageMixin(ClientProtocol):
|
|
|
428
560
|
use_queue: bool = False,
|
|
429
561
|
) -> bool:
|
|
430
562
|
"""
|
|
431
|
-
Удаляет
|
|
563
|
+
Удаляет одно или несколько сообщений.
|
|
564
|
+
|
|
565
|
+
:param chat_id: Идентификатор чата.
|
|
566
|
+
:type chat_id: int
|
|
567
|
+
:param message_ids: Список идентификаторов сообщений для удаления.
|
|
568
|
+
:type message_ids: list[int]
|
|
569
|
+
:param for_me: Удалить только для себя (не видимо другим).
|
|
570
|
+
:type for_me: bool
|
|
571
|
+
:param use_queue: Использовать очередь для отправки.
|
|
572
|
+
:type use_queue: bool
|
|
573
|
+
:return: True, если сообщения успешно удалены.
|
|
574
|
+
:rtype: bool
|
|
432
575
|
"""
|
|
433
576
|
self.logger.info(
|
|
434
577
|
"Deleting messages chat_id=%s ids=%s for_me=%s",
|
|
@@ -458,15 +601,16 @@ class MessageMixin(ClientProtocol):
|
|
|
458
601
|
self, chat_id: int, message_id: int, notify_pin: bool
|
|
459
602
|
) -> bool:
|
|
460
603
|
"""
|
|
461
|
-
Закрепляет
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
604
|
+
Закрепляет сообщение в чате.
|
|
605
|
+
|
|
606
|
+
:param chat_id: Идентификатор чата.
|
|
607
|
+
:type chat_id: int
|
|
608
|
+
:param message_id: Идентификатор сообщения.
|
|
609
|
+
:type message_id: int
|
|
610
|
+
:param notify_pin: Отправить уведомление о закреплении.
|
|
611
|
+
:type notify_pin: bool
|
|
612
|
+
:return: True, если сообщение успешно закреплено.
|
|
613
|
+
:rtype: bool
|
|
470
614
|
"""
|
|
471
615
|
payload = PinMessagePayload(
|
|
472
616
|
chat_id=chat_id,
|
|
@@ -490,7 +634,18 @@ class MessageMixin(ClientProtocol):
|
|
|
490
634
|
backward: int = 200,
|
|
491
635
|
) -> list[Message] | None:
|
|
492
636
|
"""
|
|
493
|
-
Получает историю сообщений чата.
|
|
637
|
+
Получает историю сообщений из чата.
|
|
638
|
+
|
|
639
|
+
:param chat_id: Идентификатор чата.
|
|
640
|
+
:type chat_id: int
|
|
641
|
+
:param from_time: Временная метка для начала выборки.
|
|
642
|
+
:type from_time: int | None
|
|
643
|
+
:param forward: Кол-во сообщений вперед от from_time.
|
|
644
|
+
:type forward: int
|
|
645
|
+
:param backward: Кол-во сообщений назад от from_time.
|
|
646
|
+
:type backward: int
|
|
647
|
+
:return: Список сообщений или None.
|
|
648
|
+
:rtype: list[Message] | None
|
|
494
649
|
"""
|
|
495
650
|
if from_time is None:
|
|
496
651
|
from_time = int(time.time() * 1000)
|
|
@@ -534,15 +689,14 @@ class MessageMixin(ClientProtocol):
|
|
|
534
689
|
"""
|
|
535
690
|
Получает видео
|
|
536
691
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
url (str): Ссылка на видео
|
|
692
|
+
:param chat_id: ID чата
|
|
693
|
+
:type chat_id: int
|
|
694
|
+
:param message_id: ID сообщения
|
|
695
|
+
:type message_id: int
|
|
696
|
+
:param video_id: ID видео
|
|
697
|
+
:type video_id: int
|
|
698
|
+
:return: Объект VideoRequest или None
|
|
699
|
+
:rtype: VideoRequest | None
|
|
546
700
|
"""
|
|
547
701
|
self.logger.info("Getting video_id=%s message_id=%s", video_id, message_id)
|
|
548
702
|
|
|
@@ -578,14 +732,14 @@ class MessageMixin(ClientProtocol):
|
|
|
578
732
|
"""
|
|
579
733
|
Получает файл
|
|
580
734
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
735
|
+
:param chat_id: ID чата
|
|
736
|
+
:type chat_id: int
|
|
737
|
+
:param message_id: ID сообщения
|
|
738
|
+
:type message_id: int
|
|
739
|
+
:param file_id: ID файла
|
|
740
|
+
:type file_id: int
|
|
741
|
+
:return: Объект FileRequest или None
|
|
742
|
+
:rtype: FileRequest | None
|
|
589
743
|
"""
|
|
590
744
|
self.logger.info("Getting file_id=%s message_id=%s", file_id, message_id)
|
|
591
745
|
if self.is_connected and self._socket is not None:
|
|
@@ -620,13 +774,14 @@ class MessageMixin(ClientProtocol):
|
|
|
620
774
|
"""
|
|
621
775
|
Добавляет реакцию к сообщению.
|
|
622
776
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
777
|
+
:param chat_id: ID чата
|
|
778
|
+
:type chat_id: int
|
|
779
|
+
:param message_id: ID сообщения
|
|
780
|
+
:type message_id: str
|
|
781
|
+
:param reaction: Реакция для добавления
|
|
782
|
+
:type reaction: str (emoji)
|
|
783
|
+
:return: Объект ReactionInfo или None
|
|
784
|
+
:rtype: ReactionInfo | None
|
|
630
785
|
"""
|
|
631
786
|
try:
|
|
632
787
|
self.logger.info(
|
|
@@ -665,13 +820,12 @@ class MessageMixin(ClientProtocol):
|
|
|
665
820
|
"""
|
|
666
821
|
Получает реакции на сообщения.
|
|
667
822
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
о реакциях или None при ошибке.
|
|
823
|
+
:param chat_id: ID чата
|
|
824
|
+
:type chat_id: int
|
|
825
|
+
:param message_ids: Список ID сообщений
|
|
826
|
+
:type message_ids: list[str]
|
|
827
|
+
:return: Словарь с ID сообщений и соответствующими реакциями
|
|
828
|
+
:rtype: dict[str, ReactionInfo] | None
|
|
675
829
|
"""
|
|
676
830
|
self.logger.info(
|
|
677
831
|
"Getting reactions for messages chat_id=%s message_ids=%s",
|
|
@@ -707,12 +861,12 @@ class MessageMixin(ClientProtocol):
|
|
|
707
861
|
"""
|
|
708
862
|
Удаляет реакцию с сообщения.
|
|
709
863
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
864
|
+
:param chat_id: ID чата
|
|
865
|
+
:type chat_id: int
|
|
866
|
+
:param message_id: ID сообщения
|
|
867
|
+
:type message_id: str
|
|
868
|
+
:return: Объект ReactionInfo или None
|
|
869
|
+
:rtype: ReactionInfo | None
|
|
716
870
|
"""
|
|
717
871
|
self.logger.info(
|
|
718
872
|
"Removing reaction from message chat_id=%s message_id=%s",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import traceback
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pymax.interfaces import ClientProtocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SchedulerMixin(ClientProtocol):
|
|
10
|
+
async def _run_periodic(
|
|
11
|
+
self, func: Callable[[], Any | Awaitable[Any]], interval: float
|
|
12
|
+
) -> None:
|
|
13
|
+
while True:
|
|
14
|
+
try:
|
|
15
|
+
result = func()
|
|
16
|
+
if asyncio.iscoroutine(result):
|
|
17
|
+
await result
|
|
18
|
+
except Exception as e:
|
|
19
|
+
tb = traceback.format_exc()
|
|
20
|
+
self.logger.error(f"Error in scheduled task {func}: {e}")
|
|
21
|
+
raise
|
|
22
|
+
await asyncio.sleep(interval)
|
|
23
|
+
|
|
24
|
+
async def _start_scheduled_tasks(self) -> None:
|
|
25
|
+
for func, interval in self._scheduled_tasks:
|
|
26
|
+
task = asyncio.create_task(self._run_periodic(func, interval))
|
|
27
|
+
self._background_tasks.add(task)
|
|
28
|
+
task.add_done_callback(self._background_tasks.discard)
|