maxapi-python 1.2.2__tar.gz → 1.2.3__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 (67) hide show
  1. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/PKG-INFO +1 -1
  2. maxapi_python-1.2.3/examples/example.py +74 -0
  3. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/pyproject.toml +1 -1
  4. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/core.py +1 -0
  5. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/files.py +5 -0
  6. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/interfaces.py +1 -0
  7. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/group.py +31 -12
  8. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/self.py +74 -9
  9. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/socket.py +10 -0
  10. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/websocket.py +9 -0
  11. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/payloads.py +3 -0
  12. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/static/enum.py +1 -0
  13. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/types.py +57 -28
  14. maxapi_python-1.2.2/examples/example.py +0 -268
  15. maxapi_python-1.2.2/examples/flt_test.py +0 -51
  16. maxapi_python-1.2.2/examples/large_file_upload.py +0 -51
  17. maxapi_python-1.2.2/examples/reg.py +0 -34
  18. maxapi_python-1.2.2/examples/test.py +0 -20
  19. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.coderabbit.yaml +0 -0
  20. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/FUNDING.yml +0 -0
  21. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/ISSUE_TEMPLATE/refactor.md +0 -0
  24. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/pull_request_template.md +0 -0
  25. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/workflows/publish.yml +0 -0
  26. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.github/workflows/tests.yml +0 -0
  27. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.gitignore +0 -0
  28. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/.pre-commit-config.yaml +0 -0
  29. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/LICENSE +0 -0
  30. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/README.md +0 -0
  31. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/assets/icon.svg +0 -0
  32. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/assets/logo.svg +0 -0
  33. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/examples/telegram_bridge.py +0 -0
  34. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/mkdocs.yml +0 -0
  35. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/pytest.ini +0 -0
  36. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/Makefile +0 -0
  37. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/build.sh +0 -0
  38. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/make.bat +0 -0
  39. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/_static/logo.svg +0 -0
  40. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/clients.rst +0 -0
  41. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/conf.py +0 -0
  42. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/decorators.rst +0 -0
  43. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/examples.rst +0 -0
  44. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/guides.rst +0 -0
  45. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/index.rst +0 -0
  46. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/installation.rst +0 -0
  47. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/quickstart.rst +0 -0
  48. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/redocs/source/types.rst +0 -0
  49. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/ruff.toml +0 -0
  50. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/__init__.py +0 -0
  51. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/crud.py +0 -0
  52. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/exceptions.py +0 -0
  53. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/filters.py +0 -0
  54. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/formatter.py +0 -0
  55. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/formatting.py +0 -0
  56. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/__init__.py +0 -0
  57. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/auth.py +0 -0
  58. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/channel.py +0 -0
  59. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/handler.py +0 -0
  60. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/message.py +0 -0
  61. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/scheduler.py +0 -0
  62. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/telemetry.py +0 -0
  63. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/user.py +0 -0
  64. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/mixins/utils.py +0 -0
  65. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/models.py +0 -0
  66. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/navigation.py +0 -0
  67. {maxapi_python-1.2.2 → maxapi_python-1.2.3}/src/pymax/static/constant.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/ink-developer/PyMax
6
6
  Project-URL: Repository, https://github.com/ink-developer/PyMax
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+ import datetime
3
+ import logging
4
+ from time import time
5
+ from typing import Any
6
+
7
+ import pymax
8
+ from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient, filters
9
+ from pymax.files import File, Photo, Video
10
+ from pymax.filters import Filters
11
+ from pymax.payloads import UserAgentPayload
12
+ from pymax.static.enum import AttachType, Opcode
13
+ from pymax.types import Chat
14
+
15
+ phone = "+79991234567"
16
+ headers = UserAgentPayload(device_type="WEB")
17
+
18
+ client = MaxClient(
19
+ phone=phone,
20
+ work_dir="cache",
21
+ reconnect=False,
22
+ logger=None,
23
+ headers=headers,
24
+ )
25
+ client.logger.setLevel(logging.INFO)
26
+
27
+
28
+ @client.on_start
29
+ async def handle_start() -> None:
30
+ print(f"Client started as {client.me.names[0].first_name}!")
31
+
32
+
33
+ @client.on_raw_receive
34
+ async def handle_raw_receive(data: dict[str, Any]) -> None:
35
+ print(f"Raw data received: {data}")
36
+
37
+
38
+ @client.on_reaction_change
39
+ async def handle_reaction_change(
40
+ message_id: str, chat_id: int, reaction_info: ReactionInfo
41
+ ) -> None:
42
+ print(
43
+ f"Reaction changed on message {message_id} in chat {chat_id}: "
44
+ f"Total count: {reaction_info.total_count}, "
45
+ f"Your reaction: {reaction_info.your_reaction}, "
46
+ f"Counters: {reaction_info.counters[0].reaction}={reaction_info.counters[0].count}"
47
+ )
48
+
49
+
50
+ @client.on_chat_update
51
+ async def handle_chat_update(chat: Chat) -> None:
52
+ print(f"Chat updated: {chat.id}, new title: {chat.title}")
53
+
54
+
55
+ @client.on_message(Filters.chat(0) & Filters.text("hello"))
56
+ async def handle_message(message: Message) -> None:
57
+ print(f"New message in chat {message.chat_id} from {message.sender}: {message.text}")
58
+
59
+
60
+ @client.on_message_edit()
61
+ async def handle_edited_message(message: Message) -> None:
62
+ print(f"Edited message in chat {message.chat_id}: {message.text}")
63
+
64
+
65
+ @client.on_message_delete()
66
+ async def handle_deleted_message(message: Message) -> None:
67
+ print(f"Deleted message in chat {message.chat_id}: {message.id}")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ try:
72
+ asyncio.run(client.start())
73
+ except KeyboardInterrupt:
74
+ print("Client stopped by user")
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "maxapi-python"
3
- version = "1.2.2"
3
+ version = "1.2.3"
4
4
  description = "Python wrapper для API мессенджера Max"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -116,6 +116,7 @@ class MaxClient(ApiMixin, WebSocketMixin, BaseClient):
116
116
  self.dialogs: list[Dialog] = []
117
117
  self.channels: list[Channel] = []
118
118
  self.me: Me | None = None
119
+ self.contacts: list[User] = []
119
120
  self._users: dict[int, User] = {}
120
121
 
121
122
  self._work_dir: str = work_dir
@@ -46,6 +46,11 @@ class Photo(BaseFile):
46
46
  } # FIXME: костыль ✅
47
47
 
48
48
  def __init__(self, url: str | None = None, path: str | None = None) -> None:
49
+ if path:
50
+ self.file_name = Path(path).name
51
+ elif url:
52
+ self.file_name = Path(url).name
53
+
49
54
  super().__init__(url, path)
50
55
 
51
56
  def validate_photo(self) -> tuple[str, str] | None:
@@ -45,6 +45,7 @@ class ClientProtocol(ABC):
45
45
  self.phone: str
46
46
  self.dialogs: list[Dialog] = []
47
47
  self.channels: list[Channel] = []
48
+ self.contacts: list[User] = []
48
49
  self.me: Me | None = None
49
50
  self.host: str
50
51
  self.port: int
@@ -95,9 +95,7 @@ class GroupMixin(ClientProtocol):
95
95
  operation="add",
96
96
  ).model_dump(by_alias=True)
97
97
 
98
- data = await self._send_and_wait(
99
- opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
100
- )
98
+ data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload)
101
99
 
102
100
  if data.get("payload", {}).get("error"):
103
101
  MixinsUtils.handle_error(data)
@@ -155,9 +153,7 @@ class GroupMixin(ClientProtocol):
155
153
  clean_msg_period=clean_msg_period,
156
154
  ).model_dump(by_alias=True)
157
155
 
158
- data = await self._send_and_wait(
159
- opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload
160
- )
156
+ data = await self._send_and_wait(opcode=Opcode.CHAT_MEMBERS_UPDATE, payload=payload)
161
157
 
162
158
  if data.get("payload", {}).get("error"):
163
159
  MixinsUtils.handle_error(data)
@@ -293,6 +289,33 @@ class GroupMixin(ClientProtocol):
293
289
 
294
290
  return chat
295
291
 
292
+ async def resolve_group_by_link(self, link: str) -> Chat | None:
293
+ """
294
+ Разрешает группу по ссылке
295
+
296
+ Args:
297
+ link (str): Ссылка на группу.
298
+
299
+ Returns:
300
+ Chat | None: Объект чата группы или None, если не найдено.
301
+ """
302
+ proceed_link = self._process_chat_join_link(link)
303
+ if proceed_link is None:
304
+ raise ValueError("Invalid group link")
305
+
306
+ data = await self._send_and_wait(
307
+ opcode=Opcode.LINK_INFO,
308
+ payload={
309
+ "link": proceed_link,
310
+ },
311
+ )
312
+
313
+ if data.get("payload", {}).get("error"):
314
+ MixinsUtils.handle_error(data)
315
+
316
+ chat = Chat.from_dict(data["payload"].get("chat", {}))
317
+ return chat
318
+
296
319
  async def rework_invite_link(self, chat_id: int) -> Chat:
297
320
  """
298
321
  Пересоздает ссылку для приглашения в группу
@@ -329,14 +352,10 @@ class GroupMixin(ClientProtocol):
329
352
  chat_id for chat_id in chat_ids if await self._get_chat(chat_id) is None
330
353
  ]
331
354
  if missed_chat_ids:
332
- payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(
333
- by_alias=True
334
- )
355
+ payload = GetChatInfoPayload(chat_ids=missed_chat_ids).model_dump(by_alias=True)
335
356
  else:
336
357
  chats: list[Chat] = [
337
- chat
338
- for chat_id in chat_ids
339
- if (chat := await self._get_chat(chat_id)) is not None
358
+ chat for chat_id in chat_ids if (chat := await self._get_chat(chat_id)) is not None
340
359
  ]
341
360
  return chats
342
361
 
@@ -1,7 +1,13 @@
1
+ import urllib.parse
2
+ from http import HTTPStatus
1
3
  from typing import Any
4
+ from urllib.parse import parse_qs, urlparse
2
5
  from uuid import uuid4
3
6
 
7
+ import aiohttp
8
+
4
9
  from pymax.exceptions import Error
10
+ from pymax.files import Photo
5
11
  from pymax.interfaces import ClientProtocol
6
12
  from pymax.mixins.utils import MixinsUtils
7
13
  from pymax.payloads import (
@@ -10,17 +16,60 @@ from pymax.payloads import (
10
16
  DeleteFolderPayload,
11
17
  GetFolderPayload,
12
18
  UpdateFolderPayload,
19
+ UploadPayload,
13
20
  )
14
21
  from pymax.static.enum import Opcode
15
- from pymax.types import Folder, FolderList, FolderUpdate
22
+ from pymax.types import Folder, FolderList, FolderUpdate, Me
16
23
 
17
24
 
18
25
  class SelfMixin(ClientProtocol):
26
+ async def _request_photo_upload_url(self) -> str:
27
+ self.logger.info("Requesting profile photo upload URL")
28
+
29
+ data = await self._send_and_wait(
30
+ opcode=Opcode.PHOTO_UPLOAD,
31
+ payload=UploadPayload(profile=True).model_dump(by_alias=True),
32
+ )
33
+
34
+ if data.get("payload", {}).get("error"):
35
+ MixinsUtils.handle_error(data)
36
+
37
+ return data["payload"]["url"]
38
+
39
+ async def _upload_profile_photo(self, upload_url: str, photo: Photo) -> str:
40
+ self.logger.info("Uploading profile photo")
41
+
42
+ parsed_url = urlparse(upload_url)
43
+ photo_id = parse_qs(parsed_url.query)["photoIds"][0]
44
+
45
+ form = aiohttp.FormData()
46
+ form.add_field(
47
+ "file",
48
+ await photo.read(),
49
+ filename=photo.file_name,
50
+ )
51
+
52
+ async with (
53
+ aiohttp.ClientSession() as session,
54
+ session.post(upload_url, data=form) as response,
55
+ ):
56
+ if response.status != HTTPStatus.OK:
57
+ raise Error(
58
+ "Failed to upload profile photo.", message="UploadError", title="Upload Error"
59
+ )
60
+
61
+ self.logger.info("Upload successful")
62
+ data = await response.json()
63
+ return data["photos"][photo_id][
64
+ "token"
65
+ ] # TODO: сделать нормальную типизацию и чекнинг ответа
66
+
19
67
  async def change_profile(
20
68
  self,
21
69
  first_name: str,
22
70
  last_name: str | None = None,
23
71
  description: str | None = None,
72
+ photo: Photo | None = None,
24
73
  ) -> bool:
25
74
  """
26
75
  Изменяет информацию профиля текущего пользователя.
@@ -35,20 +84,36 @@ class SelfMixin(ClientProtocol):
35
84
  :rtype: bool
36
85
  """
37
86
 
38
- payload = ChangeProfilePayload(
39
- first_name=first_name,
40
- last_name=last_name,
41
- description=description,
42
- ).model_dump(
43
- by_alias=True,
44
- exclude_none=True,
45
- )
87
+ if photo:
88
+ upload_url = await self._request_photo_upload_url()
89
+ photo_token = await self._upload_profile_photo(upload_url, photo)
90
+
91
+ payload = ChangeProfilePayload(
92
+ first_name=first_name,
93
+ last_name=last_name,
94
+ description=description,
95
+ photo_token=photo_token,
96
+ ).model_dump(
97
+ by_alias=True,
98
+ exclude_none=True,
99
+ )
100
+ else:
101
+ payload = ChangeProfilePayload(
102
+ first_name=first_name,
103
+ last_name=last_name,
104
+ description=description,
105
+ ).model_dump(
106
+ by_alias=True,
107
+ exclude_none=True,
108
+ )
46
109
 
47
110
  data = await self._send_and_wait(opcode=Opcode.PROFILE, payload=payload)
48
111
 
49
112
  if data.get("payload", {}).get("error"):
50
113
  MixinsUtils.handle_error(data)
51
114
 
115
+ self.me = Me.from_dict(data["payload"]["profile"]["contact"])
116
+
52
117
  return True
53
118
 
54
119
  async def create_folder(
@@ -28,6 +28,7 @@ from pymax.types import (
28
28
  Message,
29
29
  ReactionCounter,
30
30
  ReactionInfo,
31
+ User,
31
32
  )
32
33
 
33
34
 
@@ -603,6 +604,15 @@ Socket connections may be unstable, SSL issues are possible.
603
604
  self.channels.append(Channel.from_dict(raw_chat))
604
605
  except Exception:
605
606
  self.logger.exception("Error parsing chat entry (socket)")
607
+
608
+ for raw_user in raw_payload.get("contacts", []):
609
+ try:
610
+ user = User.from_dict(raw_user)
611
+ if user:
612
+ self.contacts.append(user)
613
+ except Exception:
614
+ self.logger.exception("Error parsing contact entry (socket)")
615
+
606
616
  if raw_payload.get("profile", {}).get("contact"):
607
617
  self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
608
618
  self.logger.info(
@@ -27,6 +27,7 @@ from pymax.types import (
27
27
  Message,
28
28
  ReactionCounter,
29
29
  ReactionInfo,
30
+ User,
30
31
  )
31
32
 
32
33
 
@@ -465,6 +466,14 @@ class WebSocketMixin(ClientProtocol):
465
466
  except Exception:
466
467
  self.logger.exception("Error parsing chat entry")
467
468
 
469
+ for raw_user in raw_payload.get("contacts", []):
470
+ try:
471
+ user = User.from_dict(raw_user)
472
+ if user:
473
+ self.contacts.append(user)
474
+ except Exception:
475
+ self.logger.exception("Error parsing contact entry")
476
+
468
477
  if raw_payload.get("profile", {}).get("contact"):
469
478
  self.me = Me.from_dict(raw_payload.get("profile", {}).get("contact", {}))
470
479
 
@@ -97,6 +97,7 @@ class ReplyLink(CamelModel):
97
97
 
98
98
  class UploadPayload(CamelModel):
99
99
  count: int = 1
100
+ profile: bool = False
100
101
 
101
102
 
102
103
  class AttachPhotoPayload(CamelModel):
@@ -168,6 +169,8 @@ class ChangeProfilePayload(CamelModel):
168
169
  first_name: str
169
170
  last_name: str | None = None
170
171
  description: str | None = None
172
+ photo_token: str | None = None
173
+ avatar_type: str = "USER_AVATAR" # TODO: вынести гада в энам
171
174
 
172
175
 
173
176
  class ResolveLinkPayload(CamelModel):
@@ -196,6 +196,7 @@ class AttachType(str, Enum):
196
196
  STICKER = "STICKER"
197
197
  AUDIO = "AUDIO"
198
198
  CONTROL = "CONTROL"
199
+ CONTACT = "CONTACT"
199
200
 
200
201
 
201
202
  class FormattingType(str, Enum):
@@ -47,7 +47,7 @@ class Name:
47
47
  def __init__(
48
48
  self,
49
49
  name: str | None,
50
- first_name: None,
50
+ first_name: None | str,
51
51
  last_name: str | None,
52
52
  type: str | None,
53
53
  ) -> None:
@@ -90,16 +90,14 @@ class Names(Name):
90
90
  def __init__(
91
91
  self,
92
92
  name: str | None,
93
- first_name: None,
93
+ first_name: None | str,
94
94
  last_name: str | None,
95
95
  type: str | None,
96
96
  ) -> None:
97
97
  """
98
98
  Синоним для класса Name.
99
99
  """
100
- super().__init__(
101
- name=name, first_name=first_name, last_name=last_name, type=type
102
- )
100
+ super().__init__(name=name, first_name=first_name, last_name=last_name, type=type)
103
101
 
104
102
 
105
103
  class Contact:
@@ -219,7 +217,7 @@ class StickerAttach:
219
217
  def __init__(
220
218
  self,
221
219
  author_type: str,
222
- lottie_url: str,
220
+ lottie_url: str | None,
223
221
  url: str,
224
222
  sticker_id: int,
225
223
  tags: list[str] | None,
@@ -248,7 +246,7 @@ class StickerAttach:
248
246
  def from_dict(cls, data: dict[str, Any]) -> Self:
249
247
  return cls(
250
248
  author_type=data["authorType"],
251
- lottie_url=data["lottieUrl"],
249
+ lottie_url=data.get("lottieUrl"),
252
250
  url=data["url"],
253
251
  sticker_id=data["stickerId"],
254
252
  tags=data.get("tags"),
@@ -443,9 +441,7 @@ class VideoAttach:
443
441
 
444
442
 
445
443
  class FileAttach:
446
- def __init__(
447
- self, file_id: int, name: str, size: int, token: str, type: AttachType
448
- ) -> None:
444
+ def __init__(self, file_id: int, name: str, size: int, token: str, type: AttachType) -> None:
449
445
  self.file_id = file_id
450
446
  self.name = name
451
447
  self.size = size
@@ -553,9 +549,7 @@ class Me:
553
549
 
554
550
 
555
551
  class Element:
556
- def __init__(
557
- self, type: FormattingType | str, length: int, from_: int | None = None
558
- ) -> None:
552
+ def __init__(self, type: FormattingType | str, length: int, from_: int | None = None) -> None:
559
553
  self.type = type
560
554
  self.length = length
561
555
  self.from_ = from_
@@ -566,9 +560,7 @@ class Element:
566
560
 
567
561
  @override
568
562
  def __repr__(self) -> str:
569
- return (
570
- f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
571
- )
563
+ return f"Element(type={self.type!r}, length={self.length!r}, from_={self.from_!r})"
572
564
 
573
565
  @override
574
566
  def __str__(self) -> str:
@@ -591,7 +583,9 @@ class MessageLink:
591
583
 
592
584
  @override
593
585
  def __repr__(self) -> str:
594
- return f"MessageLink(chat_id={self.chat_id!r}, message={self.message!r}, type={self.type!r})"
586
+ return (
587
+ f"MessageLink(chat_id={self.chat_id!r}, message={self.message!r}, type={self.type!r})"
588
+ )
595
589
 
596
590
  @override
597
591
  def __str__(self) -> str:
@@ -636,6 +630,36 @@ class ReactionInfo:
636
630
  )
637
631
 
638
632
 
633
+ class ContactAttach:
634
+ def __init__(
635
+ self, contact_id: int, first_name: str, last_name: str, name: str, photo_url: str
636
+ ) -> None:
637
+ self.contact_id = contact_id
638
+ self.first_name = first_name
639
+ self.last_name = last_name
640
+ self.name = name
641
+ self.photo_url = photo_url
642
+ self.type = AttachType.CONTACT
643
+
644
+ @classmethod
645
+ def from_dict(cls, data: dict[str, Any]) -> Self:
646
+ return cls(
647
+ contact_id=data["contactId"],
648
+ first_name=data["firstName"],
649
+ last_name=data["lastName"],
650
+ name=data["name"],
651
+ photo_url=data["photoUrl"],
652
+ )
653
+
654
+ @override
655
+ def __repr__(self) -> str:
656
+ return f"ContactAttach(contact_id={self.contact_id!r}, first_name={self.first_name!r}, last_name={self.last_name!r}, name={self.name!r}, photo_url={self.photo_url!r})"
657
+
658
+ @override
659
+ def __str__(self) -> str:
660
+ return f"ContactAttach: {self.name}"
661
+
662
+
639
663
  class Message:
640
664
  def __init__(
641
665
  self,
@@ -658,6 +682,7 @@ class Message:
658
682
  | ControlAttach
659
683
  | StickerAttach
660
684
  | AudioAttach
685
+ | ContactAttach
661
686
  ]
662
687
  | None
663
688
  ),
@@ -679,7 +704,13 @@ class Message:
679
704
  def from_dict(cls, data: dict[Any, Any]) -> Self:
680
705
  message = data["message"] if data.get("message") else data
681
706
  attaches: list[
682
- PhotoAttach | VideoAttach | FileAttach | ControlAttach | StickerAttach
707
+ PhotoAttach
708
+ | VideoAttach
709
+ | FileAttach
710
+ | ControlAttach
711
+ | StickerAttach
712
+ | AudioAttach
713
+ | ContactAttach
683
714
  ] = []
684
715
  for a in message.get("attaches", []):
685
716
  if a["_type"] == AttachType.PHOTO:
@@ -694,6 +725,8 @@ class Message:
694
725
  attaches.append(StickerAttach.from_dict(a))
695
726
  elif a["_type"] == AttachType.AUDIO:
696
727
  attaches.append(AudioAttach.from_dict(a))
728
+ elif a["_type"] == AttachType.CONTACT:
729
+ attaches.append(ContactAttach.from_dict(a))
697
730
  link_value = message.get("link")
698
731
  if isinstance(link_value, dict):
699
732
  link = MessageLink.from_dict(link_value)
@@ -778,9 +811,7 @@ class Dialog:
778
811
  join_time=data["joinTime"],
779
812
  created=data["created"],
780
813
  last_message=(
781
- Message.from_dict(data["lastMessage"])
782
- if data.get("lastMessage")
783
- else None
814
+ Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
784
815
  ),
785
816
  type=ChatType(data["type"]),
786
817
  last_fire_delayed_error_time=data["lastFireDelayedErrorTime"],
@@ -865,14 +896,10 @@ class Chat:
865
896
  @classmethod
866
897
  def from_dict(cls, data: dict[Any, Any]) -> Self:
867
898
  raw_admins = data.get("adminParticipants", {}) or {}
868
- admin_participants: dict[int, dict[Any, Any]] = {
869
- int(k): v for k, v in raw_admins.items()
870
- }
899
+ admin_participants: dict[int, dict[Any, Any]] = {int(k): v for k, v in raw_admins.items()}
871
900
  raw_participants = data.get("participants", {}) or {}
872
901
  participants: dict[int, int] = {int(k): v for k, v in raw_participants.items()}
873
- last_msg = (
874
- Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
875
- )
902
+ last_msg = Message.from_dict(data["lastMessage"]) if data.get("lastMessage") else None
876
903
  return cls(
877
904
  participants_count=data.get("participantsCount", 0),
878
905
  access=AccessType(data.get("access", AccessType.PUBLIC.value)),
@@ -1051,7 +1078,9 @@ class Session:
1051
1078
 
1052
1079
  @override
1053
1080
  def __str__(self) -> str:
1054
- return f"Session: {self.client} from {self.location} at {self.time} (current={self.current})"
1081
+ return (
1082
+ f"Session: {self.client} from {self.location} at {self.time} (current={self.current})"
1083
+ )
1055
1084
 
1056
1085
 
1057
1086
  class Folder:
@@ -1,268 +0,0 @@
1
- import asyncio
2
- import datetime
3
- import logging
4
- from time import time
5
- from typing import Any
6
-
7
- import pymax
8
- from pymax import MaxClient, Message, ReactionInfo, SocketMaxClient
9
- from pymax.files import File, Video
10
- from pymax.payloads import UserAgentPayload
11
- from pymax.static.enum import AttachType, Opcode
12
- from pymax.types import Chat
13
-
14
- phone = "+7903223111"
15
- headers = UserAgentPayload(device_type="WEB")
16
-
17
- client = MaxClient(
18
- phone=phone,
19
- work_dir="cache",
20
- reconnect=False,
21
- logger=None,
22
- headers=headers,
23
- )
24
- client.logger.setLevel(logging.INFO)
25
-
26
-
27
- @client.on_raw_receive
28
- async def handle_raw_receive(data: dict[str, Any]) -> None:
29
- print(f"Raw data received: {data}")
30
-
31
-
32
- @client.task(seconds=10)
33
- async def periodic_task() -> None:
34
- # print(f"Periodic task executed at {datetime.datetime.now()}")
35
- ...
36
-
37
-
38
- @client.on_start
39
- async def handle_start() -> None:
40
- print(f"Client started as {client.me.names[0].first_name}!")
41
-
42
- chat_id = -1
43
- max_messages = 1000
44
- messages = []
45
- from_time = int(time() * 1000)
46
- while len(messages) < max_messages:
47
- r = await client.fetch_history(chat_id=chat_id, from_time=from_time, backward=30)
48
- if not r:
49
- break
50
- from_time = r[0].time
51
- messages.extend(r)
52
- print(f"First message time: {from_time}, id: {r[0].id}, text: {r[0].text}")
53
- print(f"Last message time: {from_time}, id: {r[-1].id}, text: {r[-1].text}")
54
- print(f"Loaded {len(messages)}/{max_messages} messages...")
55
- # channel = await client.resolve_channel_by_name("fm92")
56
- # if channel:
57
- # print(f"Resolved channel by name: {channel.title}, ID: {channel.id}")
58
- # else:
59
- # print("Channel not found by name.")
60
-
61
- # channel = await client.join_channel(link)
62
- # if channel:
63
- # print(f"Joined channel: {channel.title}, ID: {channel.id}")
64
- # else:
65
- # print("Failed to join channel.")
66
- # await client.send_message(
67
- # "Hello! The client has started successfully.",
68
- # chat_id=2265456546456,
69
- # notify=True,
70
- # )
71
- # folder_update = await client.create_folder(
72
- # title="My Folder",
73
- # chat_include=[0],
74
- # )
75
- # print(f"Folder created: {folder_update.folder.title}")
76
- # video_path = "tests2/test.mp4"
77
- # video_file = Video(path=video_path)
78
-
79
- # await client.send_message(
80
- # text="Here is the video you requested.",
81
- # chat_id=0,
82
- # attachment=video_file,
83
- # notify=True,
84
- # )
85
- # chat_id = -6970655
86
- # for chat in client.chats:
87
- # if chat.id == chat_id:
88
- # print(f"Found chat: {chat.title}, ID: {chat.id}")
89
- # members_count = chat.participants_count
90
- # marker = 0
91
- # member_list = []
92
- # while len(member_list) < members_count:
93
- # await asyncio.sleep(10)
94
- # r = await client.load_members(
95
- # chat_id=chat_id,
96
- # marker=marker,
97
- # count=200,
98
- # )
99
- # members, marker = r
100
- # member_list.extend(members)
101
- # print(f"Loaded {len(member_list)}/{members_count} members...")
102
- # for member in member_list:
103
- # print(
104
- # f"Member {member.contact.names[0].first_name}, ID: {member.contact.id}"
105
- # )
106
- # r = await client.load_members(chat_id=chat_id, count=50)
107
- # print(f"Loaded {len(r)} members from chat {chat_id}")
108
- # member_list, marker = r
109
- # for member in member_list:
110
- # print(f"Member {member.contact.names[0].first_name}, ID: {member.contact.id}")
111
-
112
-
113
- # @client.on_reaction_change
114
- # async def handle_reaction_change(
115
- # message_id: str, chat_id: int, reaction_info: ReactionInfo
116
- # ) -> None:
117
- # print(
118
- # f"Reaction changed on message {message_id} in chat {chat_id}: "
119
- # f"Total count: {reaction_info.total_count}, "
120
- # f"Your reaction: {reaction_info.your_reaction}, "
121
- # f"Counters: {reaction_info.counters[0].reaction}={reaction_info.counters[0].count}"
122
- # )
123
-
124
-
125
- # @client.on_chat_update
126
- # async def handle_chat_update(chat: Chat) -> None:
127
- # print(f"Chat updated: {chat.id}, new title: {chat.title}")
128
-
129
-
130
- @client.on_message()
131
- async def handle_message(message: Message) -> None:
132
- print(f"New message in chat {message.chat_id} from {message.sender}: {message.text}")
133
- # if message.link and message.link.message.attaches:
134
- # for attach in message.link.message.attaches:
135
- # print(f"Link attach type: {attach.type}")
136
-
137
-
138
- @client.on_message_edit()
139
- async def handle_edited_message(message: Message) -> None:
140
- print(f"Edited message in chat {message.chat_id}: {message.text}")
141
-
142
-
143
- @client.on_message_delete()
144
- async def handle_deleted_message(message: Message) -> None:
145
- print(f"Deleted message in chat {message.chat_id}: {message.id}")
146
-
147
-
148
- # async def login_flow_test():
149
- # await client.connect()
150
- # temp_token = await client.request_code(phone)
151
- # code = input("Введите код: ").strip()
152
- # await client.login_with_code(temp_token, code)
153
-
154
-
155
- # asyncio.run(login_flow_test())
156
-
157
- # @client.on_message(filter=Filter(chat_id=0))
158
- # async def handle_message(message: Message) -> None:
159
- # print(str(message.sender) + ": " + message.text)
160
-
161
-
162
- # @client.on_message_edit()
163
- # async def handle_edited_message(message: Message) -> None:
164
- # print(f"Edited message in chat {message.chat_id}: {message.text}")
165
-
166
-
167
- # @client.on_message_delete()
168
- # async def handle_deleted_message(message: Message) -> None:
169
- # print(f"Deleted message in chat {message.chat_id}: {message.id}")
170
-
171
-
172
- # @client.on_start
173
- # async def handle_start() -> None:
174
- # print(f"Client started successfully at {datetime.datetime.now()}!")
175
- # print(client.me.id)
176
-
177
- # await client.send_message(
178
- # "Hello, this is a test message sent upon client start!",
179
- # chat_id=23424,
180
- # notify=True,
181
- # )
182
- # file_path = "ruff.toml"
183
- # file = File(path=file_path)
184
- # msg = await client.send_message(
185
- # text="Here is the file you requested.",
186
- # chat_id=0,
187
- # attachment=file,
188
- # notify=True,
189
- # )
190
- # if msg:
191
- # print(f"File sent successfully in message ID: {msg.id}")
192
- # history = await client.fetch_history(chat_id=0)
193
- # if history:
194
- # for message in history:
195
- # if message.attaches:
196
- # for attach in message.attaches:
197
- # if attach.type == AttachType.AUDIO:
198
- # print(attach.url)
199
- # chat = await client.rework_invite_link(chat_id=0)
200
- # print(chat.link)
201
- # text = """
202
- # **123**
203
- # *123*
204
- # __123__
205
- # ~~123~~
206
- # """
207
- # message = await client.send_message(text, chat_id=0, notify=True)
208
- # react_info = await client.add_reaction(
209
- # chat_id=0, message_id="115368067020359151", reaction="👍"
210
- # )
211
- # if react_info:
212
- # print("Reaction added!")
213
- # print(react_info.total_count)
214
- # react_info = await client.get_reactions(
215
- # chat_id=0, message_ids=["115368067020359151"]
216
- # )
217
- # if react_info:
218
- # print("Reactions fetched!")
219
- # for msg_id, info in react_info.items():
220
- # print(f"Message ID: {msg_id}, Total Reactions: {info.total_count}")
221
- # react_info = await client.remove_reaction(
222
- # chat_id=0, message_id="115368067020359151"
223
- # )
224
- # if react_info:
225
- # print("Reaction removed!")
226
- # print(react_info.total_count)
227
- # print(client.dialogs)
228
-
229
- # if history:
230
- # for message in history:
231
- # if message.link:
232
- # print(message.link.chat_id)
233
- # print(message.link.message.text)
234
- # for attach in message.attaches:
235
- # if attach.type == AttachType.CONTROL:
236
- # print(attach.event)
237
- # print(attach.extra)
238
- # if attach.type == AttachType.VIDEO:
239
- # print(message)
240
- # vid = await client.get_video_by_id(
241
- # chat_id=0,
242
- # video_id=attach.video_id,
243
- # message_id=message.id,
244
- # )
245
- # print(vid.url)
246
- # elif attach.type == AttachType.FILE:
247
- # file = await client.get_file_by_id(
248
- # chat_id=0,
249
- # file_id=attach.file_id,
250
- # message_id=message.id,
251
- # )
252
- # print(file.url)
253
- # print(client.me.names[0].first_name)
254
- # user = await client.get_user(client.me.id)
255
-
256
- # photo1 = Photo(path="tests/test.jpeg")
257
- # photo2 = Photo(path="tests/test.jpg")
258
-
259
- # await client.send_message(
260
- # "Hello with photo!", chat_id=0, photos=[photo1, photo2], notify=True
261
- # )
262
-
263
-
264
- if __name__ == "__main__":
265
- try:
266
- asyncio.run(client.start())
267
- except KeyboardInterrupt:
268
- print("Client stopped by user")
@@ -1,51 +0,0 @@
1
- import asyncio
2
- import logging
3
-
4
- import pymax
5
- import pymax.static
6
- from pymax import MaxClient
7
- from pymax.filters import Filters
8
- from pymax.payloads import UserAgentPayload
9
- from pymax.static.enum import Opcode
10
-
11
- phone = "+7903223423"
12
- headers = UserAgentPayload(device_type="WEB")
13
-
14
- client = MaxClient(
15
- phone=phone,
16
- work_dir="cache",
17
- reconnect=False,
18
- logger=None,
19
- headers=headers,
20
- )
21
- client.logger.setLevel(logging.DEBUG)
22
-
23
-
24
- @client.task(seconds=10)
25
- async def periodic_task() -> None:
26
- client.logger.info("Periodic task executed")
27
-
28
-
29
- @client.on_message(Filters.text("test") & ~Filters.chat(0))
30
- async def handle_message(message: pymax.Message) -> None:
31
- print(f"New message from {message.sender}: {message.text}")
32
-
33
-
34
- @client.on_start
35
- async def on_start():
36
- print("Client started")
37
- data = await client._send_and_wait(
38
- opcode=Opcode.FILE_UPLOAD,
39
- payload={"count": 1},
40
- )
41
- print("File upload response:", data)
42
- # opcode=pymax.static.enum.Opcode.CHATS_LIST,
43
- # payload={
44
- # "marker": 1765721869777,
45
- # },
46
- # )
47
-
48
- # print("Chats list:", data)
49
-
50
-
51
- asyncio.run(client.start())
@@ -1,51 +0,0 @@
1
- import asyncio
2
- import logging
3
- from pathlib import Path
4
-
5
- from pymax import MaxClient
6
- from pymax.files import File, Video
7
-
8
- client = MaxClient(phone="+1234567890", work_dir="cache", reconnect=False)
9
- client.logger.setLevel(logging.INFO)
10
-
11
-
12
- def create_big_file(file_path: Path, size_in_mb: int) -> None:
13
- with open(file_path, "wb") as f:
14
- f.seek(size_in_mb * 1024 * 1024 - 1)
15
- f.write(b"\0")
16
-
17
-
18
- @client.on_start
19
- async def upload_large_file_example():
20
- await asyncio.sleep(2)
21
-
22
- file_path = Path("tests2/large_file.dat")
23
-
24
- if not file_path.exists():
25
- create_big_file(file_path, size_in_mb=300)
26
- file_size = file_path.stat().st_size
27
- client.logger.info(f"File size: {file_size / (1024 * 1024):.2f} MB")
28
-
29
- file = File(path=str(file_path))
30
- chat_id = 0
31
-
32
- client.logger.info("Starting file upload...")
33
-
34
- try:
35
- await client.send_message(
36
- chat_id=chat_id,
37
- text="📎 Вот большой файл",
38
- attachment=file,
39
- )
40
- client.logger.info("File uploaded successfully!")
41
-
42
- except OSError as e:
43
- if "malloc failure" in str(e):
44
- client.logger.error("Memory error - file too large for current memory")
45
- client.logger.info("Recommendation: Upload smaller files or free up memory")
46
- else:
47
- raise
48
-
49
-
50
- if __name__ == "__main__":
51
- asyncio.run(client.start())
@@ -1,34 +0,0 @@
1
- import asyncio
2
-
3
- from pymax import MaxClient, Message
4
- from pymax.filters import Filters
5
-
6
- client = MaxClient(
7
- phone="+1234567890",
8
- work_dir="cache",
9
- )
10
-
11
-
12
- @client.on_message(Filters.chat(0))
13
- async def on_message(msg: Message):
14
- print(f"[{msg.sender}] {msg.text}")
15
- await client.send_message(chat_id=msg.chat_id, text="Привет!")
16
- await client.add_reaction(
17
- chat_id=msg.chat_id, message_id=str(msg.id), reaction="👍"
18
- )
19
-
20
-
21
- @client.on_start
22
- async def on_start():
23
- print(f"Клиент запущен. Ваш ID: {client.me.id}")
24
- history = await client.fetch_history(chat_id=0)
25
- for m in history:
26
- print(f"- {m.text}")
27
-
28
-
29
- async def main():
30
- await client.start()
31
-
32
-
33
- if __name__ == "__main__":
34
- asyncio.run(main())
@@ -1,20 +0,0 @@
1
- import asyncio
2
-
3
- from pymax import MaxClient
4
- from pymax.payloads import UserAgentPayload
5
-
6
- ua = UserAgentPayload(device_type="DESKTOP", app_version="25.12.13")
7
-
8
- client = MaxClient(
9
- phone="+79116290861",
10
- work_dir="cache",
11
- headers=ua,
12
- )
13
-
14
-
15
- @client.on_start
16
- async def on_start() -> None:
17
- print(f"MaxClient started as {client.me.names[0].first_name}!")
18
-
19
-
20
- asyncio.run(client.start())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes