maxapi-python 1.2.4__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.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.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]
@@ -0,0 +1,2 @@
1
+ from .enums import AvatarType, SelfPayloadKey
2
+ from .service import SelfService
@@ -0,0 +1,11 @@
1
+ from enum import Enum
2
+
3
+
4
+ class AvatarType(str, Enum):
5
+ USER_AVATAR = "USER_AVATAR"
6
+
7
+
8
+ class SelfPayloadKey(str, Enum):
9
+ PROFILE = "profile"
10
+ URL = "url"
11
+ TOKEN = "token"
@@ -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
@@ -0,0 +1,10 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DeviceType(str, Enum):
5
+ """Тип устройства, который отправляется в handshake и login."""
6
+
7
+ WEB = "WEB"
8
+ ANDROID = "ANDROID"
9
+ IOS = "IOS"
10
+ DESKTOP = "DESKTOP"