maxapi-python 0.1.1__py3-none-any.whl → 0.1.2__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-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/METADATA +20 -4
- maxapi_python-0.1.2.dist-info/RECORD +25 -0
- pymax/__init__.py +55 -0
- pymax/core.py +156 -0
- pymax/crud.py +99 -0
- pymax/exceptions.py +20 -0
- pymax/files.py +85 -0
- pymax/filters.py +38 -0
- pymax/interfaces.py +67 -0
- pymax/mixins/__init__.py +18 -0
- pymax/mixins/auth.py +81 -0
- pymax/mixins/channel.py +25 -0
- pymax/mixins/group.py +220 -0
- pymax/mixins/handler.py +60 -0
- pymax/mixins/message.py +293 -0
- pymax/mixins/self.py +38 -0
- pymax/mixins/user.py +82 -0
- pymax/mixins/websocket.py +242 -0
- pymax/models.py +8 -0
- pymax/payloads.py +175 -0
- pymax/static.py +210 -0
- pymax/types.py +432 -0
- pymax/utils.py +38 -0
- maxapi_python-0.1.1.dist-info/RECORD +0 -4
- {maxapi_python-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/WHEEL +0 -0
- {maxapi_python-0.1.1.dist-info → maxapi_python-0.1.2.dist-info}/licenses/LICENSE +0 -0
pymax/mixins/auth.py
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
import asyncio
|
2
|
+
import re
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from pymax.interfaces import ClientProtocol
|
6
|
+
from pymax.payloads import RequestCodePayload, SendCodePayload
|
7
|
+
from pymax.static import AuthType, Constants, Opcode
|
8
|
+
|
9
|
+
|
10
|
+
class AuthMixin(ClientProtocol):
|
11
|
+
def _check_phone(self) -> bool:
|
12
|
+
return bool(re.match(Constants.PHONE_REGEX.value, self.phone))
|
13
|
+
|
14
|
+
async def _request_code(
|
15
|
+
self, phone: str, language: str = "ru"
|
16
|
+
) -> dict[str, int | str]:
|
17
|
+
try:
|
18
|
+
self.logger.info("Requesting auth code")
|
19
|
+
|
20
|
+
payload = RequestCodePayload(
|
21
|
+
phone=phone, type=AuthType.START_AUTH, language=language
|
22
|
+
).model_dump(by_alias=True)
|
23
|
+
|
24
|
+
data = await self._send_and_wait(
|
25
|
+
opcode=Opcode.AUTH_REQUEST, payload=payload
|
26
|
+
)
|
27
|
+
self.logger.debug(
|
28
|
+
"Code request response opcode=%s seq=%s",
|
29
|
+
data.get("opcode"),
|
30
|
+
data.get("seq"),
|
31
|
+
)
|
32
|
+
return data.get("payload")
|
33
|
+
except Exception:
|
34
|
+
self.logger.error("Request code failed", exc_info=True)
|
35
|
+
raise RuntimeError("Request code failed")
|
36
|
+
|
37
|
+
async def _send_code(self, code: str, token: str) -> dict[str, Any]:
|
38
|
+
try:
|
39
|
+
self.logger.info("Sending verification code")
|
40
|
+
|
41
|
+
payload = SendCodePayload(
|
42
|
+
token=token,
|
43
|
+
verify_code=code,
|
44
|
+
auth_token_type=AuthType.CHECK_CODE,
|
45
|
+
).model_dump(by_alias=True)
|
46
|
+
|
47
|
+
data = await self._send_and_wait(opcode=Opcode.AUTH, payload=payload)
|
48
|
+
self.logger.debug(
|
49
|
+
"Send code response opcode=%s seq=%s",
|
50
|
+
data.get("opcode"),
|
51
|
+
data.get("seq"),
|
52
|
+
)
|
53
|
+
return data.get("payload")
|
54
|
+
except Exception:
|
55
|
+
self.logger.error("Send code failed", exc_info=True)
|
56
|
+
raise RuntimeError("Send code failed")
|
57
|
+
|
58
|
+
async def _login(self) -> None:
|
59
|
+
self.logger.info("Starting login flow")
|
60
|
+
request_code_payload = await self._request_code(self.phone)
|
61
|
+
temp_token = request_code_payload.get("token")
|
62
|
+
if not temp_token or not isinstance(temp_token, str):
|
63
|
+
self.logger.critical("Failed to request code: token missing")
|
64
|
+
raise ValueError("Failed to request code")
|
65
|
+
|
66
|
+
code = await asyncio.to_thread(input, "Введите код: ")
|
67
|
+
if len(code) != 6 or not code.isdigit():
|
68
|
+
self.logger.error("Invalid code format entered")
|
69
|
+
raise ValueError("Invalid code format")
|
70
|
+
|
71
|
+
login_resp = await self._send_code(code, temp_token)
|
72
|
+
token: str | None = (
|
73
|
+
login_resp.get("tokenAttrs", {}).get("LOGIN", {}).get("token")
|
74
|
+
)
|
75
|
+
if not token:
|
76
|
+
self.logger.critical("Failed to login, token not received")
|
77
|
+
raise ValueError("Failed to login, token not received")
|
78
|
+
|
79
|
+
self._token = token
|
80
|
+
self._database.update_auth_token(self._device_id, self._token)
|
81
|
+
self.logger.info("Login successful, token saved to database")
|
pymax/mixins/channel.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
from pymax.interfaces import ClientProtocol
|
2
|
+
from pymax.payloads import ResolveLinkPayload
|
3
|
+
from pymax.static import Opcode
|
4
|
+
|
5
|
+
|
6
|
+
class ChannelMixin(ClientProtocol):
|
7
|
+
async def resolve_channel_by_name(self, name: str) -> bool:
|
8
|
+
"""
|
9
|
+
Пытается найти канал по его имени
|
10
|
+
|
11
|
+
Args:
|
12
|
+
name (str): Имя канала
|
13
|
+
|
14
|
+
Returns:
|
15
|
+
bool: True, если канал найден
|
16
|
+
"""
|
17
|
+
payload = ResolveLinkPayload(
|
18
|
+
link=f"https://max.ru/{name}",
|
19
|
+
).model_dump(by_alias=True)
|
20
|
+
|
21
|
+
data = await self._send_and_wait(opcode=Opcode.LINK_INFO, payload=payload)
|
22
|
+
if error := data.get("payload", {}).get("error"):
|
23
|
+
self.logger.error("Resolve link error: %s", error)
|
24
|
+
return False
|
25
|
+
return True
|
pymax/mixins/group.py
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from pymax.interfaces import ClientProtocol
|
4
|
+
from pymax.payloads import (
|
5
|
+
ChangeGroupProfilePayload,
|
6
|
+
ChangeGroupSettingsOptions,
|
7
|
+
ChangeGroupSettingsPayload,
|
8
|
+
CreateGroupAttach,
|
9
|
+
CreateGroupMessage,
|
10
|
+
CreateGroupPayload,
|
11
|
+
InviteUsersPayload,
|
12
|
+
RemoveUsersPayload,
|
13
|
+
)
|
14
|
+
from pymax.static import Opcode
|
15
|
+
from pymax.types import Chat, Message
|
16
|
+
|
17
|
+
|
18
|
+
class GroupMixin(ClientProtocol):
|
19
|
+
async def create_group(
|
20
|
+
self, name: str, participant_ids: list[int] | None = None, notify: bool = True
|
21
|
+
) -> tuple[Chat, Message] | None:
|
22
|
+
"""
|
23
|
+
Создает группу
|
24
|
+
|
25
|
+
Args:
|
26
|
+
name (str): Название группы.
|
27
|
+
participant_ids (list[int] | None, optional): Список идентификаторов участников. Defaults to None.
|
28
|
+
notify (bool, optional): Флаг оповещения. Defaults to True.
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
tuple[Chat, Message] | None: Объект Chat и Message или None при ошибке.
|
32
|
+
"""
|
33
|
+
try:
|
34
|
+
payload = CreateGroupPayload(
|
35
|
+
message=CreateGroupMessage(
|
36
|
+
cid=int(time.time() * 1000),
|
37
|
+
attaches=[
|
38
|
+
CreateGroupAttach(
|
39
|
+
_type="CONTROL",
|
40
|
+
title=name,
|
41
|
+
user_ids=participant_ids if participant_ids else [],
|
42
|
+
)
|
43
|
+
],
|
44
|
+
),
|
45
|
+
notify=notify,
|
46
|
+
).model_dump(by_alias=True)
|
47
|
+
|
48
|
+
data = await self._send_and_wait(opcode=Opcode.MSG_SEND, payload=payload)
|
49
|
+
if error := data.get("payload", {}).get("error"):
|
50
|
+
self.logger.error("Create group error: %s", error)
|
51
|
+
return None
|
52
|
+
|
53
|
+
chat = Chat.from_dict(data["payload"]["chat"])
|
54
|
+
message = Message.from_dict(data["payload"]["message"])
|
55
|
+
|
56
|
+
if chat:
|
57
|
+
cached_chat = await self._get_chat(chat.id)
|
58
|
+
if cached_chat is None:
|
59
|
+
self.chats.append(chat)
|
60
|
+
else:
|
61
|
+
idx = self.chats.index(cached_chat)
|
62
|
+
self.chats[idx] = chat
|
63
|
+
|
64
|
+
return chat, message
|
65
|
+
|
66
|
+
except Exception:
|
67
|
+
self.logger.exception("Create group failed")
|
68
|
+
|
69
|
+
async def invite_users_to_group(
|
70
|
+
self,
|
71
|
+
chat_id: int,
|
72
|
+
user_ids: list[int],
|
73
|
+
show_history: bool = True,
|
74
|
+
) -> bool:
|
75
|
+
"""
|
76
|
+
Приглашает пользователей в группу
|
77
|
+
|
78
|
+
Args:
|
79
|
+
chat_id (int): ID группы.
|
80
|
+
user_ids (list[int]): Список идентификаторов пользователей.
|
81
|
+
show_history (bool, optional): Флаг оповещения. Defaults to True.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
bool: True, если пользователи успешно приглашены
|
85
|
+
"""
|
86
|
+
try:
|
87
|
+
payload = InviteUsersPayload(
|
88
|
+
chat_id=chat_id,
|
89
|
+
user_ids=user_ids,
|
90
|
+
show_history=show_history,
|
91
|
+
operation="add",
|
92
|
+
).model_dump(by_alias=True)
|
93
|
+
|
94
|
+
data = await self._send_and_wait(
|
95
|
+
opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
|
96
|
+
)
|
97
|
+
|
98
|
+
if error := data.get("payload", {}).get("error"):
|
99
|
+
self.logger.error("Create group error: %s", error)
|
100
|
+
return False
|
101
|
+
|
102
|
+
chat = Chat.from_dict(data["payload"]["chat"])
|
103
|
+
if chat:
|
104
|
+
cached_chat = await self._get_chat(chat.id)
|
105
|
+
if cached_chat is None:
|
106
|
+
self.chats.append(chat)
|
107
|
+
else:
|
108
|
+
idx = self.chats.index(cached_chat)
|
109
|
+
self.chats[idx] = chat
|
110
|
+
|
111
|
+
return True
|
112
|
+
|
113
|
+
except Exception:
|
114
|
+
self.logger.exception("Invite users to group failed")
|
115
|
+
return False
|
116
|
+
|
117
|
+
async def remove_users_from_group(
|
118
|
+
self,
|
119
|
+
chat_id: int,
|
120
|
+
user_ids: list[int],
|
121
|
+
clean_msg_period: int,
|
122
|
+
) -> bool:
|
123
|
+
try:
|
124
|
+
payload = RemoveUsersPayload(
|
125
|
+
chat_id=chat_id,
|
126
|
+
user_ids=user_ids,
|
127
|
+
clean_msg_period=clean_msg_period,
|
128
|
+
).model_dump(by_alias=True)
|
129
|
+
|
130
|
+
data = await self._send_and_wait(
|
131
|
+
opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
|
132
|
+
)
|
133
|
+
|
134
|
+
if error := data.get("payload", {}).get("error"):
|
135
|
+
self.logger.error("Remove users from group error: %s", error)
|
136
|
+
return False
|
137
|
+
|
138
|
+
chat = Chat.from_dict(data["payload"]["chat"])
|
139
|
+
if chat:
|
140
|
+
cached_chat = await self._get_chat(chat.id)
|
141
|
+
if cached_chat is None:
|
142
|
+
self.chats.append(chat)
|
143
|
+
else:
|
144
|
+
idx = self.chats.index(cached_chat)
|
145
|
+
self.chats[idx] = chat
|
146
|
+
|
147
|
+
return True
|
148
|
+
except Exception:
|
149
|
+
self.logger.exception("Remove users from group failed")
|
150
|
+
return False
|
151
|
+
|
152
|
+
async def change_group_settings(
|
153
|
+
self,
|
154
|
+
chat_id: int,
|
155
|
+
all_can_pin_message: bool | None = None,
|
156
|
+
only_owner_can_change_icon_title: bool | None = None,
|
157
|
+
only_admin_can_add_member: bool | None = None,
|
158
|
+
only_admin_can_call: bool | None = None,
|
159
|
+
members_can_see_private_link: bool | None = None,
|
160
|
+
):
|
161
|
+
try:
|
162
|
+
payload = ChangeGroupSettingsPayload(
|
163
|
+
chat_id=chat_id,
|
164
|
+
options=ChangeGroupSettingsOptions(
|
165
|
+
ALL_CAN_PIN_MESSAGE=all_can_pin_message,
|
166
|
+
ONLY_OWNER_CAN_CHANGE_ICON_TITLE=only_owner_can_change_icon_title,
|
167
|
+
ONLY_ADMIN_CAN_ADD_MEMBER=only_admin_can_add_member,
|
168
|
+
ONLY_ADMIN_CAN_CALL=only_admin_can_call,
|
169
|
+
MEMBERS_CAN_SEE_PRIVATE_LINK=members_can_see_private_link,
|
170
|
+
),
|
171
|
+
).model_dump(by_alias=True, exclude_none=True)
|
172
|
+
|
173
|
+
data = await self._send_and_wait(opcode=Opcode.CHAT_UPDATE, payload=payload)
|
174
|
+
|
175
|
+
if error := data.get("payload", {}).get("error"):
|
176
|
+
self.logger.error("Change group settings error: %s", error)
|
177
|
+
return
|
178
|
+
|
179
|
+
chat = Chat.from_dict(data["payload"]["chat"])
|
180
|
+
if chat:
|
181
|
+
cached_chat = await self._get_chat(chat.id)
|
182
|
+
if cached_chat is None:
|
183
|
+
self.chats.append(chat)
|
184
|
+
else:
|
185
|
+
idx = self.chats.index(cached_chat)
|
186
|
+
self.chats[idx] = chat
|
187
|
+
|
188
|
+
except Exception:
|
189
|
+
self.logger.exception("Change group settings failed")
|
190
|
+
|
191
|
+
async def change_group_profile(
|
192
|
+
self,
|
193
|
+
chat_id: int,
|
194
|
+
name: str | None,
|
195
|
+
description: str | None = None,
|
196
|
+
):
|
197
|
+
try:
|
198
|
+
payload = ChangeGroupProfilePayload(
|
199
|
+
chat_id=chat_id,
|
200
|
+
theme=name,
|
201
|
+
description=description,
|
202
|
+
).model_dump(by_alias=True, exclude_none=True)
|
203
|
+
|
204
|
+
data = await self._send_and_wait(opcode=Opcode.CHAT_UPDATE, payload=payload)
|
205
|
+
|
206
|
+
if error := data.get("payload", {}).get("error"):
|
207
|
+
self.logger.error("Change group profile error: %s", error)
|
208
|
+
return
|
209
|
+
|
210
|
+
chat = Chat.from_dict(data["payload"]["chat"])
|
211
|
+
if chat:
|
212
|
+
cached_chat = await self._get_chat(chat.id)
|
213
|
+
if cached_chat is None:
|
214
|
+
self.chats.append(chat)
|
215
|
+
else:
|
216
|
+
idx = self.chats.index(cached_chat)
|
217
|
+
self.chats[idx] = chat
|
218
|
+
|
219
|
+
except Exception:
|
220
|
+
self.logger.exception("Change group profile failed")
|
pymax/mixins/handler.py
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
from typing import Any, Awaitable, Callable
|
2
|
+
|
3
|
+
from pymax.interfaces import ClientProtocol, Filter
|
4
|
+
from pymax.types import Message
|
5
|
+
|
6
|
+
|
7
|
+
class HandlerMixin(ClientProtocol):
|
8
|
+
def on_message(
|
9
|
+
self, *, filter: Filter | None = None
|
10
|
+
) -> Callable[
|
11
|
+
[Callable[[Any], Any | Awaitable[Any]]], Callable[[Any], Any | Awaitable[Any]]
|
12
|
+
]:
|
13
|
+
"""
|
14
|
+
Декоратор для установки обработчика входящих сообщений.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
filter: Фильтр для обработки сообщений.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
Декоратор.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def decorator(
|
24
|
+
handler: Callable[[Any], Any | Awaitable[Any]],
|
25
|
+
) -> Callable[[Any], Any | Awaitable[Any]]:
|
26
|
+
self._on_message_handlers.append((handler, filter))
|
27
|
+
self.logger.info(f"on_message handler set: {handler}, filter: {filter}")
|
28
|
+
return handler
|
29
|
+
|
30
|
+
return decorator
|
31
|
+
|
32
|
+
def on_start(
|
33
|
+
self, handler: Callable[[], Any | Awaitable[Any]]
|
34
|
+
) -> Callable[[], Any | Awaitable[Any]]:
|
35
|
+
"""
|
36
|
+
Устанавливает обработчик, вызываемый при старте клиента.
|
37
|
+
|
38
|
+
Args:
|
39
|
+
handler: Функция или coroutine без аргументов.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Установленный обработчик.
|
43
|
+
"""
|
44
|
+
self._on_start_handler = handler
|
45
|
+
self.logger.debug("on_start handler set: %r", handler)
|
46
|
+
return handler
|
47
|
+
|
48
|
+
def add_message_handler(
|
49
|
+
self, handler: Callable[[Message], Any | Awaitable[Any]], filter: Filter | None
|
50
|
+
) -> Callable[[Message], Any | Awaitable[Any]]:
|
51
|
+
self.logger.debug("add_message_handler (alias) used")
|
52
|
+
self._on_message_handlers.append((handler, filter))
|
53
|
+
return handler
|
54
|
+
|
55
|
+
def add_on_start_handler(
|
56
|
+
self, handler: Callable[[], Any | Awaitable[Any]]
|
57
|
+
) -> Callable[[], Any | Awaitable[Any]]:
|
58
|
+
self.logger.debug("add_on_start_handler (alias) used")
|
59
|
+
self._on_start_handler = handler
|
60
|
+
return handler
|
pymax/mixins/message.py
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
import aiohttp
|
4
|
+
from aiohttp import ClientSession
|
5
|
+
|
6
|
+
from pymax.files import File, Photo, Video
|
7
|
+
from pymax.interfaces import ClientProtocol
|
8
|
+
from pymax.payloads import (
|
9
|
+
AttachPhotoPayload,
|
10
|
+
DeleteMessagePayload,
|
11
|
+
EditMessagePayload,
|
12
|
+
FetchHistoryPayload,
|
13
|
+
PinMessagePayload,
|
14
|
+
ReplyLink,
|
15
|
+
SendMessagePayload,
|
16
|
+
SendMessagePayloadMessage,
|
17
|
+
UploadPhotoPayload,
|
18
|
+
)
|
19
|
+
from pymax.static import AttachType, Opcode
|
20
|
+
from pymax.types import Attach, Message
|
21
|
+
|
22
|
+
|
23
|
+
class MessageMixin(ClientProtocol):
|
24
|
+
async def _upload_photo(self, photo: Photo) -> None | Attach:
|
25
|
+
try:
|
26
|
+
self.logger.info("Uploading photo")
|
27
|
+
payload = UploadPhotoPayload().model_dump(by_alias=True)
|
28
|
+
|
29
|
+
data = await self._send_and_wait(
|
30
|
+
opcode=Opcode.PHOTO_UPLOAD,
|
31
|
+
payload=payload,
|
32
|
+
)
|
33
|
+
if error := data.get("payload", {}).get("error"):
|
34
|
+
self.logger.error("Upload photo error: %s", error)
|
35
|
+
return None
|
36
|
+
|
37
|
+
url = data.get("payload", {}).get("url")
|
38
|
+
if not url:
|
39
|
+
self.logger.error("No upload URL received")
|
40
|
+
return None
|
41
|
+
|
42
|
+
photo_data = photo.validate_photo()
|
43
|
+
if not photo_data:
|
44
|
+
self.logger.error("Photo validation failed")
|
45
|
+
return None
|
46
|
+
|
47
|
+
form = aiohttp.FormData()
|
48
|
+
form.add_field(
|
49
|
+
name="file",
|
50
|
+
value=await photo.read(),
|
51
|
+
filename=f"image.{photo_data[0]}",
|
52
|
+
content_type=photo_data[1],
|
53
|
+
)
|
54
|
+
|
55
|
+
async with (
|
56
|
+
ClientSession() as session,
|
57
|
+
session.post(
|
58
|
+
url=url,
|
59
|
+
data=form,
|
60
|
+
) as response,
|
61
|
+
):
|
62
|
+
if response.status != 200:
|
63
|
+
self.logger.error(f"Upload failed with status {response.status}")
|
64
|
+
return None
|
65
|
+
|
66
|
+
result = await response.json()
|
67
|
+
|
68
|
+
if not result.get("photos"):
|
69
|
+
self.logger.error("No photos in response")
|
70
|
+
return None
|
71
|
+
|
72
|
+
photo_data = next(iter(result["photos"].values()), None)
|
73
|
+
if not photo_data or "token" not in photo_data:
|
74
|
+
self.logger.error("No token in response")
|
75
|
+
return None
|
76
|
+
|
77
|
+
return Attach(
|
78
|
+
_type=AttachType.PHOTO,
|
79
|
+
photo_token=photo_data["token"],
|
80
|
+
)
|
81
|
+
|
82
|
+
except Exception as e:
|
83
|
+
self.logger.exception("Upload photo failed: %s", str(e))
|
84
|
+
return None
|
85
|
+
|
86
|
+
async def send_message(
|
87
|
+
self,
|
88
|
+
text: str,
|
89
|
+
chat_id: int,
|
90
|
+
notify: bool,
|
91
|
+
photo: Photo | None = None,
|
92
|
+
photos: list[Photo] | None = None,
|
93
|
+
reply_to: int | None = None,
|
94
|
+
) -> Message | None:
|
95
|
+
"""
|
96
|
+
Отправляет сообщение в чат.
|
97
|
+
"""
|
98
|
+
try:
|
99
|
+
self.logger.info("Sending message to chat_id=%s notify=%s", chat_id, notify)
|
100
|
+
if photos and photo:
|
101
|
+
self.logger.warning("Both photo and photos provided; using photos")
|
102
|
+
photo = None
|
103
|
+
attaches = []
|
104
|
+
if photo:
|
105
|
+
self.logger.info("Uploading photo for message")
|
106
|
+
attach = await self._upload_photo(photo)
|
107
|
+
if not attach or not attach.photo_token:
|
108
|
+
self.logger.error("Photo upload failed, message not sent")
|
109
|
+
return None
|
110
|
+
attaches = [
|
111
|
+
AttachPhotoPayload(photo_token=attach.photo_token).model_dump(
|
112
|
+
by_alias=True
|
113
|
+
)
|
114
|
+
]
|
115
|
+
elif photos:
|
116
|
+
self.logger.info("Uploading multiple photos for message")
|
117
|
+
for p in photos:
|
118
|
+
attach = await self._upload_photo(p)
|
119
|
+
if attach and attach.photo_token:
|
120
|
+
attaches.append(
|
121
|
+
AttachPhotoPayload(
|
122
|
+
photo_token=attach.photo_token
|
123
|
+
).model_dump(by_alias=True)
|
124
|
+
)
|
125
|
+
if not attaches:
|
126
|
+
self.logger.error("All photo uploads failed, message not sent")
|
127
|
+
return None
|
128
|
+
|
129
|
+
payload = SendMessagePayload(
|
130
|
+
chat_id=chat_id,
|
131
|
+
message=SendMessagePayloadMessage(
|
132
|
+
text=text,
|
133
|
+
cid=int(time.time() * 1000),
|
134
|
+
elements=[],
|
135
|
+
attaches=attaches,
|
136
|
+
link=ReplyLink(message_id=str(reply_to)) if reply_to else None,
|
137
|
+
),
|
138
|
+
notify=notify,
|
139
|
+
).model_dump(by_alias=True)
|
140
|
+
|
141
|
+
data = await self._send_and_wait(opcode=Opcode.MSG_SEND, payload=payload)
|
142
|
+
if error := data.get("payload", {}).get("error"):
|
143
|
+
self.logger.error("Send message error: %s", error)
|
144
|
+
print(data)
|
145
|
+
return None
|
146
|
+
msg = (
|
147
|
+
Message.from_dict(data["payload"]["message"])
|
148
|
+
if data.get("payload")
|
149
|
+
else None
|
150
|
+
)
|
151
|
+
self.logger.debug("send_message result: %r", msg)
|
152
|
+
return msg
|
153
|
+
except Exception:
|
154
|
+
self.logger.exception("Send message failed")
|
155
|
+
return None
|
156
|
+
|
157
|
+
async def edit_message(
|
158
|
+
self, chat_id: int, message_id: int, text: str
|
159
|
+
) -> Message | None:
|
160
|
+
"""
|
161
|
+
Редактирует сообщение.
|
162
|
+
"""
|
163
|
+
try:
|
164
|
+
self.logger.info(
|
165
|
+
"Editing message chat_id=%s message_id=%s", chat_id, message_id
|
166
|
+
)
|
167
|
+
payload = EditMessagePayload(
|
168
|
+
chat_id=chat_id,
|
169
|
+
message_id=message_id,
|
170
|
+
text=text,
|
171
|
+
elements=[],
|
172
|
+
attaches=[],
|
173
|
+
).model_dump(by_alias=True)
|
174
|
+
data = await self._send_and_wait(opcode=Opcode.MSG_EDIT, payload=payload)
|
175
|
+
if error := data.get("payload", {}).get("error"):
|
176
|
+
self.logger.error("Edit message error: %s", error)
|
177
|
+
msg = (
|
178
|
+
Message.from_dict(data["payload"]["message"])
|
179
|
+
if data.get("payload")
|
180
|
+
else None
|
181
|
+
)
|
182
|
+
self.logger.debug("edit_message result: %r", msg)
|
183
|
+
return msg
|
184
|
+
except Exception:
|
185
|
+
self.logger.exception("Edit message failed")
|
186
|
+
return None
|
187
|
+
|
188
|
+
async def delete_message(
|
189
|
+
self, chat_id: int, message_ids: list[int], for_me: bool
|
190
|
+
) -> bool:
|
191
|
+
"""
|
192
|
+
Удаляет сообщения.
|
193
|
+
"""
|
194
|
+
try:
|
195
|
+
self.logger.info(
|
196
|
+
"Deleting messages chat_id=%s ids=%s for_me=%s",
|
197
|
+
chat_id,
|
198
|
+
message_ids,
|
199
|
+
for_me,
|
200
|
+
)
|
201
|
+
|
202
|
+
payload = DeleteMessagePayload(
|
203
|
+
chat_id=chat_id, message_ids=message_ids, for_me=for_me
|
204
|
+
).model_dump(by_alias=True)
|
205
|
+
|
206
|
+
data = await self._send_and_wait(opcode=Opcode.MSG_DELETE, payload=payload)
|
207
|
+
if error := data.get("payload", {}).get("error"):
|
208
|
+
self.logger.error("Delete message error: %s", error)
|
209
|
+
return False
|
210
|
+
self.logger.debug("delete_message success")
|
211
|
+
return True
|
212
|
+
except Exception:
|
213
|
+
self.logger.exception("Delete message failed")
|
214
|
+
return False
|
215
|
+
|
216
|
+
async def pin_message(
|
217
|
+
self, chat_id: int, message_id: int, notify_pin: bool
|
218
|
+
) -> bool:
|
219
|
+
"""
|
220
|
+
Закрепляет сообщение.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
chat_id (int): ID чата
|
224
|
+
message_id (int): ID сообщения
|
225
|
+
notify_pin (bool): Оповещать о закреплении
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
bool: True, если сообщение закреплено
|
229
|
+
"""
|
230
|
+
try:
|
231
|
+
payload = PinMessagePayload(
|
232
|
+
chat_id=chat_id,
|
233
|
+
notify_pin=notify_pin,
|
234
|
+
pin_message_id=message_id,
|
235
|
+
).model_dump(by_alias=True)
|
236
|
+
|
237
|
+
data = await self._send_and_wait(opcode=Opcode.CHAT_UPDATE, payload=payload)
|
238
|
+
if error := data.get("payload", {}).get("error"):
|
239
|
+
self.logger.error("Pin message error: %s", error)
|
240
|
+
return False
|
241
|
+
self.logger.debug("pin_message success")
|
242
|
+
return True
|
243
|
+
except Exception:
|
244
|
+
self.logger.exception("Pin message failed")
|
245
|
+
return False
|
246
|
+
|
247
|
+
async def fetch_history(
|
248
|
+
self,
|
249
|
+
chat_id: int,
|
250
|
+
from_time: int | None = None,
|
251
|
+
forward: int = 0,
|
252
|
+
backward: int = 200,
|
253
|
+
) -> list[Message] | None:
|
254
|
+
"""
|
255
|
+
Получает историю сообщений чата.
|
256
|
+
"""
|
257
|
+
if from_time is None:
|
258
|
+
from_time = int(time.time() * 1000)
|
259
|
+
|
260
|
+
try:
|
261
|
+
self.logger.info(
|
262
|
+
"Fetching history chat_id=%s from=%s forward=%s backward=%s",
|
263
|
+
chat_id,
|
264
|
+
from_time,
|
265
|
+
forward,
|
266
|
+
backward,
|
267
|
+
)
|
268
|
+
|
269
|
+
payload = FetchHistoryPayload(
|
270
|
+
chat_id=chat_id,
|
271
|
+
from_time=from_time, # pyright: ignore[reportCallIssue] FIXME: Pydantic Field alias
|
272
|
+
forward=forward,
|
273
|
+
backward=backward,
|
274
|
+
).model_dump(by_alias=True)
|
275
|
+
|
276
|
+
self.logger.debug("Payload dict keys: %s", list(payload.keys()))
|
277
|
+
|
278
|
+
data = await self._send_and_wait(
|
279
|
+
opcode=Opcode.CHAT_HISTORY, payload=payload, timeout=10
|
280
|
+
)
|
281
|
+
|
282
|
+
if error := data.get("payload", {}).get("error"):
|
283
|
+
self.logger.error("Fetch history error: %s", error)
|
284
|
+
return None
|
285
|
+
|
286
|
+
messages = [
|
287
|
+
Message.from_dict(msg) for msg in data["payload"].get("messages", [])
|
288
|
+
]
|
289
|
+
self.logger.debug("History fetched: %d messages", len(messages))
|
290
|
+
return messages
|
291
|
+
except Exception:
|
292
|
+
self.logger.exception("Fetch history failed")
|
293
|
+
return None
|