maxapi-python 1.2.2__py3-none-any.whl → 1.2.3__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.
@@ -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
@@ -1,32 +1,32 @@
1
1
  pymax/__init__.py,sha256=6wUKKwsyxFpWG3b7kwptOvHd-w78C-ygw42iCDBYQvc,1915
2
- pymax/core.py,sha256=OXGNaQ0pDaf6Ofr1Fb9m7vh5ffpbiMyvUMM0EfwlnIQ,14907
2
+ pymax/core.py,sha256=S5el07cMcqIyXOI25G0a2XtVJSz2_AltmKN7ZW6LEeg,14946
3
3
  pymax/crud.py,sha256=YC92TyhA2mr1tJCcfd-tvh8umtXKgqJfgiLo7nXUl3Q,3076
4
4
  pymax/exceptions.py,sha256=nDUNx7bM-Yjugj-qfIllcrnwLg9JpZroYqfXapjYbMQ,3178
5
- pymax/files.py,sha256=AvFIr34Desq2p4CNWXIngRqeyTBKMT98VmcnI-zvUU0,3462
5
+ pymax/files.py,sha256=bSjP1m-dLPJCN8HPZtjrLLp0Oc7OxdGxI6U1K2rQzUA,3587
6
6
  pymax/filters.py,sha256=gSHPJ1Vi37HKPxf0jRRv9Q3iGwhiQjw1MGrCaouqHzs,4325
7
7
  pymax/formatter.py,sha256=RJ_5VbY7Li8UM3xL1AvcXo8v1iYnY8GvDDkreaFqtnY,860
8
8
  pymax/formatting.py,sha256=XRtuXJGweuNZevJFdPxksDftIrfuMGEA-AOUc_v6IhQ,2484
9
- pymax/interfaces.py,sha256=wKF1z1QRw8LcjvM9rzSHWXTK6gPb6sDt2UGiQLvyMf8,8790
9
+ pymax/interfaces.py,sha256=5-0RM0tH3eBJrVZkjL494lKTTwvIVmOTJiQxDWZo-7A,8829
10
10
  pymax/models.py,sha256=PsPGbOkERxesZZltjNrmqhOfRcO44Is2ThbEToREcB8,201
11
11
  pymax/navigation.py,sha256=4ia6RGY2pXMArboNhHkbWlWX7LtcYK1VGVXorPX0Pb4,5747
12
- pymax/payloads.py,sha256=GuTLK6HYe_bLW3ardgpKeZ98f79j349tD_6B6EwkGww,7879
13
- pymax/types.py,sha256=_ARcVXLGHyiGAJKYPd6EU9QDKzz4VwS6kjTu3YEH_u4,35523
12
+ pymax/payloads.py,sha256=tlqPT-JFexfklp9idSrbpU3bALcQuiDne9H9vTnwL4w,8022
13
+ pymax/types.py,sha256=_Ee1aycVwe4nfBO_hGMnpm9lQDGzkPOkT9vME8OL-sA,36650
14
14
  pymax/mixins/__init__.py,sha256=5sXJME34S1EssuDETaN4DLRH7vhMw_Q3Jmay9myAIZM,775
15
15
  pymax/mixins/auth.py,sha256=e90vIpEOwAjUxgYMYaG7R6jR_5t9rKsei_mTBQUirL4,14716
16
16
  pymax/mixins/channel.py,sha256=W52YnBay1sUYXxF9oAWsz44ZUh_s45jSvKmAyjTbULM,5357
17
- pymax/mixins/group.py,sha256=LqI1QHmZlmtuQ0-4H1MrNeBV-O9SMDMfHT9f4B_2poE,15189
17
+ pymax/mixins/group.py,sha256=MaiZCQ9R-wWbuEPXyqf4JZAGNmxu_tXCs8qU3JEoYek,15911
18
18
  pymax/mixins/handler.py,sha256=ETnI8fA386LYJGjWtUhhWzQHREUA78di1aO1oWwtscA,12523
19
19
  pymax/mixins/message.py,sha256=AznKKmTMxdzsYl8IecT43RjWpGvlQM85GzSNGFbI8BA,33279
20
20
  pymax/mixins/scheduler.py,sha256=rcMfgfZnzu5V6MkcCg6uRgbi-jkc7UyqOjemulydWbc,964
21
- pymax/mixins/self.py,sha256=Be5L64eNYylGM-NmoxFpQZv1ohsC1Dx_Cs3Om__V96s,6976
22
- pymax/mixins/socket.py,sha256=tdHgd1NwWoEZhHCDd74XLOHFKUq-rladxhXV8Z_-APU,22860
21
+ pymax/mixins/self.py,sha256=27ranVw5rdv1qSplhV5mdUvHc4qSxHi63f1gWOjR9B4,9182
22
+ pymax/mixins/socket.py,sha256=tk2puQPsT2Dltg1j6kUSHIbXs64p6q-jiIIcs73Y1Sk,23174
23
23
  pymax/mixins/telemetry.py,sha256=LWr68DNQkPhAjGRDYQ5lORXxC3Yw6M9E8sF0TCNISTE,3609
24
24
  pymax/mixins/user.py,sha256=RSZd4t-aq8P2k3cVzNVWBkUf-_xTWILrBzwxLRgk1pw,9450
25
25
  pymax/mixins/utils.py,sha256=s3FUf3i_wjn2Gbg5YY1rWZB-90ZEGrrcUuND_MqqSTE,853
26
- pymax/mixins/websocket.py,sha256=GpdboEVWzyN1qLTcsgKZym6TlPnklcQuNeXJ5YKwg8c,17724
26
+ pymax/mixins/websocket.py,sha256=fL8IcsLkvksizSAg98UaFlDPZGyGbYtbtdHnlDzfR9g,18056
27
27
  pymax/static/constant.py,sha256=nM0svv3VpsVxK-RqoADn9qsTdQvB-IYv0Sgv-bQcWs4,1116
28
- pymax/static/enum.py,sha256=Hk0e6zSbGOJC_9Aw7gNXX3hcavnjzQfDyr8vjW22cFo,4648
29
- maxapi_python-1.2.2.dist-info/METADATA,sha256=rgiQKdSqYAO743n6jWOy0F76jZyjaGMY7A6qUlHlk64,6753
30
- maxapi_python-1.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
- maxapi_python-1.2.2.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
32
- maxapi_python-1.2.2.dist-info/RECORD,,
28
+ pymax/static/enum.py,sha256=mraN6FK_gcVXdTHBdK8Zf6-92AzVFGAWRnQLA4rzYBA,4672
29
+ maxapi_python-1.2.3.dist-info/METADATA,sha256=0NSb43LEMf9-M1KLeETofzdcjsGJ68lpZ15xP9GouI0,6753
30
+ maxapi_python-1.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ maxapi_python-1.2.3.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
32
+ maxapi_python-1.2.3.dist-info/RECORD,,
pymax/core.py CHANGED
@@ -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
pymax/files.py CHANGED
@@ -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:
pymax/interfaces.py CHANGED
@@ -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
pymax/mixins/group.py CHANGED
@@ -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
 
pymax/mixins/self.py CHANGED
@@ -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(
pymax/mixins/socket.py CHANGED
@@ -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(
pymax/mixins/websocket.py CHANGED
@@ -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
 
pymax/payloads.py CHANGED
@@ -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):
pymax/static/enum.py CHANGED
@@ -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):
pymax/types.py CHANGED
@@ -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: