maxapi-python 2.2.0__py3-none-any.whl → 2.3.1__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.1
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
@@ -31,6 +31,7 @@ Requires-Dist: pydantic>=2.10.0
31
31
  Requires-Dist: python-socks[asyncio]>=2.8.1
32
32
  Requires-Dist: qrcode>=8.2
33
33
  Requires-Dist: websockets>=16.0
34
+ Requires-Dist: zstandard>=0.25.0
34
35
  Description-Content-Type: text/markdown
35
36
 
36
37
  # 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=y6_pIn47iHEIkP4F4y7EL-qiIpAgJqs_jVKnOmooniY,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
- pymax/api/messages/payloads.py,sha256=wqseGNfqWYKeoRJhdzrRV855IboChaYU1dJLPGZg71I,2257
31
- pymax/api/messages/service.py,sha256=Zur05Rklq2LrssIWLE5tThAXPXK9b7MsvFW8BklSoHU,12422
30
+ pymax/api/messages/payloads.py,sha256=MQSaZD0kYbkojWNK6BJbwoS02ZjDX7eutFnJX3_L6_g,2712
31
+ pymax/api/messages/service.py,sha256=lGnciRoP9PkpgBDpkHQU1ukxNTwMJuGjHhM1EeSHWXU,13334
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,26 +78,26 @@ 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=4AUp8EUQ46GhsJMgh7YmgogkBT0K4wUqEkIfrFT--oI,11737
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
89
89
  pymax/protocol/models.py,sha256=kno-09OoPoLKBMixS9nrDkCbxcIwvU37RlZgaq5MfVo,584
90
90
  pymax/protocol/tcp/__init__.py,sha256=ivIZZ-UoT_MiRIUWTLiyeKSBOzf9XztSprHPHgoWNuI,34
91
- pymax/protocol/tcp/compression.py,sha256=sa9xe3cjAYXsJCPPxGc6iRyctFaT15VcqQ0fiC8vk4A,3014
91
+ pymax/protocol/tcp/compression.py,sha256=JsW84MOjtUCJPxMfsfc4CX2bMH0D5sb_2mut189KHmU,3545
92
92
  pymax/protocol/tcp/framing.py,sha256=qH2zNsJLfg5J2wNTcp1hPqsrWhZHajK-Ja7pdB_Gji0,1630
93
- pymax/protocol/tcp/payload.py,sha256=86-NJ5MVvxpgD2ffQqJGaSbDhVBFhaNOH91-k1IQJk8,3515
94
- pymax/protocol/tcp/protocol.py,sha256=-8JQe5rhXXtBMNeCWM8XJXmlnL-0lNSEUCP6LU2zU7c,2244
93
+ pymax/protocol/tcp/payload.py,sha256=roViz6uNrJvmEBukPwBPk462uFJMya48jEnLthZs-yU,4329
94
+ pymax/protocol/tcp/protocol.py,sha256=b5AAYQUOhq985Wc1pCPzt84pjGnVGfGxbdwRN0R91xk,2380
95
95
  pymax/protocol/ws/__init__.py,sha256=oQK0h28B6gGItf7eHanWy_XsgUA_L9Lclnuz9ZQL9YI,33
96
96
  pymax/protocol/ws/protocol.py,sha256=z6zzBQTlJfzwOmLsJYoYQ2L1AYpY5wYMJyPir_zfNBQ,913
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=a1wdIVfRcLo30rUUQzBAyLqCGcPoc-6Uk9jcDg8oqHQ,16497
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=tpHBnatNXdajS8noMy8oox9qp8Yej9b7mkdtg7BpjQM,5514
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.1.dist-info/METADATA,sha256=k5ItHuOsbXla9MpvlsqOWi7lJbV2u_T8wMotxOA2vDc,7309
151
+ maxapi_python-2.3.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
152
+ maxapi_python-2.3.1.dist-info/licenses/LICENSE,sha256=hOR249ItqMdcly1A0amqEWRNRTq4Gv5NJtmQ3A5qK4E,1070
153
+ maxapi_python-2.3.1.dist-info/RECORD,,
pymax/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "2.2.0"
1
+ __version__ = "2.3.1"
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,4 +1,4 @@
1
- from typing import Any
1
+ from typing import Any, Literal
2
2
 
3
3
  from pydantic import Field
4
4
 
@@ -46,6 +46,26 @@ class SendMessagePayload(CamelModel):
46
46
  notify: bool = False
47
47
 
48
48
 
49
+ class ForwardLink(CamelModel):
50
+ type: Literal["FORWARD"] = "FORWARD"
51
+ message_id: str
52
+ chat_id: int
53
+
54
+
55
+ class ForwardMessagePayloadMessage(CamelModel):
56
+ cid: int
57
+ link: ForwardLink
58
+ attaches: list[AttachPhotoPayload | VideoAttachPayload | AttachFilePayload] = Field(
59
+ default_factory=list
60
+ )
61
+
62
+
63
+ class ForwardMessagePayload(CamelModel):
64
+ chat_id: int
65
+ message: ForwardMessagePayloadMessage
66
+ notify: bool = True
67
+
68
+
49
69
  class ChatHistoryPayload(CamelModel):
50
70
  chat_id: int
51
71
  forward: int
@@ -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
@@ -35,6 +36,9 @@ from .payloads import (
35
36
  ChatHistoryPayload,
36
37
  DeleteMessagePayload,
37
38
  EditMessagePayload,
39
+ ForwardLink,
40
+ ForwardMessagePayload,
41
+ ForwardMessagePayloadMessage,
38
42
  GetFilePayload,
39
43
  GetMessagesPayload,
40
44
  GetReactionsPayload,
@@ -52,7 +56,7 @@ if TYPE_CHECKING:
52
56
  from pymax.app import App
53
57
 
54
58
  SendAttachment: TypeAlias = Photo | File | Video
55
- SendAttachments: TypeAlias = list[SendAttachment] | None
59
+ SendAttachments: TypeAlias = Sequence[SendAttachment] | None
56
60
 
57
61
  logger = get_logger(__name__)
58
62
 
@@ -138,6 +142,42 @@ class MessageService:
138
142
  logger.info("message sent chat_id=%s", chat_id)
139
143
  return message
140
144
 
145
+ async def forward_message(
146
+ self,
147
+ chat_id: int,
148
+ message_id: int | str,
149
+ source_chat_id: int | None = None,
150
+ *,
151
+ notify: bool = True,
152
+ ) -> Message | None:
153
+ source_chat_id = chat_id if source_chat_id is None else source_chat_id
154
+ logger.info(
155
+ "forwarding message source_chat_id=%s chat_id=%s message_id=%s",
156
+ source_chat_id,
157
+ chat_id,
158
+ message_id,
159
+ )
160
+
161
+ frame = ForwardMessagePayload(
162
+ chat_id=chat_id,
163
+ message=ForwardMessagePayloadMessage(
164
+ cid=-self._next_cid(),
165
+ link=ForwardLink(
166
+ message_id=str(message_id),
167
+ chat_id=source_chat_id,
168
+ ),
169
+ ),
170
+ notify=notify,
171
+ )
172
+
173
+ response = await self.app.invoke(Opcode.MSG_SEND, frame.to_payload())
174
+ message = bind_api_model(
175
+ self.app,
176
+ require_payload_model(response, Message),
177
+ )
178
+ logger.info("message forwarded source_chat_id=%s chat_id=%s", source_chat_id, chat_id)
179
+ return message
180
+
141
181
  async def get_messages(
142
182
  self,
143
183
  chat_id: int,
@@ -169,24 +209,15 @@ class MessageService:
169
209
  chat_id: int,
170
210
  message_id: int,
171
211
  text: str,
172
- attachment: SendAttachment | None = None,
173
212
  attachments: SendAttachments = None,
174
213
  ) -> 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
214
  clean_text, elements = Formatter.format_markdown(text)
184
215
  frame = EditMessagePayload(
185
216
  chat_id=chat_id,
186
217
  message_id=message_id,
187
218
  text=clean_text,
188
219
  elements=elements,
189
- attachments=await self._upload_attachments(edit_attachments),
220
+ attachments=await self._upload_attachments(attachments),
190
221
  )
191
222
 
192
223
  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
  )