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.
- maxapi_python-2.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {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"])
|