maxapi-python 2.3.1__py3-none-any.whl → 2.4.1__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.1.dist-info}/METADATA +8 -4
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +79 -66
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
- pymax/__init__.py +9 -2
- pymax/_data/apk_fingerprints.json +767 -0
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +100 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +18 -2
- pymax/api/chats/service.py +43 -1
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/payloads.py +21 -7
- pymax/api/messages/service.py +161 -24
- 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 +39 -1
- pymax/api/uploads/service.py +247 -55
- pymax/app.py +91 -18
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/sms.py +11 -1
- pymax/base.py +90 -8
- pymax/client.py +39 -10
- pymax/client_web.py +17 -14
- pymax/config.py +98 -19
- 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/base.py +58 -1
- pymax/files/video.py +51 -1
- pymax/files/voice.py +53 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +30 -0
- pymax/fingerprint/models.py +12 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +47 -5
- pymax/infra/message.py +49 -14
- pymax/infra/protocol.py +5 -1
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- pymax/session/__init__.py +1 -1
- pymax/session/models.py +30 -0
- pymax/session/protocol.py +33 -7
- pymax/session/store.py +73 -5
- pymax/transport/tcp.py +17 -4
- 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/call.py +7 -1
- pymax/types/domain/attachments/contact.py +3 -3
- pymax/types/domain/attachments/control.py +3 -0
- pymax/types/domain/attachments/enums.py +33 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +32 -11
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +108 -27
- 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
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -32,8 +32,8 @@ class VideoAttachment(CamelModel):
|
|
|
32
32
|
:vartype video_id: int
|
|
33
33
|
:ivar duration: Длительность видео.
|
|
34
34
|
:vartype duration: int | None
|
|
35
|
-
:ivar preview_data: Данные
|
|
36
|
-
:vartype preview_data: bytes
|
|
35
|
+
:ivar preview_data: Данные превью, если Max их прислал.
|
|
36
|
+
:vartype preview_data: bytes | None
|
|
37
37
|
:ivar type: Тип вложения.
|
|
38
38
|
:vartype type: Literal[AttachmentType.VIDEO]
|
|
39
39
|
:ivar thumbnail: URL миниатюры.
|
|
@@ -48,7 +48,7 @@ class VideoAttachment(CamelModel):
|
|
|
48
48
|
width: int
|
|
49
49
|
video_id: int
|
|
50
50
|
duration: int | None = None
|
|
51
|
-
preview_data: bytes
|
|
51
|
+
preview_data: bytes | None = None
|
|
52
52
|
type: Literal[AttachmentType.VIDEO] = Field(alias="_type")
|
|
53
53
|
thumbnail: str
|
|
54
54
|
token: str
|
|
@@ -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()
|
pymax/types/domain/enums.py
CHANGED
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
-
from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
|
|
4
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, 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,
|
|
@@ -22,10 +24,10 @@ from pymax.types.domain import (
|
|
|
22
24
|
|
|
23
25
|
from .base import CamelModel
|
|
24
26
|
from .element import Element
|
|
25
|
-
from .enums import MessageStatus
|
|
27
|
+
from .enums import AccessType, LinkType, MessageStatus
|
|
26
28
|
|
|
27
29
|
if TYPE_CHECKING:
|
|
28
|
-
from pymax.api.messages
|
|
30
|
+
from pymax.api.messages import DateTimeUnion, MessageService
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
KnownAttachment: TypeAlias = Annotated[
|
|
@@ -38,12 +40,14 @@ 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
|
|
50
|
+
Link: TypeAlias = Annotated["ReplyLink | ForwardLink", Field(discriminator="type")]
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
class ReactionCounter(CamelModel):
|
|
@@ -88,6 +92,66 @@ class ReadState(CamelModel):
|
|
|
88
92
|
mark: int
|
|
89
93
|
|
|
90
94
|
|
|
95
|
+
class DelayedAttributes(CamelModel):
|
|
96
|
+
time_to_fire: int
|
|
97
|
+
notify_sender: bool
|
|
98
|
+
notify_opponents: bool
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ReplyLink(CamelModel):
|
|
102
|
+
"""Источник сообщения, отправленного как reply.
|
|
103
|
+
|
|
104
|
+
:ivar message: Сообщение, на которое ответили.
|
|
105
|
+
:vartype message: Message
|
|
106
|
+
:ivar chat_id: ID чата исходного сообщения.
|
|
107
|
+
:vartype chat_id: int
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
type: Literal[LinkType.REPLY] = LinkType.REPLY
|
|
111
|
+
message: Message
|
|
112
|
+
chat_id: int
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ForwardLink(CamelModel):
|
|
116
|
+
"""Источник пересланного сообщения.
|
|
117
|
+
|
|
118
|
+
Помимо исходного сообщения содержит доступные в payload-е имя, ссылку,
|
|
119
|
+
тип доступа и иконку исходного чата. Для скрытого источника эти метаданные
|
|
120
|
+
могут быть ``None``.
|
|
121
|
+
|
|
122
|
+
:ivar message: Исходное сообщение.
|
|
123
|
+
:vartype message: Message
|
|
124
|
+
:ivar chat_id: ID исходного чата.
|
|
125
|
+
:vartype chat_id: int
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
type: Literal[LinkType.FORWARD] = LinkType.FORWARD
|
|
129
|
+
message: Message
|
|
130
|
+
chat_id: int
|
|
131
|
+
chat_name: str | None = None
|
|
132
|
+
chat_link: str | None = None
|
|
133
|
+
chat_access_type: AccessType | None = None
|
|
134
|
+
chat_icon_url: str | None = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
"link": {
|
|
139
|
+
"type": "FORWARD",
|
|
140
|
+
"message": {
|
|
141
|
+
"id": "116641336752745888",
|
|
142
|
+
"time": 1779805553478,
|
|
143
|
+
"type": "CHANNEL",
|
|
144
|
+
"text": "Добро пожаловать на наше шоу!",
|
|
145
|
+
"attaches": []
|
|
146
|
+
},
|
|
147
|
+
"chatId": -75183661482741,
|
|
148
|
+
"chatName": "Max Bounty Test",
|
|
149
|
+
"chatLink": "https://max.ru/join/htlT5aTtlpAgcno_hc93lLM44Dxe45HvOEBqx22lKms",
|
|
150
|
+
"chatAccessType": "PRIVATE"
|
|
151
|
+
}
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
|
|
91
155
|
class Message(CamelModel):
|
|
92
156
|
"""Сообщение Max с методами для действий над ним.
|
|
93
157
|
|
|
@@ -127,7 +191,8 @@ class Message(CamelModel):
|
|
|
127
191
|
:vartype type: str
|
|
128
192
|
:ivar cid: Клиентский ID сообщения, если он есть в payload-е.
|
|
129
193
|
:vartype cid: int | None
|
|
130
|
-
:ivar attaches: Вложения сообщения.
|
|
194
|
+
:ivar attaches: Вложения сообщения. Неизвестный ``_type`` представлен как
|
|
195
|
+
``UnknownAttachment`` с сохраненными дополнительными полями.
|
|
131
196
|
:vartype attaches: list[Attachment]
|
|
132
197
|
:ivar stats: Дополнительная статистика сообщения от Max.
|
|
133
198
|
:vartype stats: dict[str, Any] | None
|
|
@@ -147,6 +212,12 @@ class Message(CamelModel):
|
|
|
147
212
|
:vartype mark: int | None
|
|
148
213
|
:ivar elements: Форматированные элементы текста сообщения.
|
|
149
214
|
:vartype elements: list[Element]
|
|
215
|
+
:ivar delayed_attributes: Время срабатывания в миллисекундах и настройки
|
|
216
|
+
уведомлений для отложенного сообщения; ``None`` для обычного.
|
|
217
|
+
:vartype delayed_attributes: DelayedAttributes | None
|
|
218
|
+
:ivar link: Исходное сообщение и чат для reply или forward; ``None`` для
|
|
219
|
+
обычного сообщения без связи.
|
|
220
|
+
:vartype link: ReplyLink | ForwardLink | None
|
|
150
221
|
"""
|
|
151
222
|
|
|
152
223
|
id: int
|
|
@@ -166,6 +237,8 @@ class Message(CamelModel):
|
|
|
166
237
|
unread: int | None = None
|
|
167
238
|
mark: int | None = None
|
|
168
239
|
elements: list[Element] = Field(default_factory=list)
|
|
240
|
+
delayed_attributes: DelayedAttributes | None = None
|
|
241
|
+
link: Link | None = None
|
|
169
242
|
|
|
170
243
|
_actions: MessageService | None = PrivateAttr(default=None)
|
|
171
244
|
|
|
@@ -182,22 +255,25 @@ class Message(CamelModel):
|
|
|
182
255
|
|
|
183
256
|
async def reply(
|
|
184
257
|
self,
|
|
185
|
-
text: str,
|
|
258
|
+
text: str | None = None,
|
|
186
259
|
attachments: SendAttachments = None,
|
|
187
260
|
*,
|
|
188
261
|
notify: bool = True,
|
|
189
|
-
|
|
262
|
+
send_at: DateTimeUnion | None = None,
|
|
263
|
+
) -> Message:
|
|
190
264
|
"""Отправляет ответ на это сообщение в тот же чат.
|
|
191
265
|
|
|
192
266
|
:param text: Текст сообщения.
|
|
193
|
-
:type text: str
|
|
267
|
+
:type text: str | None
|
|
194
268
|
:param attachments: Файлы, фотографии или видео для отправки.
|
|
195
269
|
:type attachments: SendAttachments
|
|
196
270
|
:param notify: Отправить ли получателям push-уведомление.
|
|
197
271
|
:type notify: bool
|
|
198
|
-
:
|
|
199
|
-
|
|
200
|
-
:
|
|
272
|
+
:param send_at: Абсолютный ``datetime``, относительный ``timedelta``
|
|
273
|
+
или Unix time в секундах. ``None`` и ``0`` отправляют сразу.
|
|
274
|
+
:type send_at: DateTimeUnion | None
|
|
275
|
+
:returns: Отправленное сообщение.
|
|
276
|
+
:rtype: Message
|
|
201
277
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
202
278
|
содержит ``chat_id``.
|
|
203
279
|
"""
|
|
@@ -209,29 +285,33 @@ class Message(CamelModel):
|
|
|
209
285
|
reply_to=self.id,
|
|
210
286
|
attachments=attachments,
|
|
211
287
|
notify=notify,
|
|
288
|
+
send_at=send_at,
|
|
212
289
|
)
|
|
213
290
|
|
|
214
291
|
async def answer(
|
|
215
292
|
self,
|
|
216
|
-
text: str,
|
|
293
|
+
text: str | None = None,
|
|
217
294
|
reply_to: int | None = None,
|
|
218
295
|
attachments: SendAttachments = None,
|
|
219
296
|
*,
|
|
220
297
|
notify: bool = True,
|
|
221
|
-
|
|
298
|
+
send_at: DateTimeUnion | None = None,
|
|
299
|
+
) -> Message:
|
|
222
300
|
"""Отправляет сообщение в тот же чат.
|
|
223
301
|
|
|
224
302
|
:param text: Текст сообщения.
|
|
225
|
-
:type text: str
|
|
303
|
+
:type text: str | None
|
|
226
304
|
:param reply_to: ID сообщения для ответа.
|
|
227
305
|
:type reply_to: int | None
|
|
228
306
|
:param attachments: Файлы, фотографии или видео для отправки.
|
|
229
307
|
:type attachments: SendAttachments
|
|
230
308
|
:param notify: Отправить ли получателям push-уведомление.
|
|
231
309
|
:type notify: bool
|
|
232
|
-
:
|
|
233
|
-
|
|
234
|
-
:
|
|
310
|
+
:param send_at: Абсолютный ``datetime``, относительный ``timedelta``
|
|
311
|
+
или Unix time в секундах. ``None`` и ``0`` отправляют сразу.
|
|
312
|
+
:type send_at: DateTimeUnion | None
|
|
313
|
+
:returns: Отправленное сообщение.
|
|
314
|
+
:rtype: Message
|
|
235
315
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
236
316
|
содержит ``chat_id``.
|
|
237
317
|
"""
|
|
@@ -243,6 +323,7 @@ class Message(CamelModel):
|
|
|
243
323
|
reply_to=reply_to,
|
|
244
324
|
attachments=attachments,
|
|
245
325
|
notify=notify,
|
|
326
|
+
send_at=send_at,
|
|
246
327
|
)
|
|
247
328
|
|
|
248
329
|
async def forward(
|
|
@@ -250,15 +331,15 @@ class Message(CamelModel):
|
|
|
250
331
|
chat_id: int,
|
|
251
332
|
*,
|
|
252
333
|
notify: bool = True,
|
|
253
|
-
) -> Message
|
|
334
|
+
) -> Message:
|
|
254
335
|
"""Пересылает это сообщение в другой чат.
|
|
255
336
|
|
|
256
337
|
:param chat_id: ID целевого чата.
|
|
257
338
|
:type chat_id: int
|
|
258
339
|
:param notify: Отправить ли получателям push-уведомление.
|
|
259
340
|
:type notify: bool
|
|
260
|
-
:returns: Пересланное
|
|
261
|
-
:rtype: Message
|
|
341
|
+
:returns: Пересланное сообщение.
|
|
342
|
+
:rtype: Message
|
|
262
343
|
:raises RuntimeError: Если сообщение не привязано к сервису или не
|
|
263
344
|
содержит ``chat_id``.
|
|
264
345
|
"""
|
|
@@ -291,13 +372,13 @@ class Message(CamelModel):
|
|
|
291
372
|
|
|
292
373
|
async def edit(
|
|
293
374
|
self,
|
|
294
|
-
text: str,
|
|
375
|
+
text: str | None = None,
|
|
295
376
|
attachments: SendAttachments = None,
|
|
296
377
|
) -> Message:
|
|
297
378
|
"""Редактирует текст и вложения этого сообщения.
|
|
298
379
|
|
|
299
380
|
:param text: Новый текст сообщения с поддержкой markdown.
|
|
300
|
-
:type text: str
|
|
381
|
+
:type text: str | None
|
|
301
382
|
:param attachments: Новые файлы, фотографии или видео для сообщения.
|
|
302
383
|
:type attachments: SendAttachments
|
|
303
384
|
:returns: Отредактированное сообщение.
|
|
@@ -361,7 +442,7 @@ class Message(CamelModel):
|
|
|
361
442
|
|
|
362
443
|
return await actions.add_reaction(
|
|
363
444
|
chat_id=chat_id,
|
|
364
|
-
message_id=
|
|
445
|
+
message_id=self.id,
|
|
365
446
|
reaction=reaction,
|
|
366
447
|
)
|
|
367
448
|
|
|
@@ -377,7 +458,7 @@ class Message(CamelModel):
|
|
|
377
458
|
|
|
378
459
|
return await actions.remove_reaction(
|
|
379
460
|
chat_id=chat_id,
|
|
380
|
-
message_id=
|
|
461
|
+
message_id=self.id,
|
|
381
462
|
)
|
|
382
463
|
|
|
383
464
|
async def get_reactions(self) -> dict[str, ReactionInfo] | None:
|
|
@@ -393,7 +474,7 @@ class Message(CamelModel):
|
|
|
393
474
|
|
|
394
475
|
return await actions.get_reactions(
|
|
395
476
|
chat_id=chat_id,
|
|
396
|
-
message_ids=[
|
|
477
|
+
message_ids=[self.id],
|
|
397
478
|
)
|
|
398
479
|
|
|
399
480
|
def _bound(self) -> tuple[MessageService, int]:
|
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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from importlib import resources
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
|
|
7
|
+
from pymax.fingerprint.models import ApkBuildFingerprint
|
|
8
|
+
|
|
9
|
+
from .exceptions import VersionAlreadyExistsError, VersionNotFoundError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VersionCatalog:
|
|
13
|
+
"""Каталог fingerprints Android-клиента Max.
|
|
14
|
+
|
|
15
|
+
Встроенные версии загружаются при создании объекта. При ``remote=True``
|
|
16
|
+
вызов :meth:`load` дополнительно получает каталог с
|
|
17
|
+
``DEFAULT_REMOTE_URL`` и объединяет его со встроенным. Ошибки сети, HTTP и
|
|
18
|
+
валидации удаленного ответа не подавляются.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
remote: Загружать ли удаленные версии при :meth:`load`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
RECOMMENDED_APP_VERSION: str = "26.25.0"
|
|
25
|
+
DEFAULT_REMOTE_URL: str = "https://hashes.pymax.org/versions.json"
|
|
26
|
+
|
|
27
|
+
def __init__(self, remote: bool = False) -> None:
|
|
28
|
+
self.path = resources.files("pymax._data") / "apk_fingerprints.json"
|
|
29
|
+
self.remote = remote
|
|
30
|
+
self._data = self._load_fingerprints()
|
|
31
|
+
self.versions = self._convert_data()
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def recommended() -> str:
|
|
35
|
+
"""Возвращает версию, используемую ``Client`` по умолчанию."""
|
|
36
|
+
return VersionCatalog.RECOMMENDED_APP_VERSION
|
|
37
|
+
|
|
38
|
+
async def load(self) -> "VersionCatalog":
|
|
39
|
+
"""Загружает удаленные версии, если включен ``remote``.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Этот же каталог с добавленными удаленными версиями.
|
|
43
|
+
"""
|
|
44
|
+
if not self.remote:
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
versions = await self._fetch()
|
|
48
|
+
self.versions.update(versions)
|
|
49
|
+
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def add(self, version: str, fingerprint: ApkBuildFingerprint, override: bool = False) -> None:
|
|
53
|
+
"""Добавляет fingerprint версии в текущий каталог.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
version: Строка версии Android-клиента.
|
|
57
|
+
fingerprint: Fingerprint и build number этой версии.
|
|
58
|
+
override: Заменить существующую запись. По умолчанию ``False``.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: Если версия уже существует и ``override=False``.
|
|
62
|
+
"""
|
|
63
|
+
if self.versions.get(version) is not None and override is False:
|
|
64
|
+
raise VersionAlreadyExistsError(version)
|
|
65
|
+
|
|
66
|
+
self.versions[version] = fingerprint
|
|
67
|
+
|
|
68
|
+
def resolve(self, version: str) -> ApkBuildFingerprint:
|
|
69
|
+
"""Возвращает fingerprint версии.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
VersionNotFoundError: Если версии нет в каталоге.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
return self.versions[version]
|
|
76
|
+
except KeyError as e:
|
|
77
|
+
raise VersionNotFoundError(version) from e
|
|
78
|
+
|
|
79
|
+
async def _fetch(self) -> dict[str, ApkBuildFingerprint]:
|
|
80
|
+
async with (
|
|
81
|
+
aiohttp.ClientSession() as session,
|
|
82
|
+
session.get(self.DEFAULT_REMOTE_URL) as response,
|
|
83
|
+
):
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
data = await response.json()
|
|
86
|
+
|
|
87
|
+
if not isinstance(data, dict):
|
|
88
|
+
raise ValueError # TODO: msg
|
|
89
|
+
|
|
90
|
+
versions: dict[str, ApkBuildFingerprint] = {}
|
|
91
|
+
for k, v in data.items():
|
|
92
|
+
versions[k] = ApkBuildFingerprint.model_validate(v)
|
|
93
|
+
|
|
94
|
+
return versions
|
|
95
|
+
|
|
96
|
+
def _load_fingerprints(self) -> dict[str, Any]:
|
|
97
|
+
with self.path.open("r", encoding="utf-8") as f:
|
|
98
|
+
return json.load(f)
|
|
99
|
+
|
|
100
|
+
def _convert_data(self) -> dict[str, ApkBuildFingerprint]:
|
|
101
|
+
versions: dict[str, ApkBuildFingerprint] = {}
|
|
102
|
+
for k, v in self._data.items():
|
|
103
|
+
versions[k] = ApkBuildFingerprint.model_validate(v)
|
|
104
|
+
return versions
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class VersionNotFoundError(ValueError):
|
|
2
|
+
"""Выбранная версия приложения отсутствует в каталоге fingerprints."""
|
|
3
|
+
|
|
4
|
+
def __init__(self, version: str) -> None:
|
|
5
|
+
super().__init__(f"Could not find version {version} in registry")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class VersionAlreadyExistsError(ValueError):
|
|
9
|
+
def __init__(self, version: str) -> None:
|
|
10
|
+
super().__init__(f"Version {version} already exists in registry")
|
|
File without changes
|