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.
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.0.dist-info}/METADATA +1 -1
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.0.dist-info}/RECORD +24 -24
- pymax/__init__.py +1 -1
- pymax/api/chats/payloads.py +6 -0
- pymax/api/chats/service.py +18 -0
- pymax/api/messages/service.py +3 -11
- pymax/api/users/payloads.py +22 -0
- pymax/api/users/service.py +14 -1
- pymax/app.py +26 -6
- pymax/base.py +48 -3
- pymax/dispatch/__init__.py +12 -1
- pymax/dispatch/dispatcher.py +134 -16
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/router.py +86 -4
- pymax/infra/chat.py +21 -0
- pymax/infra/message.py +2 -6
- pymax/infra/user.py +12 -1
- pymax/session/store.py +11 -0
- pymax/types/domain/__init__.py +1 -1
- pymax/types/domain/chat.py +21 -1
- pymax/types/domain/message.py +3 -7
- pymax/types/domain/user.py +8 -0
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.0.dist-info}/WHEEL +0 -0
- {maxapi_python-2.2.0.dist-info → maxapi_python-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
pymax/__init__.py,sha256=
|
|
2
|
-
pymax/app.py,sha256=
|
|
3
|
-
pymax/base.py,sha256
|
|
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=
|
|
27
|
-
pymax/api/chats/service.py,sha256=
|
|
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=
|
|
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=
|
|
47
|
-
pymax/api/users/service.py,sha256=
|
|
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=
|
|
64
|
-
pymax/dispatch/dispatcher.py,sha256=
|
|
65
|
-
pymax/dispatch/enums.py,sha256=
|
|
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=
|
|
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=
|
|
82
|
-
pymax/infra/message.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
151
|
-
maxapi_python-2.
|
|
152
|
-
maxapi_python-2.
|
|
153
|
-
maxapi_python-2.
|
|
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
pymax/api/chats/payloads.py
CHANGED
|
@@ -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
|
pymax/api/chats/service.py
CHANGED
|
@@ -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)
|
pymax/api/messages/service.py
CHANGED
|
@@ -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 =
|
|
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(
|
|
181
|
+
attachments=await self._upload_attachments(attachments),
|
|
190
182
|
)
|
|
191
183
|
|
|
192
184
|
response = await self.app.invoke(Opcode.MSG_EDIT, frame.to_payload())
|
pymax/api/users/payloads.py
CHANGED
|
@@ -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
|
+
)
|
pymax/api/users/service.py
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
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.
|
|
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()
|
pymax/dispatch/__init__.py
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
from .dispatcher import Dispatcher
|
|
2
2
|
from .enums import EventType
|
|
3
|
-
from .router import
|
|
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
|
)
|
pymax/dispatch/dispatcher.py
CHANGED
|
@@ -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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
185
|
+
yield entry
|
|
158
186
|
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
pymax/types/domain/__init__.py
CHANGED
pymax/types/domain/chat.py
CHANGED
|
@@ -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],
|
pymax/types/domain/message.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
pymax/types/domain/user.py
CHANGED
|
@@ -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
|
|
|
File without changes
|
|
File without changes
|