maxapi-python 2.2.0__py3-none-any.whl → 2.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maxapi-python
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Python wrapper для API мессенджера Max
5
5
  Project-URL: Homepage, https://github.com/MaxApiTeam/PyMax
6
6
  Project-URL: Repository, https://github.com/MaxApiTeam/PyMax
@@ -1,6 +1,6 @@
1
- pymax/__init__.py,sha256=57UYiquQpEkanZR7Y4wQTzYnfd9KyvZTsNxDKb6dM9g,1500
2
- pymax/app.py,sha256=3OwH2pT4Wv-ocAhEFZNo2DleZRnC8VNUmQQprRGAG58,10265
3
- pymax/base.py,sha256=23kcR8rFjxfAqDJhOaxyOpS-TQrOSpLQlldzmx21sD0,8679
1
+ pymax/__init__.py,sha256=xPT-McHyAWLyFFEbepaDbmT65hBkHiQUPD3u-jOdFgY,1500
2
+ pymax/app.py,sha256=QQldPh1YYpFeKBOMfCQZA0BFW5bUQQO7hFBYtwpioZQ,10847
3
+ pymax/base.py,sha256=--6VwKUvqlocFAUbPWn_CfXQ-8CYfyCigXQ70ipXOgI,10500
4
4
  pymax/client.py,sha256=ePDxR6wEeHJbk23wI_PkOVFQuyy5tQyckV-OmKEwz5U,3985
5
5
  pymax/client_web.py,sha256=VASqNmMvAfdpIaR6cpdtjv3UN4GoqWkiDOXdn7QmZQ0,3025
6
6
  pymax/config.py,sha256=1dttAfxjcuWVrr939iYLDowQknvl_eOhNpJP8ugnWTg,10047
@@ -23,12 +23,12 @@ pymax/api/bots/payloads.py,sha256=aXriolCN_YvBvDZO_MPmD-fkrIVJIpVDXkOkOZVes_c,16
23
23
  pymax/api/bots/service.py,sha256=ziqVlQUMFHjQydMDFyWcQMIATboQTvIdGtJhf-AK9tA,847
24
24
  pymax/api/chats/__init__.py,sha256=UMp3uKdB3tZlnWckndgF3jaU-3-49OJE18z2d0OlmIU,155
25
25
  pymax/api/chats/enums.py,sha256=FdByRiHYX9Xs7RyNabKqxGugNs9tJ3VBXwTb_43XGC8,627
26
- pymax/api/chats/payloads.py,sha256=Y4S2x60i3txeA75GV_pGJ3ov7pLMp4GjGuhoqlRI2ow,2824
27
- pymax/api/chats/service.py,sha256=l6yReFxIj9Pfn96wOW3n60MRHRp4rq9nM569fs2gfvU,11535
26
+ pymax/api/chats/payloads.py,sha256=qA46Q984FgHaXenPmvtotm7ZJOKM07aiP7dBYF_0gqQ,2930
27
+ pymax/api/chats/service.py,sha256=jNQUlSdILR5afeTYFQmQ6mvx-rgQlWjhzmOWUA0vDcg,12064
28
28
  pymax/api/messages/__init__.py,sha256=fw_uF6tgSbkxvqaSVE65wU0KWsu47u1CtR0Y4BbD-K0,36
29
29
  pymax/api/messages/enums.py,sha256=tp5S2Q97a-FILcqpRhQiIRKdj0P_KSEBiCqWmb05dHs,368
30
30
  pymax/api/messages/payloads.py,sha256=wqseGNfqWYKeoRJhdzrRV855IboChaYU1dJLPGZg71I,2257
31
- pymax/api/messages/service.py,sha256=Zur05Rklq2LrssIWLE5tThAXPXK9b7MsvFW8BklSoHU,12422
31
+ pymax/api/messages/service.py,sha256=61reqCHswh7QiP3KvofEeo_96CylozWaoPaqik2iDSI,12117
32
32
  pymax/api/self/__init__.py,sha256=TfbqL4xLb5IMhbW8mlAK-AwVFqqPWMADogKZeWtkw0A,79
33
33
  pymax/api/self/enums.py,sha256=iKEqPy44LyQKvAPfyhpkSXgmWWohxry73F0CVgtekwY,180
34
34
  pymax/api/self/payloads.py,sha256=-SFqkNxC5ZLKnLty_Gyz_b_P3X1esu_7Urb4HjrQxM4,774
@@ -43,8 +43,8 @@ pymax/api/uploads/payloads.py,sha256=OqsfFJIRe2io7mltHPbDPORT8U5t27H6pvqrdNKPRHY
43
43
  pymax/api/uploads/service.py,sha256=83VrmMGZG4QPnKfSdVrWoIOvER4CvegrbZ2ddxmGsq8,18551
44
44
  pymax/api/users/__init__.py,sha256=CDakgSKwVAkX-kNoDmIo2JsFPsS7Nx3j2vIMwQnPRck,82
45
45
  pymax/api/users/enums.py,sha256=m3d225sjWkBIRF5NNi3ChnyZ6BgKUEbPYoYwVjpifZ4,205
46
- pymax/api/users/payloads.py,sha256=VoXXctQDl7WXt5zqoRKzdONqmris5Cxf7-6Hy_Gcd-A,288
47
- pymax/api/users/service.py,sha256=q-juReMH8Fz5a4eOEvxkpIknRHiKrlowRSByeB9nrkQ,4144
46
+ pymax/api/users/payloads.py,sha256=mWrbhu3KzrsxmBdRQU3bG8W46vtFq_Kdfb2YA7_QkNI,876
47
+ pymax/api/users/service.py,sha256=ndYv60kKkqZPnFOgvfau2LAjgOf-NsLQ7P4xxoWBAr4,4663
48
48
  pymax/auth/__init__.py,sha256=KgghKg0CPcJzX_D14TvxuDzhNmoVoFGjv414NouJVgk,512
49
49
  pymax/auth/base.py,sha256=v492FMUmSKft6oJRmCdIr33sy50Z0kqBwu5t7cVLQe4,1222
50
50
  pymax/auth/email.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -60,12 +60,12 @@ pymax/connection/readers/__init__.py,sha256=9g5ZbcWiHxW1kAi2ckiNqejoGHwoDwFd4Wvt
60
60
  pymax/connection/readers/base.py,sha256=FlvyMUFoZ4FORAh0vBU6_I0BnrQ9_1twpUIKz-Vkj58,126
61
61
  pymax/connection/readers/tcp.py,sha256=62nqlllE0gjQQQYVfjBt0arAJ8tEHH_SMZOOjtXNYdw,1073
62
62
  pymax/connection/readers/ws.py,sha256=IjeH5CoXvBWde46tZLx6uKONkjhCU6NeC79x3Swfw8E,368
63
- pymax/dispatch/__init__.py,sha256=bvHjlw2ltujMAiegW-Sv2fSGKUnkmtPcYINUZ-Ty8qo,189
64
- pymax/dispatch/dispatcher.py,sha256=OPEyFKLYfUs2xRjjNH_EvMCMJYLhJaPu5szZwEFATgk,7865
65
- pymax/dispatch/enums.py,sha256=_gPWDdBpCCgkfRcAqz-zIMptavV7SRxArpRtW3MudPA,420
63
+ pymax/dispatch/__init__.py,sha256=Big4ZqRSP4fzNAKc5mISkqQbWcYhjYMfg7UtHHCGZJw,376
64
+ pymax/dispatch/dispatcher.py,sha256=xk_8ZbJRoRUOTPg5idlZPCAAZ_kOBbrrcT2LuI57SdE,11782
65
+ pymax/dispatch/enums.py,sha256=LxX6tCxkB7mNnHr3AydKX-s-iZ8_bjoMl8IxhC7gdhQ,446
66
66
  pymax/dispatch/mapping.py,sha256=wO9BPl3meCWE7AA0XdC0iGt-4zuGVozcCqnleCTFdI0,3303
67
67
  pymax/dispatch/resolvers.py,sha256=FZvjYxQwy5CnTl6QwdfRRodK9cGrAn6MuXTZqcsJkPE,1987
68
- pymax/dispatch/router.py,sha256=thwbOgulhgFpgdP76r5ZAttGNLo3FKaXIGNWQmCrWSg,7967
68
+ pymax/dispatch/router.py,sha256=0dwXCR7TgAGNiq9oryXIuyAsJ15DJ3OMt9hrRenUBio,10453
69
69
  pymax/files/__init__.py,sha256=TR0N6YXX4IHBsXQo8uiK68C-Xg3LSj48d4Zsv71CWZE,126
70
70
  pymax/files/base.py,sha256=0aksCGxmrvqEKJdhRWcuPZJfEn7RWcl5tn5gn63q1os,2689
71
71
  pymax/files/file.py,sha256=JSkuKx0fdGxzcsyMajWTosFzTlfgZYvQbh-RDp9OZyQ,2337
@@ -78,11 +78,11 @@ pymax/infra/__init__.py,sha256=lKq6cehhB87HltV53TD0kr0p7ekWlZlrKTpaYiq0XFw,28
78
78
  pymax/infra/auth.py,sha256=YuYH_NWNz8UfPyko6bv_cIzXDIvFeivDrxZCIpGoANU,3648
79
79
  pymax/infra/base.py,sha256=sOo40Qkp14bE6CxVKppJhy3e0F5VhFPhnL3iBs8y5dA,368
80
80
  pymax/infra/bots.py,sha256=IJs4ErFxjbMeiHfQgpDiDhtEu9ws-lIp8fb8I96szy8,1202
81
- pymax/infra/chat.py,sha256=tMyzCEhOWmGqMXRLmV0_jPrKQXyowkHxzUwyVEdlztc,13134
82
- pymax/infra/message.py,sha256=pZga2B_PUoQMGhJuGRbtvU7XUpOeibjDvFPbJptn1Fk,10886
81
+ pymax/infra/chat.py,sha256=Jb5kzUnHtCg98glS4Jf-WEHIzZg4oIcS-MA8aGeJuKk,13884
82
+ pymax/infra/message.py,sha256=RW5LwaLDwekKNC_8s-12TM1PbCmXyFD_l5muAvX_rLg,10701
83
83
  pymax/infra/protocol.py,sha256=I2WrAeAgATuNSdK2gvHU-MhhxdRBfSMGMovF5HPFsQw,208
84
84
  pymax/infra/self.py,sha256=h2GnzcCF2-FeY9-qBjtHjrSCx7elc4RjuCXPaewDkgg,4828
85
- pymax/infra/user.py,sha256=nv9jkWpWvCWETHZsyUMmm5FWkzLTJT2SHXZ7SglZE8M,4331
85
+ pymax/infra/user.py,sha256=wy6lElmJx7Ypjkk48kDrlLiL3V3p7pzO2MevsgCNy1k,4801
86
86
  pymax/protocol/__init__.py,sha256=rDn1Twi1iBDUk1155YI9jyeQyaGVDRuPoWdlaaLZnsw,242
87
87
  pymax/protocol/base.py,sha256=_bisk1BU_GSMSPIqfnizSCgm_qBrdbB5cJw6foAa2tc,294
88
88
  pymax/protocol/enums.py,sha256=9y4kn9y2pKinaT_twdeW5MXVH3O-sJF6h0v5KbC9VkA,4376
@@ -97,7 +97,7 @@ pymax/protocol/ws/protocol.py,sha256=z6zzBQTlJfzwOmLsJYoYQ2L1AYpY5wYMJyPir_zfNBQ
97
97
  pymax/session/__init__.py,sha256=natjnLjPjTvkfvSYtoiPxzmU44XJi0MpjWiOPD-On4g,100
98
98
  pymax/session/models.py,sha256=jvnTHfjCX1p5TcANfWZ0vUJlciuqumatwdrgYiKm4kY,250
99
99
  pymax/session/protocol.py,sha256=8ibp-HiaJaGmx23v7GeL0GFsvLPZZqWOuIOJnanqoCM,612
100
- pymax/session/store.py,sha256=FkhrG0J2ueU2Z4PxJQxFPOcunKnpVavnAaJgPb2bx-E,7770
100
+ pymax/session/store.py,sha256=xzUpLuS8OpcYNB1lNvUwzOJIcsGz516_aOXBh2jIWT4,8087
101
101
  pymax/telemetry/__init__.py,sha256=6mP5y-iTbVIlqpSWIKWCTz2fFNBwkAEYwS4PFbyv9xM,71
102
102
  pymax/telemetry/navigation.py,sha256=vlP7d863s9aIBpt9SPpjEbTXLoq3uqpuhD-VWj0w9hI,5441
103
103
  pymax/telemetry/payloads.py,sha256=MCKYGpQ4Tvx_8LH86Et0yevea2Th2z3b7a9pfmrQRys,3959
@@ -107,24 +107,24 @@ pymax/transport/base.py,sha256=0D3srQ36F0s23pIjqd6YzRGKK9wCmXKr3wfe2XYYlos,448
107
107
  pymax/transport/tcp.py,sha256=Nl-voifBze4TGB25xjf93XxU77zvFL8rsPCjBpXYd_4,3083
108
108
  pymax/transport/websocket.py,sha256=0ezkSoENWKV00EATIizGx16-ihCnaTC76zzGnwbS_3c,1504
109
109
  pymax/types/__init__.py,sha256=y77mfDRgxmfcamobi0rj2d3PsxhH0zxyvKPhL2iUdS8,44
110
- pymax/types/domain/__init__.py,sha256=R4iJv0b5gnNVG3MrerVlGVzPvNR-n36QZFukEX64fn4,470
110
+ pymax/types/domain/__init__.py,sha256=tdC1Vs4lH-oqDrSXS_Oc5JlnJC7RTJ2I3B63EVfiLiI,483
111
111
  pymax/types/domain/auth.py,sha256=RmEXFS7TAgtFhnfXaiJH5XDb__2MUVgQ_Noavo9hFRY,6019
112
112
  pymax/types/domain/base.py,sha256=Q32PcKR3gtrISySowR3zPyl9KeQCoUL6lfyTR3ulMzo,623
113
113
  pymax/types/domain/bots.py,sha256=dErZZbvspznU6yUupzCs4R55pEWgoeps7NEhZFrb-Uo,333
114
- pymax/types/domain/chat.py,sha256=QYuE0jN7wKpjJm3BfDY8KHO_S-4XPN5FRQrtWEZjmkk,19781
114
+ pymax/types/domain/chat.py,sha256=mkee157TESCqxC_1NXveHLFLxtLOT7up22IvBnN__2o,20547
115
115
  pymax/types/domain/element.py,sha256=Hks0MulMcPAhbpuUmJa5c2d9Emwy3garh1k6bd4QHKA,655
116
116
  pymax/types/domain/enums.py,sha256=eGMWnlv0mYJ8K-0FsC_wp4Co2M6YWyKI1Lx90MJ6s4g,412
117
117
  pymax/types/domain/error.py,sha256=WZoDAXf5vt8O4VYy7iEHLGBfH6hZQZ1fVkTz9rVCTN0,584
118
118
  pymax/types/domain/folder.py,sha256=Z9pXMzOTfxuhUfrJpLjl5LPnVXqqFsBVfKBQm4IbGWM,2225
119
119
  pymax/types/domain/login.py,sha256=FBKpRhNPv6Zdjo6Wfl-lK8JpzpMx7zh84f1qrq6Z0og,1301
120
120
  pymax/types/domain/member.py,sha256=8gUNn4fBS_S2ap45A40lRlrDXYFMoq0Kx-dh57hMWQc,490
121
- pymax/types/domain/message.py,sha256=UF88nXBTubAKKQKP4qR_qtHOMS8uRdXCCTm8jtsu0wA,15678
121
+ pymax/types/domain/message.py,sha256=2klOqUolng2NUjO3zxhmv-Mq55X6EhoBV3NO5oywkEE,15503
122
122
  pymax/types/domain/name.py,sha256=qMIshIqgWkVuROxmiCRhwfJf8XtmhSBoEU6nXs_gEh0,523
123
123
  pymax/types/domain/presence.py,sha256=lKvkK0uYwY0gTSEEkaVdI-tEpWjxLOtH7gp2dy5Uh-0,534
124
124
  pymax/types/domain/profile.py,sha256=taN5PjlKp4EDypVhDx89vmBDpSKh-kT-ISpu1tR2cY8,471
125
125
  pymax/types/domain/session.py,sha256=T0b0qjI65kNQOCgx3nIkaIqynD4eGKyqnscbIlPFVnQ,2002
126
126
  pymax/types/domain/sync.py,sha256=lBDWo4ACLq2ov5XBccfnI8s_dsGEA7O4mIJVBSUpyrc,3248
127
- pymax/types/domain/user.py,sha256=NjJfQtkRBrsmB-JGg2nsXWLvgqu3q5YKx2BALkoDrOI,5120
127
+ pymax/types/domain/user.py,sha256=dh9NNPQuvIT74W-2URkFfylSWmo8VkfdHsO4_AqDzsQ,5337
128
128
  pymax/types/domain/attachments/__init__.py,sha256=ZVSb7sSAwabSB8A8BWF_JbooPOMIP1r6gF_NUASgGGk,471
129
129
  pymax/types/domain/attachments/audio.py,sha256=I_Qq1qQ9bXsEzWwxEbQbkiY8CEjSKqGqinDFYY_5DY8,1117
130
130
  pymax/types/domain/attachments/call.py,sha256=RV-BFutymZFinzUiRdaIryqx6ubku2v6y6brc-qsHOQ,818
@@ -147,7 +147,7 @@ pymax/types/events/presence.py,sha256=77Cy352CdfIkbH6ZfLijHffF8gcMz-AZk5OCeESQeD
147
147
  pymax/types/events/reaction.py,sha256=7kauScAH8O-K79xS6Q3b6Vi0KwRl4lGCKGeAMAH9sEU,672
148
148
  pymax/types/events/typing.py,sha256=vDMgqYE0jz8dN2YfqsNaJLB8k7pMd6kaDEdR_KrLk5g,376
149
149
  pymax/types/events/video.py,sha256=swBHYadmDS0SjLXGqVqRYsuMoVZ6MjD2aYR2fbM-AJc,104
150
- maxapi_python-2.2.0.dist-info/METADATA,sha256=TU_EpqMK4HYj7DC-m9UbjhUeDSq2n0WsomqomY5wbD4,7276
151
- maxapi_python-2.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
152
- maxapi_python-2.2.0.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
153
- maxapi_python-2.2.0.dist-info/RECORD,,
150
+ maxapi_python-2.3.0.dist-info/METADATA,sha256=2OJj5PZgaKs9M_5UOLdtC0z9MdVWbASx5jx5XWMB5Kw,7276
151
+ maxapi_python-2.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
152
+ maxapi_python-2.3.0.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
153
+ maxapi_python-2.3.0.dist-info/RECORD,,
pymax/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.0"
1
+ __version__ = "2.3.0"
2
2
 
3
3
 
4
4
  from .auth import (
@@ -115,3 +115,9 @@ class JoinRequestActionPayload(CamelModel):
115
115
  type: str = "JOIN_REQUEST" # TODO: ENUMM!!!
116
116
  show_history: bool | None = True
117
117
  operation: ChatMemberOperation
118
+
119
+
120
+ class DeleteChatPayload(CamelModel):
121
+ chat_id: int
122
+ last_event_time: int
123
+ for_all: bool = True
@@ -23,6 +23,7 @@ from .payloads import (
23
23
  CreateGroupAttach,
24
24
  CreateGroupMessage,
25
25
  CreateGroupPayload,
26
+ DeleteChatPayload,
26
27
  FetchChatsPayload,
27
28
  FetchJoinRequests,
28
29
  GetChatInfoPayload,
@@ -362,3 +363,20 @@ class ChatService:
362
363
  chat_id=chat_id,
363
364
  user_ids=[user_id],
364
365
  )
366
+
367
+ async def delete_chat(
368
+ self,
369
+ chat_id: int,
370
+ last_event_time: int | None = None,
371
+ for_all: bool = True,
372
+ ) -> None:
373
+ frame = DeleteChatPayload(
374
+ chat_id=chat_id,
375
+ last_event_time=(
376
+ last_event_time if last_event_time is not None else int(time.time() * 1000)
377
+ ),
378
+ for_all=for_all,
379
+ )
380
+
381
+ await self.app.invoke(Opcode.CHAT_DELETE, frame.to_payload())
382
+ self._remove_cached_chat(chat_id)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
+ from collections.abc import Sequence
4
5
  from typing import TYPE_CHECKING, TypeAlias
5
6
 
6
7
  from pymax.api.binding import bind_api_model, bind_api_models
@@ -52,7 +53,7 @@ if TYPE_CHECKING:
52
53
  from pymax.app import App
53
54
 
54
55
  SendAttachment: TypeAlias = Photo | File | Video
55
- SendAttachments: TypeAlias = list[SendAttachment] | None
56
+ SendAttachments: TypeAlias = Sequence[SendAttachment] | None
56
57
 
57
58
  logger = get_logger(__name__)
58
59
 
@@ -169,24 +170,15 @@ class MessageService:
169
170
  chat_id: int,
170
171
  message_id: int,
171
172
  text: str,
172
- attachment: SendAttachment | None = None,
173
173
  attachments: SendAttachments = None,
174
174
  ) -> Message:
175
- if attachment is not None and attachments:
176
- logger.warning("both attachment and attachments provided; using attachments")
177
- attachment = None
178
-
179
- edit_attachments = attachments
180
- if attachment is not None:
181
- edit_attachments = [attachment]
182
-
183
175
  clean_text, elements = Formatter.format_markdown(text)
184
176
  frame = EditMessagePayload(
185
177
  chat_id=chat_id,
186
178
  message_id=message_id,
187
179
  text=clean_text,
188
180
  elements=elements,
189
- attachments=await self._upload_attachments(edit_attachments),
181
+ attachments=await self._upload_attachments(attachments),
190
182
  )
191
183
 
192
184
  response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
@@ -1,4 +1,7 @@
1
+ from collections.abc import Iterable
2
+
1
3
  from pymax.api.models import CamelModel
4
+ from pymax.types.domain import ContactInfo
2
5
 
3
6
  from .enums import ContactAction
4
7
 
@@ -14,3 +17,22 @@ class SearchByPhonePayload(CamelModel):
14
17
  class ContactActionPayload(CamelModel):
15
18
  contact_id: int
16
19
  action: ContactAction
20
+
21
+
22
+ class _ContactPayload(CamelModel):
23
+ first_name: str
24
+
25
+
26
+ class ImportContactsPayload(CamelModel):
27
+ contact_list: dict[str, _ContactPayload] # phone -> contact payload
28
+
29
+ @classmethod
30
+ def from_contacts(cls, contacts: Iterable[ContactInfo]) -> "ImportContactsPayload":
31
+ return cls(
32
+ contact_list={
33
+ contact.phone: _ContactPayload(
34
+ first_name=contact.first_name,
35
+ )
36
+ for contact in contacts
37
+ }
38
+ )
@@ -10,12 +10,13 @@ from pymax.api.response import (
10
10
  )
11
11
  from pymax.logging import get_logger
12
12
  from pymax.protocol import InboundFrame, Opcode
13
- from pymax.types.domain import Session, User
13
+ from pymax.types.domain import ContactInfo, Session, User
14
14
 
15
15
  from .enums import ContactAction, UserPayloadKey
16
16
  from .payloads import (
17
17
  ContactActionPayload,
18
18
  FetchContactsPayload,
19
+ ImportContactsPayload,
19
20
  SearchByPhonePayload,
20
21
  )
21
22
 
@@ -122,5 +123,17 @@ class UserService:
122
123
  self.app.users.pop(contact_id, None)
123
124
  return True
124
125
 
126
+ async def import_contacts(self, contacts: list[ContactInfo]) -> list[User]:
127
+ frame = ImportContactsPayload.from_contacts(contacts)
128
+
129
+ response = await self.app.invoke(Opcode.SYNC, frame.to_payload())
130
+
131
+ users = parse_payload_list(
132
+ response, UserPayloadKey.CONTACTS, User
133
+ ) # TODO: maybe also return phone mapping?
134
+
135
+ # {contacts: [...], phones: {data[0]: server_phone}}
136
+ return [self._cache_user(user) for user in users]
137
+
125
138
  def get_chat_id(self, first_user_id: int, second_user_id: int) -> int:
126
139
  return first_user_id ^ second_user_id
pymax/app.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
- from typing import Any, Generic, TypeVar
2
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
3
3
 
4
4
  from pymax.api import ApiFacade
5
5
  from pymax.auth import AuthFlow
6
6
  from pymax.config import ClientConfig
7
7
  from pymax.connection import ConnectionManager
8
8
  from pymax.dispatch import Dispatcher
9
- from pymax.dispatch.router import Router
9
+ from pymax.dispatch.router import EventType, Router
10
10
  from pymax.exceptions import ApiError
11
11
  from pymax.logging import get_logger
12
12
  from pymax.protocol import Command, InboundFrame, OutboundFrame
@@ -17,8 +17,11 @@ from pymax.telemetry import TelemetryService
17
17
  from pymax.types import MaxApiError, Message
18
18
  from pymax.types.domain import Chat, Profile, User
19
19
 
20
+ if TYPE_CHECKING:
21
+ from pymax.base import BaseClient
22
+
20
23
  logger = get_logger(__name__)
21
- ClientT = TypeVar("ClientT")
24
+ ClientT = TypeVar("ClientT", bound="BaseClient")
22
25
 
23
26
 
24
27
  class App(Generic[ClientT]):
@@ -124,9 +127,26 @@ class App(Generic[ClientT]):
124
127
  self.session = session_data
125
128
 
126
129
  logger.debug("logging in")
127
- response = await self.api.auth.login(
128
- self.config.device.user_agent,
129
- )
130
+
131
+ try:
132
+ response = await self.api.auth.login(
133
+ self.config.device.user_agent,
134
+ )
135
+ except Exception as e:
136
+ handled = False
137
+ if self.dispatcher.client is not None:
138
+ handled = await self.dispatcher.emit_error(
139
+ e,
140
+ EventType.ON_START,
141
+ None,
142
+ self.dispatcher.root_router,
143
+ None,
144
+ )
145
+ if not handled:
146
+ raise
147
+
148
+ await self.close()
149
+ return
130
150
 
131
151
  if response.token is not None and response.token != self.session.token:
132
152
  await self.store.update_token(self.session.token, response.token)
pymax/base.py CHANGED
@@ -5,7 +5,8 @@ from abc import ABC, abstractmethod
5
5
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
6
  from uuid import uuid4
7
7
 
8
- from pymax.dispatch import Router
8
+ from pymax.dispatch import ErrorScope, Router
9
+ from pymax.dispatch.router import DisconnectDecorator, ErrorDecorator
9
10
  from pymax.infra import BaseMixin
10
11
  from pymax.logging import get_logger
11
12
 
@@ -128,6 +129,10 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
128
129
  while True:
129
130
  try:
130
131
  await self._app.start()
132
+ if not self._app.started:
133
+ await self.close()
134
+ return
135
+
131
136
  await self._app.dispatcher.emit_start(self)
132
137
  await self._connection.wait_closed()
133
138
  except asyncio.CancelledError:
@@ -138,14 +143,21 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
138
143
  EOFError,
139
144
  OSError,
140
145
  TimeoutError,
141
- ):
146
+ ) as e:
142
147
  await self.close()
148
+ await self._app.dispatcher.emit_disconnect(
149
+ e,
150
+ self.extra_config.reconnect,
151
+ self.extra_config.reconnect_delay,
152
+ )
153
+
143
154
  if not self.extra_config.reconnect:
144
155
  raise
145
156
 
146
- logger.exception(
157
+ logger.debug(
147
158
  "client connection failed; reconnecting in %s seconds",
148
159
  self.extra_config.reconnect_delay,
160
+ exc_info=True,
149
161
  )
150
162
  await asyncio.sleep(self.extra_config.reconnect_delay)
151
163
  self._reset_runtime()
@@ -237,6 +249,39 @@ class BaseClient(BaseMixin, ABC, Generic[ClientT]):
237
249
  """Регистрирует обработчик исходных входящих frame-ов."""
238
250
  return self._router.on_raw(*filters)
239
251
 
252
+ def on_error(self, scope: ErrorScope = ErrorScope.GLOBAL) -> ErrorDecorator[ClientT]:
253
+ """Регистрирует обработчик ошибок dispatch-а и запуска клиента."""
254
+ return self._router.on_error(scope)
255
+
256
+ def on_disconnect(self) -> DisconnectDecorator:
257
+ """Регистрирует обработчик сетевого отключения перед reconnect."""
258
+ return self._router.on_disconnect()
259
+
240
260
  def include_router(self, router: Router[ClientT]) -> None:
241
261
  """Подключает дочерний router к root router клиента."""
242
262
  self._router.include_router(router)
263
+
264
+ async def relogin(self: ClientT, drop_config_token: bool = True, start: bool = True) -> None: # noqa: PYI019
265
+ """Удаляет текущую локальную сессию и запускает авторизацию заново.
266
+
267
+ Args:
268
+ drop_config_token: Сбросить token, переданный через ``ExtraConfig``.
269
+ start: Сразу запустить клиента после сброса runtime.
270
+ """
271
+ store = self._app.store
272
+ session = self._app.session
273
+
274
+ if session is None:
275
+ raise RuntimeError("Cannot relogin before session is loaded")
276
+
277
+ await store.delete_session(session.token)
278
+ await self.close()
279
+
280
+ if drop_config_token:
281
+ self.extra_config.token = None
282
+ self._config.token = None
283
+
284
+ self._reset_runtime()
285
+
286
+ if start:
287
+ await self.start()
@@ -1,10 +1,21 @@
1
1
  from .dispatcher import Dispatcher
2
2
  from .enums import EventType
3
- from .router import ClientRouter, Router
3
+ from .router import (
4
+ ClientRouter,
5
+ DisconnectCallback,
6
+ DisconnectDecorator,
7
+ ErrorContext,
8
+ ErrorScope,
9
+ Router,
10
+ )
4
11
 
5
12
  __all__ = (
6
13
  "ClientRouter",
14
+ "DisconnectCallback",
15
+ "DisconnectDecorator",
7
16
  "Dispatcher",
17
+ "ErrorContext",
18
+ "ErrorScope",
8
19
  "EventType",
9
20
  "Router",
10
21
  )
@@ -19,11 +19,19 @@ from pymax.types.events import (
19
19
  from .enums import EventType
20
20
  from .mapping import EventMapper, EventResolver
21
21
  from .router import (
22
+ DisconnectCallback,
23
+ DisconnectDecorator,
24
+ ErrorContext,
25
+ ErrorDecorator,
26
+ ErrorEntry,
27
+ ErrorScope,
28
+ ErrorSource,
22
29
  FilterCallback,
23
30
  HandlerCallback,
24
31
  HandlerDecorator,
25
32
  HandlerEntry,
26
33
  Router,
34
+ StartCallback,
27
35
  StartDecorator,
28
36
  )
29
37
 
@@ -31,11 +39,12 @@ if TYPE_CHECKING:
31
39
  from collections.abc import Generator
32
40
 
33
41
  from pymax.app import App
42
+ from pymax.base import BaseClient
34
43
 
35
44
 
36
45
  logger = get_logger(__name__)
37
46
 
38
- ClientT = TypeVar("ClientT")
47
+ ClientT = TypeVar("ClientT", bound="BaseClient")
39
48
 
40
49
 
41
50
  class Dispatcher(Generic[ClientT]):
@@ -78,6 +87,13 @@ class Dispatcher(Generic[ClientT]):
78
87
  logger.debug("registering handler event=%s filters=%s", event, len(filters))
79
88
  return self.root_router.on(event, *filters)
80
89
 
90
+ def on_error(self, scope: ErrorScope = ErrorScope.GLOBAL) -> ErrorDecorator[ClientT]:
91
+ return self.root_router.on_error(scope)
92
+
93
+ def on_disconnect(self) -> DisconnectDecorator:
94
+ """Регистрирует обработчик сетевого отключения на root router."""
95
+ return self.root_router.on_disconnect()
96
+
81
97
  def on_message(
82
98
  self,
83
99
  *filters: FilterCallback[Message],
@@ -146,22 +162,62 @@ class Dispatcher(Generic[ClientT]):
146
162
  for child in router.children:
147
163
  yield from self._iter_router(child)
148
164
 
149
- async def emit_start(self, client: ClientT) -> None:
150
- tasks: list[asyncio.Task[Any]] = []
165
+ def iter_error_entries(
166
+ self,
167
+ ) -> Generator[tuple[Router[ClientT], ErrorEntry[ClientT]], Any, None]:
168
+ for router in self.iter_routers():
169
+ for entry in router.error_handlers:
170
+ yield router, entry
151
171
 
172
+ def iter_disconnect_handlers(self) -> Generator[DisconnectCallback, Any, None]:
173
+ """Итерирует обработчики disconnect по root router и его детям."""
152
174
  for router in self.iter_routers():
153
- handler = router.on_start_handler
154
- if handler is None:
175
+ yield from router.disconnect_handlers
176
+
177
+ def iter_error_handlers(
178
+ self,
179
+ failed_router: Router[ClientT],
180
+ ) -> Generator[ErrorEntry[ClientT], Any, None]:
181
+ for owner_router, entry in self.iter_error_entries():
182
+ if entry.scope is ErrorScope.LOCAL and owner_router is not failed_router:
155
183
  continue
156
184
 
157
- result = handler(client)
185
+ yield entry
158
186
 
159
- if inspect.iscoroutine(result):
160
- task = asyncio.create_task(result)
187
+ async def emit_start(self, client: ClientT) -> None:
188
+ tasks: list[asyncio.Task[Any]] = []
189
+
190
+ for router in self.iter_routers(): # TODO: create iter_on_start_handlers
191
+ for handler in router.on_start_handlers:
192
+ task = asyncio.create_task(self._run_start_handler(router, handler, client))
161
193
  task.add_done_callback(_log_task_error)
162
194
  tasks.append(task)
163
195
 
164
- self.startup_tasks = tasks
196
+ self.startup_tasks.extend(tasks)
197
+
198
+ async def _run_start_handler(
199
+ self,
200
+ router: Router[ClientT],
201
+ handler: StartCallback[ClientT],
202
+ client: ClientT,
203
+ ) -> None:
204
+ try:
205
+ result = handler(client)
206
+
207
+ if inspect.isawaitable(result):
208
+ await result
209
+
210
+ except Exception as e:
211
+ handled = await self.emit_error(
212
+ e,
213
+ EventType.ON_START,
214
+ None,
215
+ router,
216
+ handler,
217
+ )
218
+
219
+ if not handled:
220
+ raise
165
221
 
166
222
  async def stop_startup_tasks(self) -> None:
167
223
  if not self.startup_tasks:
@@ -201,13 +257,18 @@ class Dispatcher(Generic[ClientT]):
201
257
  event: Any,
202
258
  ) -> None:
203
259
  for entry in router.handlers.get(event_type, []):
204
- if await self._matches(entry, event):
205
- logger.debug(
206
- "calling handler event=%s callback=%s",
207
- event_type,
208
- _callback_name(entry.callback),
209
- )
210
- await self._call(entry.callback, event)
260
+ try:
261
+ if await self._matches(entry, event):
262
+ logger.debug(
263
+ "calling handler event=%s callback=%s",
264
+ event_type,
265
+ _callback_name(entry.callback),
266
+ )
267
+ await self._call(entry.callback, event)
268
+ except Exception as e: # noqa: PERF203
269
+ handled = await self.emit_error(e, event_type, event, router, entry)
270
+ if not handled:
271
+ raise
211
272
 
212
273
  for child in router.children:
213
274
  await self._dispatch_to_router(child, event_type, event)
@@ -240,6 +301,63 @@ class Dispatcher(Generic[ClientT]):
240
301
 
241
302
  return result
242
303
 
304
+ async def emit_error(
305
+ self,
306
+ exception: Exception,
307
+ event_type: EventType,
308
+ event: Any,
309
+ router: Router[ClientT],
310
+ handler: ErrorSource[ClientT] | None,
311
+ ) -> bool:
312
+ client = self.client
313
+ handled = False
314
+
315
+ if client is None:
316
+ raise RuntimeError("client is not bound to dispatcher")
317
+
318
+ ctx = ErrorContext[ClientT](
319
+ client=client,
320
+ event_type=event_type,
321
+ event=event,
322
+ router=router,
323
+ handler=handler,
324
+ )
325
+ for entry in self.iter_error_handlers(router):
326
+ handled = True
327
+ try:
328
+ result = entry.callback(exception, ctx)
329
+
330
+ if inspect.isawaitable(result):
331
+ await result
332
+ except Exception as e:
333
+ logger.exception("Error while error handling: %s", e)
334
+ return False
335
+
336
+ return handled
337
+
338
+ async def emit_disconnect(
339
+ self,
340
+ exception: Exception,
341
+ reconnect: bool,
342
+ delay: float,
343
+ ) -> None:
344
+ """Вызывает обработчики потери соединения.
345
+
346
+ Ошибки внутри disconnect-handler-ов логируются и не прерывают reconnect.
347
+ """
348
+
349
+ if self.client is None:
350
+ raise RuntimeError("client is not bound to dispatcher")
351
+
352
+ for handler in self.iter_disconnect_handlers():
353
+ try:
354
+ result = handler(exception, reconnect, delay)
355
+
356
+ if inspect.isawaitable(result):
357
+ await result
358
+ except Exception as e:
359
+ logger.exception("Error during disconnect handling: %s", e)
360
+
243
361
 
244
362
  def _callback_name(callback: Any) -> str:
245
363
  return getattr(
pymax/dispatch/enums.py CHANGED
@@ -14,3 +14,4 @@ class EventType(str, Enum):
14
14
  VIDEO_READY = "video_ready"
15
15
  FILE_READY = "file_ready"
16
16
  RAW = "raw"
17
+ ON_START = "on_start"
pymax/dispatch/router.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from collections import defaultdict
4
4
  from collections.abc import Awaitable, Callable
5
5
  from dataclasses import dataclass
6
+ from enum import Enum
6
7
  from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar
7
8
 
8
9
  from pymax.types import MessageDeleteEvent
@@ -10,7 +11,8 @@ from pymax.types import MessageDeleteEvent
10
11
  from .enums import EventType
11
12
 
12
13
  if TYPE_CHECKING:
13
- from pymax.client import Client
14
+ from pymax import Client
15
+ from pymax.base import BaseClient
14
16
  from pymax.protocol import InboundFrame
15
17
  from pymax.types import Chat
16
18
  from pymax.types.domain import Message
@@ -22,8 +24,15 @@ if TYPE_CHECKING:
22
24
  )
23
25
 
24
26
 
27
+ class ErrorScope(str, Enum):
28
+ """Область действия error-handler-а."""
29
+
30
+ GLOBAL = "global"
31
+ LOCAL = "local"
32
+
33
+
25
34
  _EventT = TypeVar("_EventT")
26
- ClientT = TypeVar("ClientT")
35
+ ClientT = TypeVar("ClientT", bound="BaseClient")
27
36
 
28
37
  HandlerCallback: TypeAlias = Callable[
29
38
  [_EventT, ClientT],
@@ -47,12 +56,53 @@ StartDecorator: TypeAlias = Callable[
47
56
  ]
48
57
 
49
58
 
59
+ @dataclass(slots=True)
60
+ class ErrorContext(Generic[ClientT]):
61
+ """Контекст ошибки, передаваемый в ``on_error`` callback."""
62
+
63
+ client: ClientT
64
+ event_type: EventType
65
+ event: Any
66
+ handler: HandlerEntry[Any, ClientT] | StartCallback | None
67
+ router: Router[ClientT]
68
+
69
+
70
+ ErrorCallback: TypeAlias = Callable[
71
+ [Exception, ErrorContext[ClientT]],
72
+ Awaitable[Any] | Any,
73
+ ]
74
+
75
+ ErrorDecorator: TypeAlias = Callable[
76
+ [ErrorCallback[ClientT]],
77
+ ErrorCallback[ClientT],
78
+ ]
79
+
80
+ DisconnectCallback: TypeAlias = Callable[
81
+ [Exception, bool, float],
82
+ Awaitable[Any] | Any,
83
+ ]
84
+
85
+ DisconnectDecorator: TypeAlias = Callable[
86
+ [DisconnectCallback],
87
+ DisconnectCallback,
88
+ ]
89
+
90
+
50
91
  @dataclass(slots=True)
51
92
  class HandlerEntry(Generic[_EventT, ClientT]):
52
93
  callback: HandlerCallback[_EventT, ClientT]
53
94
  filters: tuple[FilterCallback[_EventT], ...] = ()
54
95
 
55
96
 
97
+ @dataclass(slots=True)
98
+ class ErrorEntry(Generic[ClientT]):
99
+ callback: ErrorCallback[ClientT]
100
+ scope: ErrorScope = ErrorScope.GLOBAL
101
+
102
+
103
+ ErrorSource: TypeAlias = HandlerEntry[Any, ClientT] | StartCallback[ClientT]
104
+
105
+
56
106
  class Router(Generic[ClientT]):
57
107
  """Контейнер обработчиков событий PyMax.
58
108
 
@@ -85,7 +135,39 @@ class Router(Generic[ClientT]):
85
135
  ] = defaultdict(list)
86
136
 
87
137
  self.children: list[Router[ClientT]] = []
88
- self.on_start_handler: StartCallback[ClientT] | None = None
138
+ self.on_start_handlers: list[StartCallback[ClientT]] = []
139
+ self.error_handlers: list[ErrorEntry[ClientT]] = []
140
+ self.disconnect_handlers: list[DisconnectCallback] = []
141
+
142
+ def on_error(
143
+ self,
144
+ scope: ErrorScope = ErrorScope.GLOBAL,
145
+ ) -> ErrorDecorator[ClientT]:
146
+ """Регистрирует обработчик ошибок для текущего router-а.
147
+
148
+ ``GLOBAL``-handler видит ошибки всего дерева подключенных router-ов.
149
+ ``LOCAL``-handler видит только ошибки своего router-а.
150
+ """
151
+ scope = ErrorScope(scope)
152
+
153
+ def decorator(callback: ErrorCallback[ClientT]) -> ErrorCallback[ClientT]:
154
+ self.error_handlers.append(ErrorEntry(callback=callback, scope=scope))
155
+ return callback
156
+
157
+ return decorator
158
+
159
+ def on_disconnect(self) -> DisconnectDecorator:
160
+ """Регистрирует обработчик потери соединения.
161
+
162
+ Callback вызывается как ``handler(exception, reconnect, delay)``:
163
+ исходная ошибка, будет ли reconnect и задержка перед ним.
164
+ """
165
+
166
+ def decorator(callback: DisconnectCallback) -> DisconnectCallback:
167
+ self.disconnect_handlers.append(callback)
168
+ return callback
169
+
170
+ return decorator
89
171
 
90
172
  def on(
91
173
  self,
@@ -145,7 +227,7 @@ class Router(Generic[ClientT]):
145
227
  """
146
228
 
147
229
  def decorator(handler: StartCallback) -> StartCallback:
148
- self.on_start_handler = handler
230
+ self.on_start_handlers.append(handler)
149
231
  return handler
150
232
 
151
233
  return decorator
pymax/infra/chat.py CHANGED
@@ -227,6 +227,27 @@ class ChatMixin(IClientProtocol):
227
227
  """
228
228
  await self._app.api.chats.leave_channel(chat_id)
229
229
 
230
+ async def delete_chat(
231
+ self,
232
+ chat_id: int,
233
+ last_event_time: int | None = None,
234
+ for_all: bool = True,
235
+ ) -> None:
236
+ """Удаляет чат.
237
+
238
+ Args:
239
+ chat_id: ID чата.
240
+ last_event_time: Время последнего события чата. Для объекта
241
+ ``Chat`` это поле ``Chat.last_event_time``.
242
+ for_all: Удалить чат для всех участников, если сервер поддерживает
243
+ такой режим.
244
+ """
245
+ await self._app.api.chats.delete_chat(
246
+ chat_id=chat_id,
247
+ last_event_time=last_event_time,
248
+ for_all=for_all,
249
+ )
250
+
230
251
  async def fetch_chats(self, marker: int | None = None) -> list[Chat]:
231
252
  """Загружает список чатов с сервера и обновляет кеш клиента.
232
253
 
pymax/infra/message.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from pymax.api.messages.enums import ItemType
2
- from pymax.api.messages.service import SendAttachment, SendAttachments
2
+ from pymax.api.messages.service import SendAttachments
3
3
  from pymax.types import (
4
4
  FileRequest,
5
5
  Message,
@@ -86,7 +86,6 @@ class MessageMixin(IClientProtocol):
86
86
  chat_id: int,
87
87
  message_id: int,
88
88
  text: str,
89
- attachment: SendAttachment | None = None,
90
89
  attachments: SendAttachments = None,
91
90
  ) -> Message:
92
91
  """Редактирует текст и вложения сообщения.
@@ -95,9 +94,7 @@ class MessageMixin(IClientProtocol):
95
94
  chat_id: ID чата.
96
95
  message_id: ID сообщения.
97
96
  text: Новый текст сообщения с поддержкой markdown.
98
- attachment: Одно новое вложение.
99
- attachments: Список новых вложений. Имеет приоритет над
100
- ``attachment``.
97
+ attachments: Новые файлы, фотографии или видео для сообщения.
101
98
 
102
99
  Returns:
103
100
  Отредактированное сообщение.
@@ -106,7 +103,6 @@ class MessageMixin(IClientProtocol):
106
103
  chat_id=chat_id,
107
104
  message_id=message_id,
108
105
  text=text,
109
- attachment=attachment,
110
106
  attachments=attachments,
111
107
  )
112
108
 
pymax/infra/user.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import Literal
2
2
 
3
- from pymax.types import Session, User
3
+ from pymax.types import ContactInfo, Session, User
4
4
 
5
5
  from .protocol import IClientProtocol
6
6
 
@@ -94,6 +94,17 @@ class UserMixin(IClientProtocol):
94
94
  """
95
95
  return await self._app.api.users.remove_contact(contact_id)
96
96
 
97
+ async def import_contacts(self, contacts: list[ContactInfo]) -> list[User]:
98
+ """Импортирует контакты из телефонной книги.
99
+
100
+ Args:
101
+ contacts: Контакты с телефоном и именем.
102
+
103
+ Returns:
104
+ Контакты Max, найденные или созданные сервером.
105
+ """
106
+ return await self._app.api.users.import_contacts(contacts)
107
+
97
108
  def get_chat_id(self, first_user_id: int, second_user_id: int) -> int:
98
109
  """Вычисляет ID личного чата для пары пользователей.
99
110
 
pymax/session/store.py CHANGED
@@ -194,6 +194,17 @@ class SessionStore:
194
194
  await conn.commit()
195
195
  logger.info("session deleted")
196
196
 
197
+ async def delete_all_sessions(self) -> None:
198
+ conn = await self._get_connection()
199
+ logger.warning("deleting all sessions")
200
+ await conn.execute(
201
+ """
202
+ DELETE FROM sessions
203
+ """
204
+ )
205
+ await conn.commit()
206
+ logger.info("all sessions deleted")
207
+
197
208
  async def update_token(self, old_token: str, new_token: str) -> None:
198
209
  conn = await self._get_connection()
199
210
  logger.debug(
@@ -11,4 +11,4 @@ from .presence import Presence
11
11
  from .profile import Profile
12
12
  from .session import Session
13
13
  from .sync import SyncOverrides, SyncState
14
- from .user import User
14
+ from .user import ContactInfo, User
@@ -20,7 +20,7 @@ class Chat(CamelModel):
20
20
  Объекты чатов, полученные через клиент, обычно уже привязаны к сервисам
21
21
  сообщений и чатов. После этого можно вызывать удобные методы объекта:
22
22
  :meth:`answer`, :meth:`history`, :meth:`get_message`,
23
- :meth:`get_messages`, :meth:`leave`, :meth:`invite`,
23
+ :meth:`get_messages`, :meth:`leave`, :meth:`delete`, :meth:`invite`,
24
24
  :meth:`remove_users`, :meth:`pin_message`, :meth:`update_settings` и
25
25
  :meth:`rework_invite_link`.
26
26
 
@@ -305,6 +305,26 @@ class Chat(CamelModel):
305
305
 
306
306
  raise ValueError("Unknown chat type=%s", self.type)
307
307
 
308
+ async def delete(self, *, for_all: bool = True) -> None:
309
+ """Удаляет этот чат.
310
+
311
+ Для ``last_event_time`` используется значение ``Chat.last_event_time``.
312
+
313
+ :param for_all: Удалить чат для всех участников, если сервер
314
+ поддерживает такой режим.
315
+ :type for_all: bool
316
+ :returns: ``None``.
317
+ :rtype: None
318
+ :raises RuntimeError: Если чат не привязан к клиенту.
319
+ """
320
+ _, chat_actions = self._bound()
321
+
322
+ return await chat_actions.delete_chat(
323
+ self.id,
324
+ last_event_time=self.last_event_time,
325
+ for_all=for_all,
326
+ )
327
+
308
328
  async def invite(
309
329
  self,
310
330
  user_ids: list[int],
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Sequence
3
4
  from typing import TYPE_CHECKING, Annotated, Any, TypeAlias
4
5
 
5
6
  from pydantic import Field, PrivateAttr, model_validator
@@ -42,7 +43,7 @@ KnownAttachment: TypeAlias = Annotated[
42
43
  ]
43
44
  Attachment: TypeAlias = KnownAttachment | UnknownAttachment
44
45
  SendAttachment: TypeAlias = Photo | File | Video
45
- SendAttachments: TypeAlias = list[SendAttachment] | None
46
+ SendAttachments: TypeAlias = Sequence[SendAttachment] | None
46
47
 
47
48
 
48
49
  class ReactionCounter(CamelModel):
@@ -264,17 +265,13 @@ class Message(CamelModel):
264
265
  async def edit(
265
266
  self,
266
267
  text: str,
267
- attachment: SendAttachment | None = None,
268
268
  attachments: SendAttachments = None,
269
269
  ) -> Message:
270
270
  """Редактирует текст и вложения этого сообщения.
271
271
 
272
272
  :param text: Новый текст сообщения с поддержкой markdown.
273
273
  :type text: str
274
- :param attachment: Одно новое вложение.
275
- :type attachment: SendAttachment | None
276
- :param attachments: Список новых вложений. Имеет приоритет над
277
- ``attachment``.
274
+ :param attachments: Новые файлы, фотографии или видео для сообщения.
278
275
  :type attachments: SendAttachments
279
276
  :returns: Отредактированное сообщение.
280
277
  :rtype: Message
@@ -287,7 +284,6 @@ class Message(CamelModel):
287
284
  chat_id=chat_id,
288
285
  message_id=self.id,
289
286
  text=text,
290
- attachment=attachment,
291
287
  attachments=attachments,
292
288
  )
293
289
 
@@ -11,6 +11,14 @@ if TYPE_CHECKING:
11
11
  from pymax.api.users.service import UserService
12
12
 
13
13
 
14
+ class ContactInfo(CamelModel): # TODO: move to another file
15
+ """Контакт телефонной книги для ``import_contacts``."""
16
+
17
+ phone: str
18
+ first_name: str
19
+ last_name: str | None = None
20
+
21
+
14
22
  class User(CamelModel):
15
23
  """Контакт или пользователь Max.
16
24