maxapi-python 1.2.4__py3-none-any.whl → 2.0.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.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/message.py DELETED
@@ -1,879 +0,0 @@
1
- import asyncio
2
- import time
3
- from http import HTTPStatus
4
- from pathlib import Path
5
-
6
- import aiohttp
7
- from aiofiles import open as aio_open
8
- from aiohttp import ClientSession, TCPConnector
9
-
10
- from pymax.exceptions import Error
11
- from pymax.files import File, Photo, Video
12
- from pymax.formatting import Formatting
13
- from pymax.payloads import (
14
- AddReactionPayload,
15
- AttachFilePayload,
16
- AttachPhotoPayload,
17
- DeleteMessagePayload,
18
- EditMessagePayload,
19
- FetchHistoryPayload,
20
- GetFilePayload,
21
- GetReactionsPayload,
22
- GetVideoPayload,
23
- MessageElement,
24
- PinMessagePayload,
25
- ReactionInfoPayload,
26
- ReadMessagesPayload,
27
- RemoveReactionPayload,
28
- ReplyLink,
29
- SendMessagePayload,
30
- SendMessagePayloadMessage,
31
- UploadPayload,
32
- VideoAttachPayload,
33
- )
34
- from pymax.protocols import ClientProtocol
35
- from pymax.static.constant import DEFAULT_TIMEOUT
36
- from pymax.static.enum import AttachType, Opcode, ReadAction
37
- from pymax.types import (
38
- Attach,
39
- FileRequest,
40
- Message,
41
- ReactionInfo,
42
- ReadState,
43
- VideoRequest,
44
- )
45
- from pymax.utils import MixinsUtils
46
-
47
-
48
- class MessageMixin(ClientProtocol):
49
- CHUNK_SIZE = 6 * 1024 * 1024
50
-
51
- async def _upload_file(self, file: File) -> None | Attach:
52
- try:
53
- self.logger.info("Uploading file")
54
-
55
- payload = UploadPayload().model_dump(by_alias=True)
56
- data = await self._send_and_wait(
57
- opcode=Opcode.FILE_UPLOAD,
58
- payload=payload,
59
- )
60
- if data.get("payload", {}).get("error"):
61
- MixinsUtils.handle_error(data)
62
-
63
- url = data.get("payload", {}).get("info", [None])[0].get("url", None)
64
- file_id = data.get("payload", {}).get("info", [None])[0].get("fileId", None)
65
- if not url or not file_id:
66
- self.logger.error("No upload URL or file ID received")
67
- return None
68
-
69
- self.logger.debug("Got upload URL and file_id=%s", file_id)
70
-
71
- if file.path:
72
- file_size = Path(file.path).stat().st_size
73
- self.logger.info("File size from path: %.2f MB", file_size / (1024 * 1024))
74
- else:
75
- file_bytes = await file.read()
76
- file_size = len(file_bytes)
77
- self.logger.info("File size from URL: %.2f MB", file_size / (1024 * 1024))
78
-
79
- connector = TCPConnector(limit=0)
80
- timeout = aiohttp.ClientTimeout(total=None, sock_read=None, sock_connect=30)
81
-
82
- headers = {
83
- "Content-Disposition": f"attachment; filename={file.file_name}",
84
- "Content-Length": str(file_size),
85
- "Content-Range": f"0-{file_size - 1}/{file_size}",
86
- }
87
-
88
- loop = asyncio.get_running_loop()
89
- fut: asyncio.Future[dict] = loop.create_future()
90
- self._file_upload_waiters[int(file_id)] = fut
91
-
92
- async def file_generator():
93
- bytes_sent = 0
94
- chunk_num = 0
95
- self.logger.debug("Starting file streaming from: %s", file.path)
96
- async with aio_open(file.path, "rb") as f:
97
- while True:
98
- chunk = await f.read(self.CHUNK_SIZE)
99
- if not chunk:
100
- self.logger.info(
101
- "File streaming complete: %d bytes in %d chunks",
102
- bytes_sent,
103
- chunk_num,
104
- )
105
- break
106
-
107
- yield chunk
108
-
109
- bytes_sent += len(chunk)
110
- chunk_num += 1
111
- if chunk_num % 10 == 0:
112
- self.logger.info(
113
- "Upload progress: %.1f MB in %d chunks",
114
- bytes_sent / (1024 * 1024),
115
- chunk_num,
116
- )
117
- if chunk_num % 4 == 0:
118
- await asyncio.sleep(0)
119
-
120
- async def bytes_generator(b: bytes):
121
- bytes_sent = 0
122
- chunk_num = 0
123
- for i in range(0, len(b), self.CHUNK_SIZE):
124
- chunk = b[i : i + self.CHUNK_SIZE]
125
- yield chunk
126
- bytes_sent += len(chunk)
127
- chunk_num += 1
128
- if chunk_num % 10 == 0:
129
- self.logger.info(
130
- "Upload progress: %.1f MB in %d chunks",
131
- bytes_sent / (1024 * 1024),
132
- chunk_num,
133
- )
134
- if chunk_num % 4 == 0:
135
- await asyncio.sleep(0)
136
-
137
- if file.path:
138
- data_to_send = file_generator()
139
- else:
140
- data_to_send = bytes_generator(file_bytes)
141
-
142
- self.logger.info("Starting file upload: %s", file.file_name)
143
-
144
- async with (
145
- ClientSession(connector=connector, timeout=timeout) as session,
146
- session.post(url=url, headers=headers, data=data_to_send) as response,
147
- ):
148
- self.logger.debug("Server response status: %d", response.status)
149
- if response.status != HTTPStatus.OK:
150
- self.logger.error("Upload failed with status %s", response.status)
151
- self._file_upload_waiters.pop(int(file_id), None)
152
- return None
153
-
154
- self.logger.debug(
155
- "File sent successfully. Waiting for server confirmation "
156
- "(timeout=%d seconds, fileId=%s)",
157
- DEFAULT_TIMEOUT,
158
- file_id,
159
- )
160
- try:
161
- await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
162
- self.logger.info("File upload completed successfully (fileId=%s)", file_id)
163
- return Attach(_type=AttachType.FILE, file_id=file_id)
164
- except asyncio.TimeoutError:
165
- self.logger.warning(
166
- "Timed out waiting for file processing notification for fileId=%s",
167
- file_id,
168
- )
169
- self._file_upload_waiters.pop(int(file_id), None)
170
- return None
171
-
172
- except Exception:
173
- self.logger.exception("Upload file failed")
174
- raise
175
-
176
- async def _upload_video(self, video: Video) -> None | Attach:
177
- try:
178
- self.logger.info("Uploading video")
179
- payload = UploadPayload().model_dump(by_alias=True)
180
- data = await self._send_and_wait(
181
- opcode=Opcode.VIDEO_UPLOAD,
182
- payload=payload,
183
- )
184
-
185
- if data.get("payload", {}).get("error"):
186
- MixinsUtils.handle_error(data)
187
-
188
- url = data.get("payload", {}).get("info", [None])[0].get("url", None)
189
- video_id = data.get("payload", {}).get("info", [None])[0].get("videoId", None)
190
- if not url or not video_id:
191
- self.logger.error("No upload URL or video ID received")
192
- return None
193
-
194
- token = data.get("payload", {}).get("info", [None])[0].get("token", None)
195
- if not token:
196
- self.logger.error("No upload token received")
197
- return None
198
-
199
- file_bytes = await video.read()
200
- file_size = len(file_bytes)
201
-
202
- # Настройки для ClientSession
203
- connector = TCPConnector(limit=0)
204
- timeout = aiohttp.ClientTimeout(total=900, sock_read=60) # 15 минут на видео
205
-
206
- headers = {
207
- "Content-Disposition": f"attachment; filename={video.file_name}",
208
- "Content-Range": f"0-{file_size - 1}/{file_size}",
209
- "Content-Length": str(file_size),
210
- "Connection": "keep-alive",
211
- }
212
-
213
- loop = asyncio.get_running_loop()
214
- fut: asyncio.Future[dict] = loop.create_future()
215
- try:
216
- self._file_upload_waiters[int(video_id)] = fut
217
- except Exception:
218
- self.logger.exception("Failed to register file upload waiter")
219
-
220
- try:
221
- async with ClientSession(connector=connector, timeout=timeout) as session:
222
- async with session.post(
223
- url=url,
224
- headers=headers,
225
- data=file_bytes,
226
- ) as response:
227
- if response.status != HTTPStatus.OK:
228
- self.logger.error("Upload failed with status %s", response.status)
229
- self._file_upload_waiters.pop(int(video_id), None)
230
- return None
231
-
232
- try:
233
- await asyncio.wait_for(fut, timeout=DEFAULT_TIMEOUT)
234
- return Attach(_type=AttachType.VIDEO, video_id=video_id, token=token)
235
- except asyncio.TimeoutError:
236
- self.logger.warning(
237
- "Timed out waiting for video processing notification for videoId=%s",
238
- video_id,
239
- )
240
- self._file_upload_waiters.pop(int(video_id), None)
241
- return None
242
- except OSError as e:
243
- if "malloc failure" in str(e) or "BUF" in str(e):
244
- self.logger.exception(
245
- "Memory error during video upload. File too large or insufficient memory. Try uploading smaller files or free up memory."
246
- )
247
- self._file_upload_waiters.pop(int(video_id), None)
248
- raise
249
-
250
- except Exception:
251
- self.logger.exception("Upload video failed")
252
- raise
253
-
254
- async def _upload_photo(self, photo: Photo) -> None | Attach:
255
- try:
256
- self.logger.info("Uploading photo")
257
- payload = UploadPayload().model_dump(by_alias=True)
258
-
259
- data = await self._send_and_wait(
260
- opcode=Opcode.PHOTO_UPLOAD,
261
- payload=payload,
262
- )
263
-
264
- if data.get("payload", {}).get("error"):
265
- MixinsUtils.handle_error(data)
266
-
267
- url = data.get("payload", {}).get("url")
268
- if not url:
269
- self.logger.error("No upload URL received")
270
- return None
271
-
272
- photo_data = photo.validate_photo()
273
- if not photo_data:
274
- self.logger.error("Photo validation failed")
275
- return None
276
-
277
- form = aiohttp.FormData()
278
- form.add_field(
279
- name="file",
280
- value=await photo.read(),
281
- filename=f"image.{photo_data[0]}",
282
- content_type=photo_data[1],
283
- )
284
-
285
- async with (
286
- ClientSession() as session,
287
- session.post(
288
- url=url,
289
- data=form,
290
- ) as response,
291
- ):
292
- if response.status != HTTPStatus.OK:
293
- self.logger.error(f"Upload failed with status {response.status}")
294
- return None
295
-
296
- result = await response.json()
297
-
298
- if not result.get("photos"):
299
- self.logger.error("No photos in response")
300
- return None
301
-
302
- photo_data = next(iter(result["photos"].values()), None)
303
- if not photo_data or "token" not in photo_data:
304
- self.logger.error("No token in response")
305
- return None
306
-
307
- return Attach(
308
- _type=AttachType.PHOTO,
309
- photo_token=photo_data["token"],
310
- )
311
-
312
- except Exception as e:
313
- self.logger.exception("Upload photo failed: %s", str(e))
314
- return None
315
-
316
- async def _upload_attachment(self, attach: Photo | File | Video) -> dict | None:
317
- if isinstance(attach, Photo):
318
- uploaded = await self._upload_photo(attach)
319
- if uploaded and uploaded.photo_token:
320
- return AttachPhotoPayload(photo_token=uploaded.photo_token).model_dump(
321
- by_alias=True
322
- )
323
- elif isinstance(attach, File):
324
- uploaded = await self._upload_file(attach)
325
- if uploaded and uploaded.file_id:
326
- return AttachFilePayload(file_id=uploaded.file_id).model_dump(by_alias=True)
327
- elif isinstance(attach, Video):
328
- uploaded = await self._upload_video(attach)
329
- if uploaded and uploaded.video_id and uploaded.token:
330
- return VideoAttachPayload(
331
- video_id=uploaded.video_id, token=uploaded.token
332
- ).model_dump(by_alias=True)
333
- self.logger.error(f"Attachment upload failed for {attach}")
334
- return None
335
-
336
- async def send_message(
337
- self,
338
- text: str,
339
- chat_id: int,
340
- notify: bool = True,
341
- attachment: Photo | File | Video | None = None,
342
- attachments: list[Photo | File | Video] | None = None,
343
- reply_to: int | None = None,
344
- use_queue: bool = False,
345
- ) -> Message | None:
346
- """
347
- Отправляет текстовое сообщение в чат с опциональными вложениями.
348
-
349
- :param text: Текст сообщения.
350
- :type text: str
351
- :param chat_id: Идентификатор чата, в который отправляется сообщение.
352
- :type chat_id: int
353
- :param notify: Флаг оповещения о новом сообщении. По умолчанию True.
354
- :type notify: bool
355
- :param attachment: Одно вложение (фото, файл или видео).
356
- :type attachment: Photo | File | Video | None
357
- :param attachments: Список множественных вложений.
358
- :type attachments: list[Photo | File | Video] | None
359
- :param reply_to: Идентификатор сообщения для ответа.
360
- :type reply_to: int | None
361
- :param use_queue: Использовать очередь для отправки. По умолчанию False.
362
- :type use_queue: bool
363
- :return: Объект сообщения или None, если используется очередь.
364
- :rtype: Message | None
365
- :raises Error: Если загрузка вложения или отправка сообщения не удалась.
366
- """
367
-
368
- self.logger.info("Sending message to chat_id=%s notify=%s", chat_id, notify)
369
- if attachments and attachment:
370
- self.logger.warning("Both photo and photos provided; using photos")
371
- attachment = None
372
-
373
- attaches = []
374
- if attachment:
375
- self.logger.info("Uploading attachment for message")
376
- result = await self._upload_attachment(attachment)
377
- if not result:
378
- raise Error("upload_failed", "Failed to upload attachment", "Upload Error")
379
- attaches.append(result)
380
-
381
- elif attachments:
382
- self.logger.info("Uploading multiple attachments for message")
383
- for p in attachments:
384
- result = await self._upload_attachment(p)
385
- if result:
386
- attaches.append(result)
387
- else:
388
- raise Error("upload_failed", "Failed to upload attachment", "Upload Error")
389
-
390
- if not attaches:
391
- raise Error("upload_failed", "All attachments failed to upload", "Upload Error")
392
-
393
- elements = []
394
- clean_text = None
395
- raw_elements, parsed_text = Formatting.get_elements_from_markdown(text)
396
- if raw_elements:
397
- clean_text = parsed_text
398
- elements = [
399
- MessageElement(type=e.type, length=e.length, from_=e.from_) for e in raw_elements
400
- ]
401
-
402
- payload = SendMessagePayload(
403
- chat_id=chat_id,
404
- message=SendMessagePayloadMessage(
405
- text=clean_text if clean_text else text,
406
- cid=int(time.time() * 1000),
407
- elements=elements,
408
- attaches=attaches,
409
- link=(ReplyLink(message_id=str(reply_to)) if reply_to else None),
410
- ),
411
- notify=notify,
412
- ).model_dump(by_alias=True)
413
-
414
- if use_queue:
415
- await self._queue_message(opcode=Opcode.MSG_SEND, payload=payload)
416
- self.logger.debug("Message queued for sending")
417
- return None
418
-
419
- data = await self._send_and_wait(opcode=Opcode.MSG_SEND, payload=payload)
420
-
421
- if data.get("payload", {}).get("error"):
422
- MixinsUtils.handle_error(data)
423
-
424
- msg = Message.from_dict(data["payload"]) if data.get("payload") else None
425
- self.logger.debug("send_message result: %r", msg)
426
- if not msg:
427
- raise Error("no_message", "Message data missing in response", "Message Error")
428
-
429
- return msg
430
-
431
- async def edit_message(
432
- self,
433
- chat_id: int,
434
- message_id: int,
435
- text: str,
436
- attachment: Photo | File | Video | None = None,
437
- attachments: list[Photo | Video | File] | None = None,
438
- use_queue: bool = False,
439
- ) -> Message | None:
440
- """
441
- Редактирует текст и/или вложения существующего сообщения.
442
-
443
- :param chat_id: Идентификатор чата.
444
- :type chat_id: int
445
- :param message_id: Идентификатор сообщения для редактирования.
446
- :type message_id: int
447
- :param text: Новый текст сообщения.
448
- :type text: str
449
- :param attachment: Новое вложение (фото, файл или видео).
450
- :type attachment: Photo | File | Video | None
451
- :param attachments: Список новых множественных вложений.
452
- :type attachments: list[Photo | Video | File] | None
453
- :param use_queue: Использовать очередь для отправки.
454
- :type use_queue: bool
455
- :return: Отредактированное сообщение или None.
456
- :rtype: Message | None
457
- :raises Error: Если редактирование не удалось.
458
- """
459
- self.logger.info("Editing message chat_id=%s message_id=%s", chat_id, message_id)
460
-
461
- if attachments and attachment:
462
- self.logger.warning("Both photo and photos provided; using photos")
463
- attachment = None
464
-
465
- attaches = []
466
- if attachment:
467
- self.logger.info("Uploading attachment for message")
468
- result = await self._upload_attachment(attachment)
469
- if not result:
470
- raise Error("upload_failed", "Failed to upload attachment", "Upload Error")
471
- attaches.append(result)
472
-
473
- elif attachments:
474
- self.logger.info("Uploading multiple attachments for message")
475
- for p in attachments:
476
- result = await self._upload_attachment(p)
477
- if result:
478
- attaches.append(result)
479
- else:
480
- raise Error("upload_failed", "Failed to upload attachment", "Upload Error")
481
-
482
- if not attaches:
483
- raise Error("upload_failed", "All attachments failed to upload", "Upload Error")
484
-
485
- elements = []
486
- clean_text = None
487
- raw_elements = Formatting.get_elements_from_markdown(text)[0]
488
- if raw_elements:
489
- clean_text = Formatting.get_elements_from_markdown(text)[1]
490
- elements = [
491
- MessageElement(type=e.type, length=e.length, from_=e.from_) for e in raw_elements
492
- ]
493
-
494
- payload = EditMessagePayload(
495
- chat_id=chat_id,
496
- message_id=message_id,
497
- text=clean_text if clean_text else text,
498
- elements=elements,
499
- attaches=attaches,
500
- ).model_dump(by_alias=True)
501
-
502
- if use_queue:
503
- await self._queue_message(opcode=Opcode.MSG_EDIT, payload=payload)
504
- self.logger.debug("Edit message queued for sending")
505
- return None
506
-
507
- data = await self._send_and_wait(opcode=Opcode.MSG_EDIT, payload=payload)
508
-
509
- if data.get("payload", {}).get("error"):
510
- MixinsUtils.handle_error(data)
511
-
512
- msg = Message.from_dict(data["payload"]) if data.get("payload") else None
513
- self.logger.debug("edit_message result: %r", msg)
514
- if not msg:
515
- raise Error("no_message", "Message data missing in response", "Message Error")
516
-
517
- return msg
518
-
519
- async def delete_message(
520
- self,
521
- chat_id: int,
522
- message_ids: list[int],
523
- for_me: bool,
524
- use_queue: bool = False,
525
- ) -> bool:
526
- """
527
- Удаляет одно или несколько сообщений.
528
-
529
- :param chat_id: Идентификатор чата.
530
- :type chat_id: int
531
- :param message_ids: Список идентификаторов сообщений для удаления.
532
- :type message_ids: list[int]
533
- :param for_me: Удалить только для себя (не видимо другим).
534
- :type for_me: bool
535
- :param use_queue: Использовать очередь для отправки.
536
- :type use_queue: bool
537
- :return: True, если сообщения успешно удалены.
538
- :rtype: bool
539
- """
540
- self.logger.info(
541
- "Deleting messages chat_id=%s ids=%s for_me=%s",
542
- chat_id,
543
- message_ids,
544
- for_me,
545
- )
546
-
547
- payload = DeleteMessagePayload(
548
- chat_id=chat_id, message_ids=message_ids, for_me=for_me
549
- ).model_dump(by_alias=True)
550
-
551
- if use_queue:
552
- await self._queue_message(opcode=Opcode.MSG_DELETE, payload=payload)
553
- self.logger.debug("Delete message queued for sending")
554
- return True
555
-
556
- data = await self._send_and_wait(opcode=Opcode.MSG_DELETE, payload=payload)
557
-
558
- if data.get("payload", {}).get("error"):
559
- MixinsUtils.handle_error(data)
560
-
561
- self.logger.debug("delete_message success")
562
- return True
563
-
564
- async def pin_message(self, chat_id: int, message_id: int, notify_pin: bool) -> bool:
565
- """
566
- Закрепляет сообщение в чате.
567
-
568
- :param chat_id: Идентификатор чата.
569
- :type chat_id: int
570
- :param message_id: Идентификатор сообщения.
571
- :type message_id: int
572
- :param notify_pin: Отправить уведомление о закреплении.
573
- :type notify_pin: bool
574
- :return: True, если сообщение успешно закреплено.
575
- :rtype: bool
576
- """
577
- payload = PinMessagePayload(
578
- chat_id=chat_id,
579
- notify_pin=notify_pin,
580
- pin_message_id=message_id,
581
- ).model_dump(by_alias=True)
582
-
583
- data = await self._send_and_wait(opcode=Opcode.CHAT_UPDATE, payload=payload)
584
-
585
- if data.get("payload", {}).get("error"):
586
- MixinsUtils.handle_error(data)
587
-
588
- self.logger.debug("pin_message success")
589
- return True
590
-
591
- async def fetch_history(
592
- self,
593
- chat_id: int,
594
- from_time: int | None = None,
595
- forward: int = 0,
596
- backward: int = 200,
597
- ) -> list[Message] | None:
598
- """
599
- Получает историю сообщений из чата.
600
-
601
- :param chat_id: Идентификатор чата.
602
- :type chat_id: int
603
- :param from_time: Временная метка для начала выборки.
604
- :type from_time: int | None
605
- :param forward: Кол-во сообщений вперед от from_time.
606
- :type forward: int
607
- :param backward: Кол-во сообщений назад от from_time.
608
- :type backward: int
609
- :return: Список сообщений или None.
610
- :rtype: list[Message] | None
611
- """
612
- if from_time is None:
613
- from_time = int(time.time() * 1000)
614
-
615
- self.logger.info(
616
- "Fetching history chat_id=%s from=%s forward=%s backward=%s",
617
- chat_id,
618
- from_time,
619
- forward,
620
- backward,
621
- )
622
-
623
- payload = FetchHistoryPayload(
624
- chat_id=chat_id,
625
- from_time=from_time,
626
- forward=forward,
627
- backward=backward,
628
- ).model_dump(by_alias=True)
629
-
630
- self.logger.debug("Payload dict keys: %s", list(payload.keys()))
631
-
632
- data = await self._send_and_wait(opcode=Opcode.CHAT_HISTORY, payload=payload, timeout=10)
633
-
634
- if data.get("payload", {}).get("error"):
635
- MixinsUtils.handle_error(data)
636
-
637
- messages = [Message.from_dict(msg) for msg in data["payload"].get("messages", [])]
638
- self.logger.debug("History fetched: %d messages", len(messages))
639
- return messages
640
-
641
- async def get_video_by_id(
642
- self,
643
- chat_id: int,
644
- message_id: int,
645
- video_id: int,
646
- ) -> VideoRequest | None:
647
- """
648
- Получает видео
649
-
650
- :param chat_id: ID чата
651
- :type chat_id: int
652
- :param message_id: ID сообщения
653
- :type message_id: int
654
- :param video_id: ID видео
655
- :type video_id: int
656
- :return: Объект VideoRequest или None
657
- :rtype: VideoRequest | None
658
- """
659
- self.logger.info("Getting video_id=%s message_id=%s", video_id, message_id)
660
-
661
- if self.is_connected and self._socket is not None:
662
- payload = GetVideoPayload(
663
- chat_id=chat_id, message_id=message_id, video_id=video_id
664
- ).model_dump(by_alias=True)
665
- else:
666
- payload = GetVideoPayload(
667
- chat_id=chat_id,
668
- message_id=str(message_id),
669
- video_id=video_id,
670
- ).model_dump(by_alias=True)
671
-
672
- data = await self._send_and_wait(opcode=Opcode.VIDEO_PLAY, payload=payload)
673
-
674
- if data.get("payload", {}).get("error"):
675
- MixinsUtils.handle_error(data)
676
-
677
- video = VideoRequest.from_dict(data["payload"]) if data.get("payload") else None
678
- self.logger.debug("result: %r", video)
679
- if not video:
680
- raise Error("no_video", "Video data missing in response", "Video Error")
681
-
682
- return video
683
-
684
- async def get_file_by_id(
685
- self,
686
- chat_id: int,
687
- message_id: int,
688
- file_id: int,
689
- ) -> FileRequest | None:
690
- """
691
- Получает файл
692
-
693
- :param chat_id: ID чата
694
- :type chat_id: int
695
- :param message_id: ID сообщения
696
- :type message_id: int
697
- :param file_id: ID файла
698
- :type file_id: int
699
- :return: Объект FileRequest или None
700
- :rtype: FileRequest | None
701
- """
702
- self.logger.info("Getting file_id=%s message_id=%s", file_id, message_id)
703
- if self.is_connected and self._socket is not None:
704
- payload = GetFilePayload(
705
- chat_id=chat_id, message_id=message_id, file_id=file_id
706
- ).model_dump(by_alias=True)
707
- else:
708
- payload = GetFilePayload(
709
- chat_id=chat_id,
710
- message_id=str(message_id),
711
- file_id=file_id,
712
- ).model_dump(by_alias=True)
713
-
714
- data = await self._send_and_wait(opcode=Opcode.FILE_DOWNLOAD, payload=payload)
715
-
716
- if data.get("payload", {}).get("error"):
717
- MixinsUtils.handle_error(data)
718
-
719
- file = FileRequest.from_dict(data["payload"]) if data.get("payload") else None
720
- self.logger.debug(" result: %r", file)
721
- if not file:
722
- raise Error("no_file", "File data missing in response", "File Error")
723
-
724
- return file
725
-
726
- async def add_reaction(
727
- self,
728
- chat_id: int,
729
- message_id: str,
730
- reaction: str,
731
- ) -> ReactionInfo | None:
732
- """
733
- Добавляет реакцию к сообщению.
734
-
735
- :param chat_id: ID чата
736
- :type chat_id: int
737
- :param message_id: ID сообщения
738
- :type message_id: str
739
- :param reaction: Реакция для добавления
740
- :type reaction: str (emoji)
741
- :return: Объект ReactionInfo или None
742
- :rtype: ReactionInfo | None
743
- """
744
- try:
745
- self.logger.info(
746
- "Adding reaction to message chat_id=%s message_id=%s reaction=%s",
747
- chat_id,
748
- message_id,
749
- reaction,
750
- )
751
-
752
- payload = AddReactionPayload(
753
- chat_id=chat_id,
754
- message_id=message_id,
755
- reaction=ReactionInfoPayload(id=reaction),
756
- ).model_dump(by_alias=True)
757
-
758
- data = await self._send_and_wait(opcode=Opcode.MSG_REACTION, payload=payload)
759
-
760
- if data.get("payload", {}).get("error"):
761
- MixinsUtils.handle_error(data)
762
-
763
- self.logger.debug("add_reaction success")
764
- return (
765
- ReactionInfo.from_dict(data["payload"]["reactionInfo"])
766
- if data.get("payload")
767
- else None
768
- )
769
- except Exception:
770
- self.logger.exception("Add reaction failed")
771
- return None
772
-
773
- async def get_reactions(
774
- self, chat_id: int, message_ids: list[str]
775
- ) -> dict[str, ReactionInfo] | None:
776
- """
777
- Получает реакции на сообщения.
778
-
779
- :param chat_id: ID чата
780
- :type chat_id: int
781
- :param message_ids: Список ID сообщений
782
- :type message_ids: list[str]
783
- :return: Словарь с ID сообщений и соответствующими реакциями
784
- :rtype: dict[str, ReactionInfo] | None
785
- """
786
- self.logger.info(
787
- "Getting reactions for messages chat_id=%s message_ids=%s",
788
- chat_id,
789
- message_ids,
790
- )
791
-
792
- payload = GetReactionsPayload(chat_id=chat_id, message_ids=message_ids).model_dump(
793
- by_alias=True
794
- )
795
-
796
- data = await self._send_and_wait(opcode=Opcode.MSG_GET_REACTIONS, payload=payload)
797
-
798
- if data.get("payload", {}).get("error"):
799
- MixinsUtils.handle_error(data)
800
-
801
- reactions = {}
802
- for msg_id, reaction_data in data.get("payload", {}).get("messagesReactions", {}).items():
803
- reactions[msg_id] = ReactionInfo.from_dict(reaction_data)
804
-
805
- self.logger.debug("get_reactions success")
806
- return reactions
807
-
808
- async def remove_reaction(
809
- self,
810
- chat_id: int,
811
- message_id: str,
812
- ) -> ReactionInfo | None:
813
- """
814
- Удаляет реакцию с сообщения.
815
-
816
- :param chat_id: ID чата
817
- :type chat_id: int
818
- :param message_id: ID сообщения
819
- :type message_id: str
820
- :return: Объект ReactionInfo или None
821
- :rtype: ReactionInfo | None
822
- """
823
- self.logger.info(
824
- "Removing reaction from message chat_id=%s message_id=%s",
825
- chat_id,
826
- message_id,
827
- )
828
-
829
- payload = RemoveReactionPayload(
830
- chat_id=chat_id,
831
- message_id=message_id,
832
- ).model_dump(by_alias=True)
833
-
834
- data = await self._send_and_wait(opcode=Opcode.MSG_CANCEL_REACTION, payload=payload)
835
-
836
- if data.get("payload", {}).get("error"):
837
- MixinsUtils.handle_error(data)
838
-
839
- self.logger.debug("remove_reaction success")
840
- if not data.get("payload"):
841
- raise Error("no_reaction", "Reaction data missing in response", "Reaction Error")
842
-
843
- reaction = ReactionInfo.from_dict(data["payload"]["reactionInfo"])
844
- if not reaction:
845
- raise Error(
846
- "invalid_reaction",
847
- "Invalid reaction data in response",
848
- "Reaction Error",
849
- )
850
-
851
- return reaction
852
-
853
- async def read_message(self, message_id: int, chat_id: int) -> ReadState:
854
- """
855
- Отмечает сообщение как прочитанное.
856
-
857
- :param message_id: ID сообщения
858
- :type message_id: int
859
- :param chat_id: ID чата
860
- :type chat_id: int
861
- :return: Объект ReadState
862
- :rtype: ReadState
863
- """
864
- self.logger.info("Marking message as read chat_id=%s message_id=%s", chat_id, message_id)
865
-
866
- payload = ReadMessagesPayload(
867
- type=ReadAction.READ_MESSAGE,
868
- chat_id=chat_id,
869
- message_id=str(message_id),
870
- mark=int(time.time() * 1000),
871
- ).model_dump(by_alias=True)
872
-
873
- data = await self._send_and_wait(opcode=Opcode.CHAT_MARK, payload=payload)
874
-
875
- if data.get("payload", {}).get("error"):
876
- MixinsUtils.handle_error(data)
877
-
878
- self.logger.debug("read_message success")
879
- return ReadState.from_dict(data["payload"])