Skip to content

Commit 8c40b71

Browse files
Merge pull request #68 from MaxApiTeam/dev/2.3.1
Dev/2.3.1
2 parents ff3af5b + c712108 commit 8c40b71

19 files changed

Lines changed: 437 additions & 23 deletions

docs/client.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,9 @@ Debug-логи показывают handshake, login, входящие собы
300300
Клиент собирает несколько API-направлений:
301301

302302
Сообщения
303-
``send_message()``, ``fetch_history()``, ``delete_message()``,
304-
``pin_message()``, ``read_message()``, реакции и получение URL для входящих
305-
файлов/видео.
303+
``send_message()``, ``forward_message()``, ``fetch_history()``,
304+
``delete_message()``, ``pin_message()``, ``read_message()``, реакции и
305+
получение URL для входящих файлов/видео.
306306

307307
Чаты
308308
``get_chat()``, ``fetch_chats()``, создание групп, invite-ссылки,

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ PyMax - асинхронная Python-библиотека для Max API. Он
2222
:maxdepth: 1
2323
:caption: Новости
2424

25+
release-2-3-1
2526
release-2-3-0
2627
release-2-2-0
2728
release-2-1-3

docs/messages.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ Messages
7070
async def on_message(message: Message, client: Client) -> None:
7171
await message.answer("Ответ в тот же чат")
7272
await message.reply("Ответ реплаем")
73+
await message.forward(chat_id=654321)
74+
75+
Переслать сообщение напрямую через клиент можно с указанием исходного и
76+
целевого чатов:
77+
78+
.. code-block:: python
79+
80+
await client.forward_message(
81+
chat_id=654321,
82+
message_id=987654,
83+
source_chat_id=123456,
84+
)
7385
7486
Ответ, реакции, удаление и прочтение
7587
----------------------------------------

docs/release-2-3-1.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
PyMax 2.3.1
2+
===========
3+
4+
Изменения относительно ``2.3.0``.
5+
6+
Добавлено
7+
---------
8+
9+
* ``forward_message()`` на клиенте и ``Message.forward()`` на bound-объекте
10+
сообщения. Для пересылки между разными чатами укажите ``source_chat_id``.
11+
12+
Исправлено
13+
----------
14+
15+
* Декодирование сжатых TCP payload-ов: коэффициенты LZ4 теперь обрабатываются
16+
корректно, а payload-ы с флагом ``0xFF`` декодируются через Zstandard.
17+
* Разбор профилей bot-аккаунтов, в которых ``gender`` приходит числом, а
18+
``web_app`` — URL-строкой.
19+
20+
Зависимости
21+
-----------
22+
23+
* Добавлена runtime-зависимость ``zstandard`` для декодирования TCP payload-ов.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "maxapi-python"
3-
version = "2.3.0"
3+
version = "2.3.1"
44
description = "Python wrapper для API мессенджера Max"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -31,6 +31,7 @@ dependencies = [
3131
"python-socks[asyncio]>=2.8.1",
3232
"qrcode>=8.2",
3333
"websockets>=16.0",
34+
"zstandard>=0.25.0",
3435
]
3536

3637
[project.urls]

src/pymax/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.3.0"
1+
__version__ = "2.3.1"
22

33

44
from .auth import (

src/pymax/api/messages/payloads.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, Literal
22

33
from pydantic import Field
44

@@ -46,6 +46,26 @@ class SendMessagePayload(CamelModel):
4646
notify: bool = False
4747

4848

49+
class ForwardLink(CamelModel):
50+
type: Literal["FORWARD"] = "FORWARD"
51+
message_id: str
52+
chat_id: int
53+
54+
55+
class ForwardMessagePayloadMessage(CamelModel):
56+
cid: int
57+
link: ForwardLink
58+
attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
59+
default_factory=list
60+
)
61+
62+
63+
class ForwardMessagePayload(CamelModel):
64+
chat_id: int
65+
message: ForwardMessagePayloadMessage
66+
notify: bool = True
67+
68+
4969
class ChatHistoryPayload(CamelModel):
5070
chat_id: int
5171
forward: int

src/pymax/api/messages/service.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
ChatHistoryPayload,
3737
DeleteMessagePayload,
3838
EditMessagePayload,
39+
ForwardLink,
40+
ForwardMessagePayload,
41+
ForwardMessagePayloadMessage,
3942
GetFilePayload,
4043
GetMessagesPayload,
4144
GetReactionsPayload,
@@ -139,6 +142,42 @@ async def send_message(
139142
logger.info("message sent chat_id=%s", chat_id)
140143
return message
141144

145+
async def forward_message(
146+
self,
147+
chat_id: int,
148+
message_id: int | str,
149+
source_chat_id: int | None = None,
150+
*,
151+
notify: bool = True,
152+
) -> Message | None:
153+
source_chat_id = chat_id if source_chat_id is None else source_chat_id
154+
logger.info(
155+
"forwarding message source_chat_id=%s chat_id=%s message_id=%s",
156+
source_chat_id,
157+
chat_id,
158+
message_id,
159+
)
160+
161+
frame = ForwardMessagePayload(
162+
chat_id=chat_id,
163+
message=ForwardMessagePayloadMessage(
164+
cid=-self._next_cid(),
165+
link=ForwardLink(
166+
message_id=str(message_id),
167+
chat_id=source_chat_id,
168+
),
169+
),
170+
notify=notify,
171+
)
172+
173+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
174+
message = bind_api_model(
175+
self.app,
176+
require_payload_model(response, Message),
177+
)
178+
logger.info("message forwarded source_chat_id=%s chat_id=%s", source_chat_id, chat_id)
179+
return message
180+
142181
async def get_messages(
143182
self,
144183
chat_id: int,

src/pymax/infra/message.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,33 @@ async def get_message(
6262
message_id=message_id,
6363
)
6464

65+
async def forward_message(
66+
self,
67+
chat_id: int,
68+
message_id: int | str,
69+
source_chat_id: int | None = None,
70+
*,
71+
notify: bool = True,
72+
) -> Message | None:
73+
"""Пересылает существующее сообщение в чат.
74+
75+
Args:
76+
chat_id: ID целевого чата.
77+
message_id: ID пересылаемого сообщения.
78+
source_chat_id: ID исходного чата. Если не указан, используется
79+
целевой чат.
80+
notify: Отправить ли получателям push-уведомление.
81+
82+
Returns:
83+
Пересланное сообщение или ``None``, если сервер не вернул его.
84+
"""
85+
return await self._app.api.messages.forward_message(
86+
chat_id=chat_id,
87+
message_id=message_id,
88+
source_chat_id=source_chat_id,
89+
notify=notify,
90+
)
91+
6592
async def get_messages(
6693
self,
6794
chat_id: int,

src/pymax/protocol/tcp/compression.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
from io import BytesIO
2+
3+
import zstandard
4+
5+
16
class Lz4BlockCompression:
27
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
38
dst = bytearray()
@@ -95,3 +100,16 @@ def compress(self, src: bytes) -> bytes:
95100
dst.extend(src[lit_start : lit_start + lit_len])
96101

97102
return bytes(dst)
103+
104+
105+
class ZstdCompression:
106+
def decompress(self, src: bytes, max_output: int = 5 * 1024 * 1024) -> bytes:
107+
try:
108+
with zstandard.ZstdDecompressor().stream_reader(BytesIO(src)) as reader:
109+
result = reader.read(max_output + 1)
110+
except zstandard.ZstdError as e:
111+
raise ValueError("Zstd: failed to decompress payload") from e
112+
113+
if len(result) > max_output:
114+
raise ValueError("Zstd: output too large")
115+
return result

0 commit comments

Comments
 (0)