maxapi-python 1.1.4__tar.gz → 1.1.7__tar.gz

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 (41) hide show
  1. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/.gitignore +4 -0
  2. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/PKG-INFO +2 -2
  3. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/examples/example.py +21 -36
  4. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/pyproject.toml +2 -2
  5. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/message.py +94 -2
  6. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/user.py +36 -1
  7. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/payloads.py +16 -0
  8. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/types.py +42 -1
  9. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/.github/FUNDING.yml +0 -0
  10. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/.github/workflows/publish.yml +0 -0
  11. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/LICENSE +0 -0
  12. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/README.md +0 -0
  13. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/assets/icon.svg +0 -0
  14. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/assets/logo.svg +0 -0
  15. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/docs/api.md +0 -0
  16. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/docs/assets/icon.svg +0 -0
  17. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/docs/examples.md +0 -0
  18. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/docs/index.md +0 -0
  19. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/mkdocs.yml +0 -0
  20. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/ruff.toml +0 -0
  21. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/scripts/build.py +0 -0
  22. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/__init__.py +0 -0
  23. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/core.py +0 -0
  24. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/crud.py +0 -0
  25. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/exceptions.py +0 -0
  26. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/files.py +0 -0
  27. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/filters.py +0 -0
  28. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/interfaces.py +0 -0
  29. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/__init__.py +0 -0
  30. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/auth.py +0 -0
  31. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/channel.py +0 -0
  32. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/group.py +0 -0
  33. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/handler.py +0 -0
  34. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/self.py +0 -0
  35. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/socket.py +0 -0
  36. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/telemetry.py +0 -0
  37. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/mixins/websocket.py +0 -0
  38. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/models.py +0 -0
  39. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/navigation.py +0 -0
  40. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/static.py +0 -0
  41. {maxapi_python-1.1.4 → maxapi_python-1.1.7}/src/pymax/utils.py +0 -0
@@ -115,3 +115,7 @@ cache/
115
115
  # Keep lockfiles and important configs tracked? If you want to track specific lockfiles,
116
116
  # remove them from this .gitignore (for example: remove poetry.lock or uv.lock).
117
117
  tests/
118
+
119
+
120
+ # Bad dev's requirements
121
+ requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.1.4
3
+ Version: 1.1.7
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/noxzion/PyMax
6
6
  Project-URL: Repository, https://github.com/noxzion/PyMax
@@ -17,7 +17,7 @@ Requires-Dist: aiohttp>=3.12.15
17
17
  Requires-Dist: lz4>=4.4.4
18
18
  Requires-Dist: msgpack>=1.1.1
19
19
  Requires-Dist: sqlmodel>=0.0.24
20
- Requires-Dist: websockets>=11.0
20
+ Requires-Dist: websockets>=15.0
21
21
  Description-Content-Type: text/markdown
22
22
 
23
23
  <p align="center">
@@ -13,31 +13,6 @@ client = MaxClient(phone=phone, work_dir="cache")
13
13
  # client = SocketMaxClient(phone=phone, work_dir="cache")
14
14
 
15
15
 
16
- async def main() -> None:
17
- for chat in client.chats:
18
- print(chat.title)
19
-
20
- message = await client.send_message(
21
- "Hello from MaxClient!", chat.id, notify=True
22
- )
23
-
24
- await asyncio.sleep(5)
25
- message = await client.edit_message(
26
- chat.id, message.id, "Hello from MaxClient! (edited)"
27
- )
28
- await asyncio.sleep(5)
29
-
30
- await client.delete_message(chat.id, [message.id], for_me=False)
31
-
32
- for dialog in client.dialogs:
33
- print(dialog.last_message.text)
34
-
35
- for channel in client.channels:
36
- print(channel.title)
37
-
38
- await client.close()
39
-
40
-
41
16
  @client.on_message(filter=Filter(chat_id=0))
42
17
  async def handle_message(message: Message) -> None:
43
18
  print(str(message.sender) + ": " + message.text)
@@ -46,17 +21,27 @@ async def handle_message(message: Message) -> None:
46
21
  @client.on_start
47
22
  async def handle_start() -> None:
48
23
  print("Client started successfully!")
49
- history = await client.fetch_history(chat_id=0)
50
- if history:
51
- for message in history:
52
- user_id = message.sender
53
- chat_id = message.chat_id
54
- print(chat_id)
55
- user = await client.get_user(user_id)
56
-
57
- if user:
58
- print(f"{user.names[0].name}: {message.text}")
59
-
24
+ # print(client.dialogs)
25
+ # history = await client.fetch_history(chat_id=0)
26
+ # if history:
27
+ # for message in history:
28
+ # if message.attaches:
29
+ # for attach in message.attaches:
30
+ # if attach.type == AttachType.VIDEO:
31
+ # print(message)
32
+ # vid = await client.get_video_by_id(
33
+ # chat_id=0,
34
+ # video_id=attach.video_id,
35
+ # message_id=message.id,
36
+ # )
37
+ # print(vid.url)
38
+ # elif attach.type == AttachType.FILE:
39
+ # file = await client.get_file_by_id(
40
+ # chat_id=0,
41
+ # file_id=attach.file_id,
42
+ # message_id=message.id,
43
+ # )
44
+ # print(file.url)
60
45
  # print(client.me.names[0].first_name)
61
46
  # user = await client.get_user(client.me.id)
62
47
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "maxapi-python"
3
- version = "1.1.4"
3
+ version = "1.1.7"
4
4
  description = "Python wrapper для API мессенджера Max"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -16,7 +16,7 @@ classifiers = [
16
16
  ]
17
17
  dependencies = [
18
18
  "sqlmodel>=0.0.24",
19
- "websockets>=11.0",
19
+ "websockets>=15.0",
20
20
  "msgpack>=1.1.1",
21
21
  "lz4>=4.4.4",
22
22
  "aiohttp>=3.12.15",
@@ -10,6 +10,8 @@ from pymax.payloads import (
10
10
  DeleteMessagePayload,
11
11
  EditMessagePayload,
12
12
  FetchHistoryPayload,
13
+ GetFilePayload,
14
+ GetVideoPayload,
13
15
  PinMessagePayload,
14
16
  ReplyLink,
15
17
  SendMessagePayload,
@@ -17,7 +19,7 @@ from pymax.payloads import (
17
19
  UploadPhotoPayload,
18
20
  )
19
21
  from pymax.static import AttachType, Opcode
20
- from pymax.types import Attach, Message
22
+ from pymax.types import Attach, FileRequest, Message, VideoRequest
21
23
 
22
24
 
23
25
  class MessageMixin(ClientProtocol):
@@ -141,7 +143,6 @@ class MessageMixin(ClientProtocol):
141
143
  data = await self._send_and_wait(opcode=Opcode.MSG_SEND, payload=payload)
142
144
  if error := data.get("payload", {}).get("error"):
143
145
  self.logger.error("Send message error: %s", error)
144
- print(data)
145
146
  return None
146
147
  msg = Message.from_dict(data["payload"]) if data.get("payload") else None
147
148
  self.logger.debug("send_message result: %r", msg)
@@ -283,3 +284,94 @@ class MessageMixin(ClientProtocol):
283
284
  except Exception:
284
285
  self.logger.exception("Fetch history failed")
285
286
  return None
287
+
288
+ async def get_video_by_id(
289
+ self,
290
+ chat_id: int,
291
+ message_id: int,
292
+ video_id: int,
293
+ ) -> VideoRequest | None:
294
+ """
295
+ Получает видео
296
+
297
+ Args:
298
+ chat_id (int): ID чата
299
+ message_id (int): ID сообщения
300
+ video_id (int): ID видео
301
+
302
+ Returns:
303
+ external (str): Странная ссылка из апи
304
+ cache (bool): True, если видео кэшировано
305
+ url (str): Ссылка на видео
306
+ """
307
+ try:
308
+ self.logger.info("Getting video_id=%s message_id=%s", video_id, message_id)
309
+
310
+ if self.is_connected and self._socket is not None:
311
+ payload = GetVideoPayload(
312
+ chat_id=chat_id, message_id=message_id, video_id=video_id
313
+ ).model_dump(by_alias=True)
314
+ else:
315
+ payload = GetVideoPayload(
316
+ chat_id=chat_id, message_id=str(message_id), video_id=video_id
317
+ ).model_dump(by_alias=True)
318
+
319
+ data = await self._send_and_wait(opcode=Opcode.VIDEO_PLAY, payload=payload)
320
+
321
+ if error := data.get("payload", {}).get("error"):
322
+ self.logger.error("Get video error: %s", error)
323
+ return
324
+
325
+ video = (
326
+ VideoRequest.from_dict(data["payload"]) if data.get("payload") else None
327
+ )
328
+ self.logger.debug("result: %r", video)
329
+ return video
330
+ except Exception:
331
+ self.logger.exception("Get video error")
332
+ return None
333
+
334
+ async def get_file_by_id(
335
+ self,
336
+ chat_id: int,
337
+ message_id: int,
338
+ file_id: int,
339
+ ) -> FileRequest | None:
340
+ """
341
+ Получает файл
342
+
343
+ Args:
344
+ chat_id (int): ID чата
345
+ message_id (int): ID сообщения
346
+ file_id (int): ID видео
347
+
348
+ Returns:
349
+ unsafe (bool): Проверка файла на безопасность максом
350
+ url (str): Ссылка на скачивание файла
351
+ """
352
+ try:
353
+ self.logger.info("Getting file_id=%s message_id=%s", file_id, message_id)
354
+ if self.is_connected and self._socket is not None:
355
+ payload = GetFilePayload(
356
+ chat_id=chat_id, message_id=message_id, file_id=file_id
357
+ ).model_dump(by_alias=True)
358
+ else:
359
+ payload = GetFilePayload(
360
+ chat_id=chat_id, message_id=str(message_id), file_id=file_id
361
+ ).model_dump(by_alias=True)
362
+ data = await self._send_and_wait(
363
+ opcode=Opcode.FILE_DOWNLOAD, payload=payload
364
+ )
365
+
366
+ if error := data.get("payload", {}).get("error"):
367
+ self.logger.error("Get file error: %s", error)
368
+ return
369
+
370
+ file = (
371
+ FileRequest.from_dict(data["payload"]) if data.get("payload") else None
372
+ )
373
+ self.logger.debug(" result: %r", file)
374
+ return file
375
+ except Exception:
376
+ self.logger.exception("Get video error")
377
+ return None
@@ -1,5 +1,5 @@
1
1
  from pymax.interfaces import ClientProtocol
2
- from pymax.payloads import FetchContactsPayload
2
+ from pymax.payloads import FetchContactsPayload, SearchByPhonePayload
3
3
  from pymax.static import Opcode
4
4
  from pymax.types import User
5
5
 
@@ -80,3 +80,38 @@ class UserMixin(ClientProtocol):
80
80
  except Exception:
81
81
  self.logger.exception("Fetch users failed")
82
82
  return []
83
+
84
+ async def search_by_phone(self, phone: str) -> User | None:
85
+ """
86
+ Ищет пользователя по номеру телефона.
87
+
88
+ Args:
89
+ phone (str): Номер телефона.
90
+
91
+ Returns:
92
+ User | None: Объект User или None при ошибке.
93
+ """
94
+ try:
95
+ self.logger.info("Searching user by phone: %s", phone)
96
+
97
+ payload = SearchByPhonePayload(phone=phone).model_dump(by_alias=True)
98
+
99
+ data = await self._send_and_wait(
100
+ opcode=Opcode.CONTACT_INFO_BY_PHONE, payload=payload
101
+ )
102
+ if error := data.get("payload", {}).get("error"):
103
+ self.logger.error("Search by phone error: %s", error)
104
+ return None
105
+
106
+ user = (
107
+ User.from_dict(data["payload"]["contact"])
108
+ if data.get("payload")
109
+ else None
110
+ )
111
+ if user:
112
+ self._users[user.id] = user
113
+ self.logger.debug("Found user by phone: %s", user)
114
+ return user
115
+ except Exception:
116
+ self.logger.exception("Search by phone failed")
117
+ return None
@@ -193,3 +193,19 @@ class NavigationEventPayload(CamelModel):
193
193
 
194
194
  class NavigationPayload(CamelModel):
195
195
  events: list[NavigationEventPayload]
196
+
197
+
198
+ class GetVideoPayload(CamelModel):
199
+ chat_id: int
200
+ message_id: int | str
201
+ video_id: int
202
+
203
+
204
+ class GetFilePayload(CamelModel):
205
+ chat_id: int
206
+ message_id: str | int
207
+ file_id: int
208
+
209
+
210
+ class SearchByPhonePayload(CamelModel):
211
+ phone: str
@@ -167,6 +167,47 @@ class FileAttach:
167
167
  return f"FileAttach: {self.file_id}"
168
168
 
169
169
 
170
+ class FileRequest:
171
+ def __init__(
172
+ self,
173
+ unsafe: bool,
174
+ url: str,
175
+ ) -> None:
176
+ self.unsafe = unsafe
177
+ self.url = url
178
+
179
+ @classmethod
180
+ def from_dict(cls, data: dict[str, Any]) -> "FileRequest":
181
+ return cls(
182
+ unsafe=data["unsafe"],
183
+ url=data["url"],
184
+ )
185
+
186
+
187
+ class VideoRequest:
188
+ def __init__(
189
+ self,
190
+ external: str,
191
+ cache: bool,
192
+ url: str,
193
+ ) -> None:
194
+ self.external = external
195
+ self.cache = cache
196
+ self.url = url
197
+
198
+ @classmethod
199
+ def from_dict(cls, data: dict[str, Any]) -> "VideoRequest":
200
+ # listdata = list(data.values()) # Костыль ✅
201
+ url = [v for k, v in data.items() if k not in ("EXTERNAL", "cache")][
202
+ 0
203
+ ] # Еще больший костыль ✅
204
+ return cls(
205
+ external=data["EXTERNAL"],
206
+ cache=data["cache"],
207
+ url=url,
208
+ )
209
+
210
+
170
211
  class Me:
171
212
  def __init__(
172
213
  self,
@@ -538,7 +579,7 @@ class User:
538
579
  return f"User {self.id}: {', '.join(str(n) for n in self.names)}"
539
580
 
540
581
 
541
- class Attach: # УБРАТЬ ГАДА!!!
582
+ class Attach: # УБРАТЬ ГАДА!!! или нет...
542
583
  def __init__(
543
584
  self,
544
585
  _type: AttachType,
File without changes
File without changes
File without changes
File without changes
File without changes