maxapi-python 2.3.1__py3-none-any.whl → 2.4.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.3.1.dist-info → maxapi_python-2.4.0.dist-info}/METADATA +3 -1
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/RECORD +59 -51
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +7 -2
- pymax/_data/apk_fingerprints.json +682 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +88 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +16 -1
- pymax/api/chats/service.py +36 -1
- pymax/api/messages/payloads.py +12 -4
- pymax/api/messages/service.py +57 -14
- pymax/api/self/__init__.py +2 -1
- pymax/api/self/enums.py +9 -0
- pymax/api/self/payloads.py +40 -1
- pymax/api/self/service.py +37 -0
- pymax/api/session/service.py +21 -8
- pymax/api/uploads/payloads.py +6 -0
- pymax/api/uploads/service.py +198 -21
- pymax/app.py +69 -15
- pymax/base.py +8 -0
- pymax/client_web.py +2 -2
- pymax/config.py +44 -7
- pymax/connection/connection.py +29 -18
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +3 -1
- pymax/dispatch/resolvers.py +7 -1
- pymax/files/__init__.py +4 -1
- pymax/files/video.py +58 -0
- pymax/files/voice.py +39 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +47 -0
- pymax/fingerprint/models.py +11 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +38 -0
- pymax/infra/message.py +40 -11
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- pymax/transport/tcp.py +6 -1
- pymax/transport/websocket.py +6 -3
- pymax/types/domain/__init__.py +2 -1
- pymax/types/domain/attachments/__init__.py +2 -1
- pymax/types/domain/attachments/enums.py +17 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +29 -8
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +21 -20
- pymax/types/domain/user.py +1 -3
- pymax/types/events/__init__.py +1 -0
- pymax/types/events/reaction.py +2 -2
- pymax/types/events/voice.py +5 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
pymax/protocol/tcp/payload.py
CHANGED
|
@@ -11,6 +11,8 @@ logger = get_logger(__name__)
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MsgpackPayloadCodec:
|
|
14
|
+
WRAPPED_VALUE_EXT_CODE = 1
|
|
15
|
+
|
|
14
16
|
def _to_msgpack_value(self, value: Any) -> Any:
|
|
15
17
|
if isinstance(value, Enum):
|
|
16
18
|
return value.value
|
|
@@ -30,10 +32,25 @@ class MsgpackPayloadCodec:
|
|
|
30
32
|
def _unpack_stream(
|
|
31
33
|
self, payload_bytes: bytes, *, raw: bool
|
|
32
34
|
) -> list[Any]: # TODO: deprecate? idk
|
|
33
|
-
unpacker = msgpack.Unpacker(
|
|
35
|
+
unpacker = msgpack.Unpacker(
|
|
36
|
+
raw=raw,
|
|
37
|
+
strict_map_key=False,
|
|
38
|
+
ext_hook=self._decode_ext,
|
|
39
|
+
)
|
|
34
40
|
unpacker.feed(payload_bytes)
|
|
35
41
|
return list(unpacker)
|
|
36
42
|
|
|
43
|
+
def _decode_ext(self, code: int, data: bytes) -> Any:
|
|
44
|
+
if code != self.WRAPPED_VALUE_EXT_CODE:
|
|
45
|
+
return msgpack.ExtType(code, data)
|
|
46
|
+
|
|
47
|
+
return msgpack.unpackb(
|
|
48
|
+
data,
|
|
49
|
+
raw=False,
|
|
50
|
+
strict_map_key=False,
|
|
51
|
+
ext_hook=self._decode_ext,
|
|
52
|
+
)
|
|
53
|
+
|
|
37
54
|
def decode(self, payload_bytes: bytes) -> Any:
|
|
38
55
|
if not payload_bytes:
|
|
39
56
|
return {}
|
|
@@ -43,6 +60,7 @@ class MsgpackPayloadCodec:
|
|
|
43
60
|
payload_bytes,
|
|
44
61
|
raw=False,
|
|
45
62
|
strict_map_key=False,
|
|
63
|
+
ext_hook=self._decode_ext,
|
|
46
64
|
)
|
|
47
65
|
|
|
48
66
|
except msgpack.exceptions.ExtraData as e:
|
|
@@ -81,6 +99,8 @@ class TcpPayloadDecoder:
|
|
|
81
99
|
return {self._normalize_key(k): self._normalize_keys(v) for k, v in obj.items()}
|
|
82
100
|
if isinstance(obj, list):
|
|
83
101
|
return [self._normalize_keys(item) for item in obj]
|
|
102
|
+
if isinstance(obj, msgpack.ExtType):
|
|
103
|
+
return obj
|
|
84
104
|
if isinstance(obj, tuple):
|
|
85
105
|
return tuple(self._normalize_keys(item) for item in obj)
|
|
86
106
|
return obj
|
pymax/transport/tcp.py
CHANGED
|
@@ -7,6 +7,7 @@ from pymax.logging import get_logger
|
|
|
7
7
|
from .base import Transport
|
|
8
8
|
|
|
9
9
|
logger = get_logger(__name__)
|
|
10
|
+
_CLOSE_TIMEOUT = 5.0
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TCPTransport(Transport):
|
|
@@ -66,7 +67,11 @@ class TCPTransport(Transport):
|
|
|
66
67
|
if writer:
|
|
67
68
|
logger.debug("tcp close")
|
|
68
69
|
writer.close()
|
|
69
|
-
|
|
70
|
+
try:
|
|
71
|
+
await asyncio.wait_for(writer.wait_closed(), _CLOSE_TIMEOUT)
|
|
72
|
+
except (OSError, TimeoutError) as e:
|
|
73
|
+
logger.warning("tcp close did not finish cleanly: %s", e)
|
|
74
|
+
writer.transport.abort()
|
|
70
75
|
logger.debug("tcp closed")
|
|
71
76
|
|
|
72
77
|
async def send(self, data: bytes | str) -> None:
|
pymax/transport/websocket.py
CHANGED
|
@@ -17,11 +17,14 @@ class WebSocketTransport(Transport):
|
|
|
17
17
|
async def connect(self) -> None:
|
|
18
18
|
if self.proxy:
|
|
19
19
|
self.ws = await client.connect(
|
|
20
|
-
self.url,
|
|
20
|
+
self.url,
|
|
21
|
+
origin=Origin("https://web.max.ru"),
|
|
22
|
+
proxy=self.proxy,
|
|
23
|
+
max_size=1024 * 1024 * 10, # 10 MB
|
|
21
24
|
)
|
|
22
25
|
else:
|
|
23
26
|
self.ws = await client.connect(
|
|
24
|
-
self.url, origin=Origin("https://web.max.ru")
|
|
27
|
+
self.url, origin=Origin("https://web.max.ru"), max_size=1024 * 1024 * 10
|
|
25
28
|
) # TODO: origin should be configurable
|
|
26
29
|
|
|
27
30
|
async def close(self) -> None:
|
|
@@ -41,7 +44,7 @@ class WebSocketTransport(Transport):
|
|
|
41
44
|
if self.ws is None or not self.connected:
|
|
42
45
|
raise ConnectionError("Not connected to the server")
|
|
43
46
|
|
|
44
|
-
return await self.ws.recv(decode=
|
|
47
|
+
return await self.ws.recv(decode=False)
|
|
45
48
|
|
|
46
49
|
@property
|
|
47
50
|
def connected(self) -> bool:
|
pymax/types/domain/__init__.py
CHANGED
|
@@ -3,7 +3,8 @@ from .bots import InitData
|
|
|
3
3
|
from .chat import Chat
|
|
4
4
|
from .error import MaxApiError
|
|
5
5
|
from .folder import Folder, FolderList, FolderUpdate
|
|
6
|
-
from .
|
|
6
|
+
from .handshake import HandshakeResponse
|
|
7
|
+
from .login import Login2Flags, Login2Response, LoginResponse
|
|
7
8
|
from .member import Member
|
|
8
9
|
from .message import Message, ReactionCounter, ReactionInfo, ReadState
|
|
9
10
|
from .name import Name
|
|
@@ -2,10 +2,11 @@ from .audio import AudioAttachment
|
|
|
2
2
|
from .call import CallAttachment
|
|
3
3
|
from .contact import ContactAttachment
|
|
4
4
|
from .control import ControlAttachment
|
|
5
|
-
from .enums import AttachmentType
|
|
5
|
+
from .enums import AttachmentType, PollFlags
|
|
6
6
|
from .file import FileAttachment, FileRequest
|
|
7
7
|
from .keyboards import InlineKeyboardAttachment
|
|
8
8
|
from .photo import PhotoAttachment
|
|
9
|
+
from .poll import Poll, PollAnswer, PollAttachment, PollState
|
|
9
10
|
from .share import ShareAttachment
|
|
10
11
|
from .sticker import StickerAttachment
|
|
11
12
|
from .unknown import UnknownAttachment
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from enum import Enum
|
|
1
|
+
from enum import Enum, IntFlag
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class AttachmentType(str, Enum):
|
|
@@ -14,6 +14,7 @@ class AttachmentType(str, Enum):
|
|
|
14
14
|
CALL = "CALL"
|
|
15
15
|
SHARE = "SHARE"
|
|
16
16
|
INLINE_KEYBOARD = "INLINE_KEYBOARD"
|
|
17
|
+
POLL = "POLL"
|
|
17
18
|
UNKNOWN = "UNKNOWN"
|
|
18
19
|
|
|
19
20
|
|
|
@@ -26,3 +27,18 @@ class TranscriptionStatus(str, Enum):
|
|
|
26
27
|
PROCESSING = "PROCESSING"
|
|
27
28
|
SUCCESS = "SUCCESS"
|
|
28
29
|
UNKNOWN = "UNKNOWN"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PollFlags(IntFlag):
|
|
33
|
+
"""Настройки опроса, представленные битовой маской.
|
|
34
|
+
|
|
35
|
+
Несколько настроек объединяются оператором ``|``. Числовые значения,
|
|
36
|
+
полученные от Max, автоматически преобразуются в ``PollFlags``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
FLAG_SETTINGS_ANONYMOUS = 1
|
|
40
|
+
FLAG_SETTINGS_MULTISELECT = 2
|
|
41
|
+
FLAG_SETTINGS_REVOTE = 4
|
|
42
|
+
FLAG_SETTINGS_CLOSED = 8
|
|
43
|
+
FLAG_SETTINGS_QUIZ = 16
|
|
44
|
+
FLAG_SETTINGS_CAN_FORWARD = 32
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from pymax.types.domain.base import CamelModel
|
|
6
|
+
|
|
7
|
+
from .enums import AttachmentType, PollFlags
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PollAnswer(CamelModel):
|
|
11
|
+
"""Вариант ответа в опросе.
|
|
12
|
+
|
|
13
|
+
:ivar text: Текст варианта ответа.
|
|
14
|
+
:vartype text: str
|
|
15
|
+
:ivar answer_id: ID варианта, назначенный Max.
|
|
16
|
+
:vartype answer_id: int | None
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
text: str
|
|
20
|
+
answer_id: int | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PollVote(CamelModel):
|
|
24
|
+
"""Голос пользователя за один вариант ответа.
|
|
25
|
+
|
|
26
|
+
:ivar timestamp: Время голосования в формате Unix time.
|
|
27
|
+
:vartype timestamp: int
|
|
28
|
+
:ivar user_id: ID проголосовавшего пользователя.
|
|
29
|
+
:vartype user_id: int
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
timestamp: int
|
|
33
|
+
user_id: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PollResult(CamelModel):
|
|
37
|
+
"""Результат голосования по одному варианту ответа.
|
|
38
|
+
|
|
39
|
+
:ivar answer_id: ID варианта ответа.
|
|
40
|
+
:vartype answer_id: int
|
|
41
|
+
:ivar vote_count: Количество голосов.
|
|
42
|
+
:vartype vote_count: int
|
|
43
|
+
:ivar votes: Голоса пользователей, доступные текущему аккаунту.
|
|
44
|
+
:vartype votes: list[PollVote]
|
|
45
|
+
:ivar rate: Доля голосов в формате, возвращаемом Max.
|
|
46
|
+
:vartype rate: int
|
|
47
|
+
:ivar options: Дополнительные параметры результата от Max.
|
|
48
|
+
:vartype options: int
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
answer_id: int
|
|
52
|
+
vote_count: int
|
|
53
|
+
votes: list[PollVote]
|
|
54
|
+
rate: int
|
|
55
|
+
options: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PollState(CamelModel):
|
|
59
|
+
"""Текущее состояние голосования.
|
|
60
|
+
|
|
61
|
+
:ivar total: Общее количество голосов.
|
|
62
|
+
:vartype total: int
|
|
63
|
+
:ivar result: Результаты по вариантам ответа.
|
|
64
|
+
:vartype result: list[PollResult] | None
|
|
65
|
+
:ivar voter_preview_ids: ID пользователей для предпросмотра списка
|
|
66
|
+
проголосовавших.
|
|
67
|
+
:vartype voter_preview_ids: list[int]
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
total: int = 0
|
|
71
|
+
result: list[PollResult] | None = None
|
|
72
|
+
voter_preview_ids: list[int]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Poll(CamelModel):
|
|
76
|
+
"""Опрос для отправки в сообщении.
|
|
77
|
+
|
|
78
|
+
:ivar title: Вопрос или заголовок опроса.
|
|
79
|
+
:vartype title: str
|
|
80
|
+
:ivar answers: Варианты ответа.
|
|
81
|
+
:vartype answers: list[PollAnswer]
|
|
82
|
+
:ivar settings: Настройки опроса. Несколько ``PollFlags`` объединяются
|
|
83
|
+
оператором ``|``.
|
|
84
|
+
:vartype settings: PollFlags
|
|
85
|
+
:ivar type: Тип вложения.
|
|
86
|
+
:vartype type: Literal[AttachmentType.POLL]
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
title: str
|
|
90
|
+
answers: list[PollAnswer]
|
|
91
|
+
settings: PollFlags
|
|
92
|
+
type: Literal[AttachmentType.POLL] = Field(alias="_type", default=AttachmentType.POLL)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PollAttachment(Poll):
|
|
96
|
+
"""Опрос, полученный как вложение сообщения.
|
|
97
|
+
|
|
98
|
+
Помимо параметров опроса содержит назначенный Max ID, версию и текущее
|
|
99
|
+
состояние голосования.
|
|
100
|
+
|
|
101
|
+
:ivar poll_id: ID опроса.
|
|
102
|
+
:vartype poll_id: int
|
|
103
|
+
:ivar version: Версия данных опроса.
|
|
104
|
+
:vartype version: int
|
|
105
|
+
:ivar state: Текущее состояние голосования.
|
|
106
|
+
:vartype state: PollState
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
poll_id: int
|
|
110
|
+
version: int
|
|
111
|
+
state: PollState
|
|
@@ -62,18 +62,18 @@ class VideoRequest(CamelModel):
|
|
|
62
62
|
:vartype external: str | bool | None
|
|
63
63
|
:ivar cache: Использовать ли кеш.
|
|
64
64
|
:vartype cache: bool
|
|
65
|
-
:ivar url: URL видео.
|
|
66
|
-
:vartype url: str
|
|
65
|
+
:ivar url: Прямой URL видео или ``None`` для внешнего видео.
|
|
66
|
+
:vartype url: str | None
|
|
67
67
|
"""
|
|
68
68
|
|
|
69
69
|
external: str | bool | None = Field(default=None, alias="EXTERNAL")
|
|
70
|
-
cache: bool
|
|
71
|
-
url: str
|
|
70
|
+
cache: bool = False # TODO: idk maybe | None = None better
|
|
71
|
+
url: str | None = None
|
|
72
72
|
|
|
73
73
|
@model_validator(mode="before")
|
|
74
74
|
@classmethod
|
|
75
|
-
def
|
|
76
|
-
"""
|
|
75
|
+
def select_video_url(cls, value: Any) -> Any:
|
|
76
|
+
"""Выбирает прямой URL с максимальным доступным MP4-качеством.
|
|
77
77
|
|
|
78
78
|
:param value: Значение, переданное в валидатор модели.
|
|
79
79
|
:type value: Any
|
|
@@ -83,8 +83,29 @@ class VideoRequest(CamelModel):
|
|
|
83
83
|
if not isinstance(value, dict) or "url" in value:
|
|
84
84
|
return value
|
|
85
85
|
|
|
86
|
+
mp4_urls: list[tuple[int, str]] = []
|
|
86
87
|
for key, url in value.items():
|
|
87
|
-
if key not
|
|
88
|
-
|
|
88
|
+
if not isinstance(key, str) or not isinstance(url, str):
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
normalized_key = key.upper()
|
|
92
|
+
if not normalized_key.startswith("MP4_"):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
quality = int(normalized_key.removeprefix("MP4_"))
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if quality > 0:
|
|
101
|
+
mp4_urls.append((quality, url))
|
|
102
|
+
|
|
103
|
+
if mp4_urls:
|
|
104
|
+
_, url = max(mp4_urls, key=lambda item: item[0])
|
|
105
|
+
return {**value, "url": url}
|
|
106
|
+
|
|
107
|
+
legacy_url = value.get("dynamicUrl", value.get("dynamic_url"))
|
|
108
|
+
if isinstance(legacy_url, str):
|
|
109
|
+
return {**value, "url": legacy_url}
|
|
89
110
|
|
|
90
111
|
return value
|
pymax/types/domain/chat.py
CHANGED
|
@@ -156,25 +156,24 @@ class Chat(CamelModel):
|
|
|
156
156
|
|
|
157
157
|
async def answer(
|
|
158
158
|
self,
|
|
159
|
-
text: str,
|
|
159
|
+
text: str | None = None,
|
|
160
160
|
reply_to: int | None = None,
|
|
161
161
|
attachments: SendAttachments = None,
|
|
162
162
|
*,
|
|
163
163
|
notify: bool = True,
|
|
164
|
-
) -> Message
|
|
164
|
+
) -> Message:
|
|
165
165
|
"""Отправляет сообщение в этот чат.
|
|
166
166
|
|
|
167
167
|
:param text: Текст сообщения.
|
|
168
|
-
:type text: str
|
|
168
|
+
:type text: str | None
|
|
169
169
|
:param reply_to: ID сообщения для ответа.
|
|
170
170
|
:type reply_to: int | None
|
|
171
171
|
:param attachments: Файлы, фотографии или видео для отправки.
|
|
172
172
|
:type attachments: SendAttachments
|
|
173
173
|
:param notify: Отправить ли получателям push-уведомление.
|
|
174
174
|
:type notify: bool
|
|
175
|
-
:returns: Отправленное
|
|
176
|
-
|
|
177
|
-
:rtype: Message | None
|
|
175
|
+
:returns: Отправленное сообщение.
|
|
176
|
+
:rtype: Message
|
|
178
177
|
:raises RuntimeError: Если чат не привязан к клиенту.
|
|
179
178
|
"""
|
|
180
179
|
actions, _ = self._bound()
|
|
@@ -198,7 +197,7 @@ class Chat(CamelModel):
|
|
|
198
197
|
get_chat: bool = False,
|
|
199
198
|
get_messages: bool = True,
|
|
200
199
|
interactive: bool = False,
|
|
201
|
-
) -> list[Message]
|
|
200
|
+
) -> list[Message]:
|
|
202
201
|
"""Загружает историю сообщений этого чата.
|
|
203
202
|
|
|
204
203
|
``from_time``, ``backward_time`` и ``forward_time`` передаются в
|
|
@@ -224,8 +223,9 @@ class Chat(CamelModel):
|
|
|
224
223
|
:type get_messages: bool
|
|
225
224
|
:param interactive: Пометить запрос как интерактивный.
|
|
226
225
|
:type interactive: bool
|
|
227
|
-
:returns:
|
|
228
|
-
|
|
226
|
+
:returns: Список сообщений. Если сервер не вернул сообщения, список
|
|
227
|
+
пуст.
|
|
228
|
+
:rtype: list[Message]
|
|
229
229
|
:raises RuntimeError: Если чат не привязан к клиенту.
|
|
230
230
|
"""
|
|
231
231
|
actions, _ = self._bound()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from .base import CamelModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HandshakeResponse(CamelModel):
|
|
7
|
+
"""Результат рукопожатия.
|
|
8
|
+
|
|
9
|
+
:ivar calls_seed: Сид для генерации хэшей вызовов.
|
|
10
|
+
:vartype calls_seed: int
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
calls_seed: int | None = None
|
|
14
|
+
app_update_type: int | None = Field(
|
|
15
|
+
alias="app-update-type", default=None
|
|
16
|
+
) # very bizarre casing
|
pymax/types/domain/login.py
CHANGED
|
@@ -13,14 +13,26 @@ class LoginConfig(CamelModel):
|
|
|
13
13
|
hash: ConfigHash | None = None
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class Login2Flags(CamelModel):
|
|
17
|
+
config_enabled: bool = False
|
|
18
|
+
contact_enabled: bool = False
|
|
19
|
+
profile_enabled: bool = False
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def enabled(self) -> bool:
|
|
23
|
+
return self.config_enabled or self.contact_enabled or self.profile_enabled
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
class LoginResponse(CamelModel):
|
|
17
27
|
chats: list[Chat] = Field(default_factory=list)
|
|
18
|
-
profile: Profile
|
|
28
|
+
profile: Profile | None = None
|
|
19
29
|
messages: dict[int, list[Message]] = Field(default_factory=dict) # chat_id -> [message]
|
|
20
30
|
contacts: list[User | None] = Field(default_factory=list)
|
|
21
31
|
token: str | None = None
|
|
22
32
|
time: int | None = None
|
|
23
33
|
config: LoginConfig | None = None
|
|
34
|
+
updates: int | None = None
|
|
35
|
+
login2_flags: Login2Flags | None = None
|
|
24
36
|
|
|
25
37
|
def update_sync_state(self, current: SyncState) -> SyncState:
|
|
26
38
|
sync_time = self.time
|
|
@@ -33,3 +45,20 @@ class LoginResponse(CamelModel):
|
|
|
33
45
|
presence_sync=(sync_time if sync_time is not None else current.presence_sync),
|
|
34
46
|
config_hash=(config_hash if config_hash is not None else current.config_hash),
|
|
35
47
|
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Login2Response(CamelModel):
|
|
51
|
+
profile: Profile | None = None
|
|
52
|
+
contacts: list[User | None] = Field(default_factory=list, alias="contactInfos")
|
|
53
|
+
config: LoginConfig | None = None
|
|
54
|
+
|
|
55
|
+
def update_sync_state(self, current: SyncState) -> SyncState:
|
|
56
|
+
config_hash = self.config.hash if self.config is not None else None
|
|
57
|
+
|
|
58
|
+
return SyncState(
|
|
59
|
+
chats_sync=current.chats_sync,
|
|
60
|
+
contacts_sync=current.contacts_sync,
|
|
61
|
+
drafts_sync=current.drafts_sync,
|
|
62
|
+
presence_sync=current.presence_sync,
|
|
63
|
+
config_hash=(config_hash if config_hash is not None else current.config_hash),
|
|
64
|
+
)
|
pymax/types/domain/message.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
|
|
|
5
5
|
|
|
6
6
|
from pydantic import Field, PrivateAttr, model_validator
|
|
7
7
|
|
|
8
|
-
from pymax.files import File, Photo, Video
|
|
8
|
+
from pymax.files import File, Photo, Video, VideoNote, Voice
|
|
9
9
|
from pymax.types.domain import (
|
|
10
10
|
AudioAttachment,
|
|
11
11
|
CallAttachment,
|
|
@@ -14,6 +14,8 @@ from pymax.types.domain import (
|
|
|
14
14
|
FileAttachment,
|
|
15
15
|
InlineKeyboardAttachment,
|
|
16
16
|
PhotoAttachment,
|
|
17
|
+
Poll,
|
|
18
|
+
PollAttachment,
|
|
17
19
|
ShareAttachment,
|
|
18
20
|
StickerAttachment,
|
|
19
21
|
UnknownAttachment,
|
|
@@ -38,11 +40,12 @@ KnownAttachment: TypeAlias = Annotated[
|
|
|
38
40
|
| ControlAttachment
|
|
39
41
|
| InlineKeyboardAttachment
|
|
40
42
|
| ShareAttachment
|
|
41
|
-
| CallAttachment
|
|
43
|
+
| CallAttachment
|
|
44
|
+
| PollAttachment,
|
|
42
45
|
Field(discriminator="type"),
|
|
43
46
|
]
|
|
44
47
|
Attachment: TypeAlias = KnownAttachment | UnknownAttachment
|
|
45
|
-
SendAttachment: TypeAlias = Photo | File | Video
|
|
48
|
+
SendAttachment: TypeAlias = Photo | File | Video | Poll | Voice | VideoNote
|
|
46
49
|
SendAttachments: TypeAlias = Sequence[SendAttachment] | None
|
|
47
50
|
|
|
48
51
|
|
|
@@ -182,22 +185,21 @@ class Message(CamelModel):
|
|
|
182
185
|
|
|
183
186
|
async def reply(
|
|
184
187
|
self,
|
|
185
|
-
text: str,
|
|
188
|
+
text: str | None = None,
|
|
186
189
|
attachments: SendAttachments = None,
|
|
187
190
|
*,
|
|
188
191
|
notify: bool = True,
|
|
189
|
-
) -> Message
|
|
192
|
+
) -> Message:
|
|
190
193
|
"""Отправляет ответ на это сообщение в тот же чат.
|
|
191
194
|
|
|
192
195
|
:param text: Текст сообщения.
|
|
193
|
-
:type text: str
|
|
196
|
+
:type text: str | None
|
|
194
197
|
:param attachments: Файлы, фотографии или видео для отправки.
|
|
195
198
|
:type attachments: SendAttachments
|
|
196
199
|
:param notify: Отправить ли получателям push-уведомление.
|
|
197
200
|
:type notify: bool
|
|
198
|
-
:returns: Отправленное
|
|
199
|
-
|
|
200
|
-
:rtype: Message | None
|
|
201
|
+
:returns: Отправленное сообщение.
|
|
202
|
+
:rtype: Message
|
|
201
203
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
202
204
|
содержит ``chat_id``.
|
|
203
205
|
"""
|
|
@@ -213,25 +215,24 @@ class Message(CamelModel):
|
|
|
213
215
|
|
|
214
216
|
async def answer(
|
|
215
217
|
self,
|
|
216
|
-
text: str,
|
|
218
|
+
text: str | None = None,
|
|
217
219
|
reply_to: int | None = None,
|
|
218
220
|
attachments: SendAttachments = None,
|
|
219
221
|
*,
|
|
220
222
|
notify: bool = True,
|
|
221
|
-
) -> Message
|
|
223
|
+
) -> Message:
|
|
222
224
|
"""Отправляет сообщение в тот же чат.
|
|
223
225
|
|
|
224
226
|
:param text: Текст сообщения.
|
|
225
|
-
:type text: str
|
|
227
|
+
:type text: str | None
|
|
226
228
|
:param reply_to: ID сообщения для ответа.
|
|
227
229
|
:type reply_to: int | None
|
|
228
230
|
:param attachments: Файлы, фотографии или видео для отправки.
|
|
229
231
|
:type attachments: SendAttachments
|
|
230
232
|
:param notify: Отправить ли получателям push-уведомление.
|
|
231
233
|
:type notify: bool
|
|
232
|
-
:returns: Отправленное
|
|
233
|
-
|
|
234
|
-
:rtype: Message | None
|
|
234
|
+
:returns: Отправленное сообщение.
|
|
235
|
+
:rtype: Message
|
|
235
236
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
236
237
|
содержит ``chat_id``.
|
|
237
238
|
"""
|
|
@@ -250,15 +251,15 @@ class Message(CamelModel):
|
|
|
250
251
|
chat_id: int,
|
|
251
252
|
*,
|
|
252
253
|
notify: bool = True,
|
|
253
|
-
) -> Message
|
|
254
|
+
) -> Message:
|
|
254
255
|
"""Пересылает это сообщение в другой чат.
|
|
255
256
|
|
|
256
257
|
:param chat_id: ID целевого чата.
|
|
257
258
|
:type chat_id: int
|
|
258
259
|
:param notify: Отправить ли получателям push-уведомление.
|
|
259
260
|
:type notify: bool
|
|
260
|
-
:returns: Пересланное
|
|
261
|
-
:rtype: Message
|
|
261
|
+
:returns: Пересланное сообщение.
|
|
262
|
+
:rtype: Message
|
|
262
263
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
263
264
|
содержит ``chat_id``.
|
|
264
265
|
"""
|
|
@@ -291,13 +292,13 @@ class Message(CamelModel):
|
|
|
291
292
|
|
|
292
293
|
async def edit(
|
|
293
294
|
self,
|
|
294
|
-
text: str,
|
|
295
|
+
text: str | None = None,
|
|
295
296
|
attachments: SendAttachments = None,
|
|
296
297
|
) -> Message:
|
|
297
298
|
"""Редактирует текст и вложения этого сообщения.
|
|
298
299
|
|
|
299
300
|
:param text: Новый текст сообщения с поддержкой markdown.
|
|
300
|
-
:type text: str
|
|
301
|
+
:type text: str | None
|
|
301
302
|
:param attachments: Новые файлы, фотографии или видео для сообщения.
|
|
302
303
|
:type attachments: SendAttachments
|
|
303
304
|
:returns: Отредактированное сообщение.
|
pymax/types/domain/user.py
CHANGED
|
@@ -71,10 +71,8 @@ class User(CamelModel):
|
|
|
71
71
|
phone: int | None = None
|
|
72
72
|
status: str | None = None
|
|
73
73
|
description: str | None = None
|
|
74
|
-
# Bots may send ``gender`` as a numeric code and ``web_app`` as a URL
|
|
75
|
-
# string instead of an object; accept these so profile parsing won't fail.
|
|
76
74
|
gender: str | int | None = None
|
|
77
|
-
link: str | None = None
|
|
75
|
+
link: str | int | None = None
|
|
78
76
|
web_app: dict[str, Any] | str | None = None
|
|
79
77
|
menu_button: dict[str, Any] | None = None
|
|
80
78
|
|
pymax/types/events/__init__.py
CHANGED
pymax/types/events/reaction.py
CHANGED
|
File without changes
|