maxapi-python 1.2.5__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.5.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/protocol/enums.py +180 -0
- 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.5.dist-info/METADATA +0 -202
- maxapi_python-1.2.5.dist-info/RECORD +0 -33
- pymax/core.py +0 -398
- 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 -558
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -594
- 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 -306
- pymax/mixins/telemetry.py +0 -118
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -151
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -403
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -96
- pymax/static/enum.py +0 -231
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.5.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import TYPE_CHECKING, TypeAlias
|
|
5
|
+
|
|
6
|
+
from pymax.api.response import (
|
|
7
|
+
parse_payload_list,
|
|
8
|
+
parse_payload_model,
|
|
9
|
+
payload_item,
|
|
10
|
+
require_payload_model,
|
|
11
|
+
)
|
|
12
|
+
from pymax.api.uploads.payloads import AttachFilePayload, AttachPhotoPayload, VideoAttachPayload
|
|
13
|
+
from pymax.exceptions import UploadError
|
|
14
|
+
from pymax.files import File, Photo, Video
|
|
15
|
+
from pymax.formatting.markdown import Formatter
|
|
16
|
+
from pymax.logging import get_logger
|
|
17
|
+
from pymax.protocol import Opcode
|
|
18
|
+
from pymax.types.domain import (
|
|
19
|
+
FileRequest,
|
|
20
|
+
Message,
|
|
21
|
+
ReactionInfo,
|
|
22
|
+
ReadState,
|
|
23
|
+
VideoRequest,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .enums import ItemType, MessagePayloadKey, ReadAction
|
|
27
|
+
from .payloads import (
|
|
28
|
+
AddReactionPayload,
|
|
29
|
+
ChatHistoryPayload,
|
|
30
|
+
DeleteMessagePayload,
|
|
31
|
+
GetFilePayload,
|
|
32
|
+
GetReactionsPayload,
|
|
33
|
+
GetVideoPayload,
|
|
34
|
+
PinMessagePayload,
|
|
35
|
+
ReactionInfoPayload,
|
|
36
|
+
ReadMessagesPayload,
|
|
37
|
+
RemoveReactionPayload,
|
|
38
|
+
ReplyLink,
|
|
39
|
+
SendMessagePayload,
|
|
40
|
+
SendMessagePayloadMessage,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from pymax.app import App
|
|
45
|
+
|
|
46
|
+
SendAttachment: TypeAlias = Photo | File | Video
|
|
47
|
+
SendAttachments: TypeAlias = list[SendAttachment] | None
|
|
48
|
+
|
|
49
|
+
logger = get_logger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MessageService:
|
|
53
|
+
def __init__(self, app: App) -> None:
|
|
54
|
+
self.app = app
|
|
55
|
+
|
|
56
|
+
self._prev = int(time.time() * 1000)
|
|
57
|
+
|
|
58
|
+
def _next_cid(self) -> int:
|
|
59
|
+
now = int(time.time() * 1000)
|
|
60
|
+
e = max(now, self._prev + 1)
|
|
61
|
+
self._prev = e
|
|
62
|
+
logger.debug("generated message cid=%s", e)
|
|
63
|
+
return e
|
|
64
|
+
|
|
65
|
+
async def _upload_attachments(
|
|
66
|
+
self, attachments: SendAttachments
|
|
67
|
+
) -> list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload]:
|
|
68
|
+
result: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = []
|
|
69
|
+
if not attachments:
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
for attachment in attachments:
|
|
73
|
+
if isinstance(attachment, Photo):
|
|
74
|
+
upload_result = await self.app.api.uploads.upload_photo(attachment)
|
|
75
|
+
if not upload_result:
|
|
76
|
+
logger.error("Photo uploading failed")
|
|
77
|
+
raise UploadError("Photo uploading failed")
|
|
78
|
+
|
|
79
|
+
result.append(upload_result)
|
|
80
|
+
|
|
81
|
+
elif isinstance(attachment, Video):
|
|
82
|
+
upload_result = await self.app.api.uploads.upload_video(attachment)
|
|
83
|
+
if not upload_result:
|
|
84
|
+
logger.error("Video uploading failed")
|
|
85
|
+
raise UploadError("Video uploading failed")
|
|
86
|
+
|
|
87
|
+
result.append(upload_result)
|
|
88
|
+
|
|
89
|
+
elif isinstance(attachment, File):
|
|
90
|
+
upload_result = await self.app.api.uploads.upload_file(attachment)
|
|
91
|
+
if not upload_result:
|
|
92
|
+
logger.error("File uploading failed")
|
|
93
|
+
raise UploadError("File uploading failed")
|
|
94
|
+
|
|
95
|
+
result.append(upload_result)
|
|
96
|
+
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
async def send_message(
|
|
100
|
+
self,
|
|
101
|
+
chat_id: int,
|
|
102
|
+
text: str,
|
|
103
|
+
reply_to: int | None = None,
|
|
104
|
+
attachments: SendAttachments = None,
|
|
105
|
+
*,
|
|
106
|
+
notify: bool = True,
|
|
107
|
+
) -> Message | None:
|
|
108
|
+
logger.info("sending message chat_id=%s text_len=%s", chat_id, len(text))
|
|
109
|
+
|
|
110
|
+
clean_text, elements = Formatter.format_markdown(text)
|
|
111
|
+
|
|
112
|
+
frame = SendMessagePayload(
|
|
113
|
+
chat_id=chat_id,
|
|
114
|
+
message=SendMessagePayloadMessage(
|
|
115
|
+
text=clean_text,
|
|
116
|
+
cid=self._next_cid(),
|
|
117
|
+
elements=elements,
|
|
118
|
+
attaches=await self._upload_attachments(attachments),
|
|
119
|
+
link=ReplyLink(message_id=reply_to) if reply_to else None,
|
|
120
|
+
),
|
|
121
|
+
notify=notify,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
|
|
125
|
+
|
|
126
|
+
message = require_payload_model(response, Message).bind(self)
|
|
127
|
+
logger.info("message sent chat_id=%s", chat_id)
|
|
128
|
+
return message
|
|
129
|
+
|
|
130
|
+
async def fetch_history(
|
|
131
|
+
self,
|
|
132
|
+
chat_id: int,
|
|
133
|
+
forward: int = 0,
|
|
134
|
+
backward: int = 40,
|
|
135
|
+
backward_time: int = 0,
|
|
136
|
+
forward_time: int = 0,
|
|
137
|
+
from_: int | None = None,
|
|
138
|
+
item_type: ItemType = ItemType.REGULAR,
|
|
139
|
+
get_chat: bool = False,
|
|
140
|
+
get_messages: bool = True,
|
|
141
|
+
interactive: bool = False,
|
|
142
|
+
) -> list[Message] | None:
|
|
143
|
+
frame = ChatHistoryPayload(
|
|
144
|
+
chat_id=chat_id,
|
|
145
|
+
forward=forward,
|
|
146
|
+
backward=backward,
|
|
147
|
+
backward_time=backward_time,
|
|
148
|
+
forward_time=forward_time,
|
|
149
|
+
from_=from_ or int(time.time() * 1000),
|
|
150
|
+
item_type=item_type,
|
|
151
|
+
get_chat=get_chat,
|
|
152
|
+
get_messages=get_messages,
|
|
153
|
+
interactive=interactive,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
response = await self.app.invoke(
|
|
157
|
+
Opcode.CHAT_HISTORY,
|
|
158
|
+
payload=frame.to_payload(),
|
|
159
|
+
)
|
|
160
|
+
return parse_payload_list(response, MessagePayloadKey.MESSAGES, Message) or None
|
|
161
|
+
|
|
162
|
+
async def delete_message(
|
|
163
|
+
self,
|
|
164
|
+
chat_id: int,
|
|
165
|
+
message_ids: list[int],
|
|
166
|
+
for_me: bool,
|
|
167
|
+
) -> bool:
|
|
168
|
+
logger.info(
|
|
169
|
+
"deleting messages chat_id=%s ids=%s for_me=%s",
|
|
170
|
+
chat_id,
|
|
171
|
+
message_ids,
|
|
172
|
+
for_me,
|
|
173
|
+
)
|
|
174
|
+
frame = DeleteMessagePayload(
|
|
175
|
+
chat_id=chat_id,
|
|
176
|
+
message_ids=message_ids,
|
|
177
|
+
for_me=for_me,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
await self.app.invoke(Opcode.MSG_DELETE, frame.to_payload())
|
|
181
|
+
logger.info("messages deleted chat_id=%s count=%s", chat_id, len(message_ids))
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
async def pin_message(
|
|
185
|
+
self,
|
|
186
|
+
chat_id: int,
|
|
187
|
+
message_id: int,
|
|
188
|
+
notify_pin: bool,
|
|
189
|
+
) -> bool:
|
|
190
|
+
logger.info(
|
|
191
|
+
"pinning message chat_id=%s message_id=%s notify_pin=%s",
|
|
192
|
+
chat_id,
|
|
193
|
+
message_id,
|
|
194
|
+
notify_pin,
|
|
195
|
+
)
|
|
196
|
+
frame = PinMessagePayload(
|
|
197
|
+
chat_id=chat_id,
|
|
198
|
+
notify_pin=notify_pin,
|
|
199
|
+
pin_message_id=message_id,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
await self.app.invoke(Opcode.CHAT_UPDATE, frame.to_payload())
|
|
203
|
+
logger.info("message pinned chat_id=%s message_id=%s", chat_id, message_id)
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
async def get_video_by_id(
|
|
207
|
+
self,
|
|
208
|
+
chat_id: int,
|
|
209
|
+
message_id: int | str,
|
|
210
|
+
video_id: int,
|
|
211
|
+
) -> VideoRequest | None:
|
|
212
|
+
logger.info(
|
|
213
|
+
"getting video chat_id=%s message_id=%s video_id=%s",
|
|
214
|
+
chat_id,
|
|
215
|
+
message_id,
|
|
216
|
+
video_id,
|
|
217
|
+
)
|
|
218
|
+
frame = GetVideoPayload(
|
|
219
|
+
chat_id=chat_id,
|
|
220
|
+
message_id=message_id,
|
|
221
|
+
video_id=video_id,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
response = await self.app.invoke(Opcode.VIDEO_PLAY, frame.to_payload())
|
|
225
|
+
return parse_payload_model(response, VideoRequest)
|
|
226
|
+
|
|
227
|
+
async def get_file_by_id(
|
|
228
|
+
self,
|
|
229
|
+
chat_id: int,
|
|
230
|
+
message_id: int | str,
|
|
231
|
+
file_id: int,
|
|
232
|
+
) -> FileRequest | None:
|
|
233
|
+
logger.info(
|
|
234
|
+
"getting file chat_id=%s message_id=%s file_id=%s",
|
|
235
|
+
chat_id,
|
|
236
|
+
message_id,
|
|
237
|
+
file_id,
|
|
238
|
+
)
|
|
239
|
+
frame = GetFilePayload(
|
|
240
|
+
chat_id=chat_id,
|
|
241
|
+
message_id=message_id,
|
|
242
|
+
file_id=file_id,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
response = await self.app.invoke(Opcode.FILE_DOWNLOAD, frame.to_payload())
|
|
246
|
+
return parse_payload_model(response, FileRequest)
|
|
247
|
+
|
|
248
|
+
async def add_reaction(
|
|
249
|
+
self,
|
|
250
|
+
chat_id: int,
|
|
251
|
+
message_id: str,
|
|
252
|
+
reaction: str,
|
|
253
|
+
) -> ReactionInfo | None:
|
|
254
|
+
logger.info(
|
|
255
|
+
"adding reaction chat_id=%s message_id=%s reaction=%s",
|
|
256
|
+
chat_id,
|
|
257
|
+
message_id,
|
|
258
|
+
reaction,
|
|
259
|
+
)
|
|
260
|
+
frame = AddReactionPayload(
|
|
261
|
+
chat_id=chat_id,
|
|
262
|
+
message_id=message_id,
|
|
263
|
+
reaction=ReactionInfoPayload(id=reaction),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
response = await self.app.invoke(Opcode.MSG_REACTION, frame.to_payload())
|
|
267
|
+
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
268
|
+
if reaction_info:
|
|
269
|
+
return ReactionInfo.model_validate(reaction_info)
|
|
270
|
+
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
async def get_reactions(
|
|
274
|
+
self,
|
|
275
|
+
chat_id: int,
|
|
276
|
+
message_ids: list[str],
|
|
277
|
+
) -> dict[str, ReactionInfo] | None:
|
|
278
|
+
logger.info(
|
|
279
|
+
"getting reactions chat_id=%s message_ids=%s",
|
|
280
|
+
chat_id,
|
|
281
|
+
message_ids,
|
|
282
|
+
)
|
|
283
|
+
frame = GetReactionsPayload(chat_id=chat_id, message_ids=message_ids)
|
|
284
|
+
|
|
285
|
+
response = await self.app.invoke(
|
|
286
|
+
Opcode.MSG_GET_REACTIONS,
|
|
287
|
+
frame.to_payload(),
|
|
288
|
+
)
|
|
289
|
+
messages_reactions = payload_item(
|
|
290
|
+
response,
|
|
291
|
+
MessagePayloadKey.MESSAGES_REACTIONS,
|
|
292
|
+
)
|
|
293
|
+
if messages_reactions is None:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
message_id: ReactionInfo.model_validate(reaction_data)
|
|
298
|
+
for message_id, reaction_data in messages_reactions.items()
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async def remove_reaction(
|
|
302
|
+
self,
|
|
303
|
+
chat_id: int,
|
|
304
|
+
message_id: str,
|
|
305
|
+
) -> ReactionInfo | None:
|
|
306
|
+
logger.info(
|
|
307
|
+
"removing reaction chat_id=%s message_id=%s",
|
|
308
|
+
chat_id,
|
|
309
|
+
message_id,
|
|
310
|
+
)
|
|
311
|
+
frame = RemoveReactionPayload(chat_id=chat_id, message_id=message_id)
|
|
312
|
+
|
|
313
|
+
response = await self.app.invoke(
|
|
314
|
+
Opcode.MSG_CANCEL_REACTION,
|
|
315
|
+
frame.to_payload(),
|
|
316
|
+
)
|
|
317
|
+
reaction_info = payload_item(response, MessagePayloadKey.REACTION_INFO)
|
|
318
|
+
if reaction_info:
|
|
319
|
+
return ReactionInfo.model_validate(reaction_info)
|
|
320
|
+
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
async def read_message(self, message_id: int, chat_id: int) -> ReadState:
|
|
324
|
+
logger.info(
|
|
325
|
+
"marking message as read chat_id=%s message_id=%s",
|
|
326
|
+
chat_id,
|
|
327
|
+
message_id,
|
|
328
|
+
)
|
|
329
|
+
frame = ReadMessagesPayload(
|
|
330
|
+
type=ReadAction.READ_MESSAGE,
|
|
331
|
+
chat_id=chat_id,
|
|
332
|
+
message_id=str(message_id),
|
|
333
|
+
mark=int(time.time() * 1000),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
response = await self.app.invoke(Opcode.CHAT_MARK, frame.to_payload())
|
|
337
|
+
return require_payload_model(response, ReadState)
|
pymax/api/models.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from pydantic.alias_generators import to_camel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CamelModel(BaseModel):
|
|
6
|
+
model_config = {
|
|
7
|
+
"alias_generator": to_camel,
|
|
8
|
+
"populate_by_name": True,
|
|
9
|
+
"arbitrary_types_allowed": True,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def to_payload(self) -> dict:
|
|
13
|
+
return self.model_dump(by_alias=True, exclude_none=True)
|
pymax/api/response.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, TypeVar, overload
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from pymax.exceptions import PyMaxError
|
|
7
|
+
from pymax.protocol import InboundFrame
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound=BaseModel)
|
|
10
|
+
V = TypeVar("V")
|
|
11
|
+
|
|
12
|
+
PayloadKey = str | Enum
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _key_value(key: PayloadKey) -> str:
|
|
16
|
+
value = key.value if isinstance(key, Enum) else key
|
|
17
|
+
return str(value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def payload_dict(response: InboundFrame) -> dict[Any, Any]:
|
|
21
|
+
return response.payload or {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def payload_keys(response: InboundFrame) -> list[Any]:
|
|
25
|
+
return sorted(payload_dict(response))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def require_payload_dict(response: InboundFrame) -> dict[Any, Any]:
|
|
29
|
+
if not isinstance(response.payload, dict):
|
|
30
|
+
raise PyMaxError("Invalid response payload")
|
|
31
|
+
|
|
32
|
+
return response.payload
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def payload_item(
|
|
37
|
+
response: InboundFrame,
|
|
38
|
+
key: PayloadKey,
|
|
39
|
+
) -> Any | None: ...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def payload_item(
|
|
44
|
+
response: InboundFrame,
|
|
45
|
+
key: PayloadKey,
|
|
46
|
+
validation_type: type[V],
|
|
47
|
+
) -> V | None: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def payload_item(
|
|
51
|
+
response: InboundFrame,
|
|
52
|
+
key: PayloadKey,
|
|
53
|
+
validation_type: type[V] | None = None,
|
|
54
|
+
) -> V | Any | None:
|
|
55
|
+
data = payload_dict(response).get(_key_value(key))
|
|
56
|
+
|
|
57
|
+
if data is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if validation_type is None:
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
return validation_type(data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def require_payload_item(
|
|
67
|
+
response: InboundFrame,
|
|
68
|
+
key: PayloadKey,
|
|
69
|
+
) -> Any:
|
|
70
|
+
item = payload_item(response, key)
|
|
71
|
+
if item is None:
|
|
72
|
+
raise PyMaxError(f"Missing `{_key_value(key)}` in response")
|
|
73
|
+
|
|
74
|
+
return item
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_payload_model(
|
|
78
|
+
response: InboundFrame,
|
|
79
|
+
model: type[T],
|
|
80
|
+
) -> T | None:
|
|
81
|
+
if response.payload:
|
|
82
|
+
return model.model_validate(response.payload)
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def require_payload_model(
|
|
88
|
+
response: InboundFrame,
|
|
89
|
+
model: type[T],
|
|
90
|
+
) -> T:
|
|
91
|
+
if not response.payload:
|
|
92
|
+
raise PyMaxError("Missing payload in response")
|
|
93
|
+
|
|
94
|
+
return model.model_validate(response.payload)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_payload_item_model(
|
|
98
|
+
response: InboundFrame,
|
|
99
|
+
key: PayloadKey,
|
|
100
|
+
model: type[T],
|
|
101
|
+
) -> T | None:
|
|
102
|
+
item = payload_item(response, key)
|
|
103
|
+
if item:
|
|
104
|
+
return model.model_validate(item)
|
|
105
|
+
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def require_payload_item_model(
|
|
110
|
+
response: InboundFrame,
|
|
111
|
+
key: PayloadKey,
|
|
112
|
+
model: type[T],
|
|
113
|
+
) -> T:
|
|
114
|
+
return model.model_validate(require_payload_item(response, key))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_payload_list(
|
|
118
|
+
response: InboundFrame,
|
|
119
|
+
key: PayloadKey,
|
|
120
|
+
model: type[T],
|
|
121
|
+
) -> list[T]:
|
|
122
|
+
items = payload_item(response, key) or []
|
|
123
|
+
return [model.model_validate(item) for item in items]
|
pymax/api/self/enums.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pymax.api.models import CamelModel
|
|
4
|
+
|
|
5
|
+
from .enums import AvatarType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UploadPayload(CamelModel):
|
|
9
|
+
count: int = 1
|
|
10
|
+
profile: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChangeProfilePayload(CamelModel):
|
|
14
|
+
first_name: str
|
|
15
|
+
last_name: str | None = None
|
|
16
|
+
description: str | None = None
|
|
17
|
+
photo_token: str | None = None
|
|
18
|
+
avatar_type: AvatarType = AvatarType.USER_AVATAR
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CreateFolderPayload(CamelModel):
|
|
22
|
+
id: str
|
|
23
|
+
title: str
|
|
24
|
+
include: list[int]
|
|
25
|
+
filters: list[Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GetFolderPayload(CamelModel):
|
|
29
|
+
folder_sync: int = 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UpdateFolderPayload(CamelModel):
|
|
33
|
+
id: str
|
|
34
|
+
title: str
|
|
35
|
+
include: list[int]
|
|
36
|
+
filters: list[Any]
|
|
37
|
+
options: list[Any]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DeleteFolderPayload(CamelModel):
|
|
41
|
+
folder_ids: list[str]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from pymax.api.response import (
|
|
7
|
+
payload_item,
|
|
8
|
+
require_payload_item,
|
|
9
|
+
require_payload_item_model,
|
|
10
|
+
require_payload_model,
|
|
11
|
+
)
|
|
12
|
+
from pymax.logging import get_logger
|
|
13
|
+
from pymax.protocol import Opcode
|
|
14
|
+
from pymax.types.domain import FolderList, FolderUpdate, Profile
|
|
15
|
+
|
|
16
|
+
from .enums import SelfPayloadKey
|
|
17
|
+
from .payloads import (
|
|
18
|
+
ChangeProfilePayload,
|
|
19
|
+
CreateFolderPayload,
|
|
20
|
+
DeleteFolderPayload,
|
|
21
|
+
GetFolderPayload,
|
|
22
|
+
UpdateFolderPayload,
|
|
23
|
+
UploadPayload,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from pymax.app import App
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SelfService:
|
|
34
|
+
def __init__(self, app: App) -> None:
|
|
35
|
+
self.app = app
|
|
36
|
+
|
|
37
|
+
async def request_profile_photo_upload_url(self) -> str:
|
|
38
|
+
logger.info("requesting profile photo upload url")
|
|
39
|
+
frame = UploadPayload(profile=True)
|
|
40
|
+
response = await self.app.invoke(Opcode.PHOTO_UPLOAD, frame.to_payload())
|
|
41
|
+
return str(require_payload_item(response, SelfPayloadKey.URL))
|
|
42
|
+
|
|
43
|
+
async def change_profile(
|
|
44
|
+
self,
|
|
45
|
+
first_name: str,
|
|
46
|
+
last_name: str | None = None,
|
|
47
|
+
description: str | None = None,
|
|
48
|
+
photo: Any | None = None,
|
|
49
|
+
*,
|
|
50
|
+
photo_token: str | None = None,
|
|
51
|
+
) -> bool:
|
|
52
|
+
if photo is not None:
|
|
53
|
+
raise NotImplementedError(
|
|
54
|
+
"Profile photo upload is not implemented without upload infra. "
|
|
55
|
+
"Pass photo_token instead."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
frame = ChangeProfilePayload(
|
|
59
|
+
first_name=first_name,
|
|
60
|
+
last_name=last_name,
|
|
61
|
+
description=description,
|
|
62
|
+
photo_token=photo_token,
|
|
63
|
+
)
|
|
64
|
+
response = await self.app.invoke(Opcode.PROFILE, frame.to_payload())
|
|
65
|
+
profile = require_payload_item_model(
|
|
66
|
+
response,
|
|
67
|
+
SelfPayloadKey.PROFILE,
|
|
68
|
+
Profile,
|
|
69
|
+
)
|
|
70
|
+
self.app.me = profile
|
|
71
|
+
self.app.users[profile.contact.id] = profile.contact
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
async def create_folder(
|
|
75
|
+
self,
|
|
76
|
+
title: str,
|
|
77
|
+
chat_include: list[int],
|
|
78
|
+
filters: list[Any] | None = None,
|
|
79
|
+
) -> FolderUpdate:
|
|
80
|
+
logger.info("creating folder")
|
|
81
|
+
frame = CreateFolderPayload(
|
|
82
|
+
id=str(uuid4()),
|
|
83
|
+
title=title,
|
|
84
|
+
include=chat_include,
|
|
85
|
+
filters=filters or [],
|
|
86
|
+
)
|
|
87
|
+
response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
|
|
88
|
+
return require_payload_model(response, FolderUpdate)
|
|
89
|
+
|
|
90
|
+
async def get_folders(self, folder_sync: int = 0) -> FolderList:
|
|
91
|
+
logger.info("fetching folders")
|
|
92
|
+
frame = GetFolderPayload(folder_sync=folder_sync)
|
|
93
|
+
response = await self.app.invoke(Opcode.FOLDERS_GET, frame.to_payload())
|
|
94
|
+
return require_payload_model(response, FolderList)
|
|
95
|
+
|
|
96
|
+
async def update_folder(
|
|
97
|
+
self,
|
|
98
|
+
folder_id: str,
|
|
99
|
+
title: str,
|
|
100
|
+
chat_include: list[int] | None = None,
|
|
101
|
+
filters: list[Any] | None = None,
|
|
102
|
+
options: list[Any] | None = None,
|
|
103
|
+
) -> FolderUpdate:
|
|
104
|
+
logger.info("updating folder")
|
|
105
|
+
frame = UpdateFolderPayload(
|
|
106
|
+
id=folder_id,
|
|
107
|
+
title=title,
|
|
108
|
+
include=chat_include or [],
|
|
109
|
+
filters=filters or [],
|
|
110
|
+
options=options or [],
|
|
111
|
+
)
|
|
112
|
+
response = await self.app.invoke(Opcode.FOLDERS_UPDATE, frame.to_payload())
|
|
113
|
+
return require_payload_model(response, FolderUpdate)
|
|
114
|
+
|
|
115
|
+
async def delete_folder(self, folder_id: str) -> FolderUpdate:
|
|
116
|
+
logger.info("deleting folder")
|
|
117
|
+
frame = DeleteFolderPayload(folder_ids=[folder_id])
|
|
118
|
+
response = await self.app.invoke(Opcode.FOLDERS_DELETE, frame.to_payload())
|
|
119
|
+
return require_payload_model(response, FolderUpdate)
|
|
120
|
+
|
|
121
|
+
async def close_all_sessions(self) -> bool:
|
|
122
|
+
logger.info("closing all other sessions")
|
|
123
|
+
|
|
124
|
+
if not self.app.session:
|
|
125
|
+
logger.warning("no session found, skipping closing sessions")
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
response = await self.app.invoke(Opcode.SESSIONS_CLOSE, {})
|
|
129
|
+
token = payload_item(response, SelfPayloadKey.TOKEN, str)
|
|
130
|
+
|
|
131
|
+
if not token:
|
|
132
|
+
logger.warning("no token received after closing sessions, skipping token update")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
await self.app.store.update_token(self.app.session.token, token)
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
async def logout(self) -> bool:
|
|
140
|
+
logger.info("logging out")
|
|
141
|
+
await self.app.invoke(Opcode.LOGOUT, {})
|
|
142
|
+
return True
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .service import SessionService
|