maxapi-python 2.3.1__py3-none-any.whl → 2.4.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.3.1.dist-info → maxapi_python-2.4.0.dist-info}/METADATA +3 -1
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/RECORD +59 -51
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +7 -2
- pymax/_data/apk_fingerprints.json +682 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +88 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +16 -1
- pymax/api/chats/service.py +36 -1
- pymax/api/messages/payloads.py +12 -4
- pymax/api/messages/service.py +57 -14
- pymax/api/self/__init__.py +2 -1
- pymax/api/self/enums.py +9 -0
- pymax/api/self/payloads.py +40 -1
- pymax/api/self/service.py +37 -0
- pymax/api/session/service.py +21 -8
- pymax/api/uploads/payloads.py +6 -0
- pymax/api/uploads/service.py +198 -21
- pymax/app.py +69 -15
- pymax/base.py +8 -0
- pymax/client_web.py +2 -2
- pymax/config.py +44 -7
- pymax/connection/connection.py +29 -18
- pymax/dispatch/enums.py +1 -0
- pymax/dispatch/mapping.py +3 -1
- pymax/dispatch/resolvers.py +7 -1
- pymax/files/__init__.py +4 -1
- pymax/files/video.py +58 -0
- pymax/files/voice.py +39 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +47 -0
- pymax/fingerprint/models.py +11 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +38 -0
- pymax/infra/message.py +40 -11
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- pymax/transport/tcp.py +6 -1
- pymax/transport/websocket.py +6 -3
- pymax/types/domain/__init__.py +2 -1
- pymax/types/domain/attachments/__init__.py +2 -1
- pymax/types/domain/attachments/enums.py +17 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +29 -8
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +21 -20
- pymax/types/domain/user.py +1 -3
- pymax/types/events/__init__.py +1 -0
- pymax/types/events/reaction.py +2 -2
- pymax/types/events/voice.py +5 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.0.dist-info}/licenses/LICENSE +0 -0
pymax/config.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from random import choice, randint
|
|
1
|
+
from random import choice, randint, random
|
|
2
2
|
from uuid import uuid4
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -11,17 +11,48 @@ from pymax.api.session.payloads import (
|
|
|
11
11
|
from pymax.session import StoreProtocol
|
|
12
12
|
from pymax.types.domain.sync import SyncOverrides
|
|
13
13
|
|
|
14
|
+
MIN_PREFERRED_BUILD = 6712
|
|
14
15
|
APP_VERSIONS: tuple[tuple[str, int], ...] = (
|
|
16
|
+
("26.25.0", 6790),
|
|
17
|
+
("26.24.0", 6784),
|
|
18
|
+
("26.23.2", 6779),
|
|
19
|
+
("26.23.1", 6778),
|
|
20
|
+
("26.23.0", 6777),
|
|
21
|
+
("26.22.2", 6773),
|
|
22
|
+
("26.22.1", 6772),
|
|
23
|
+
("26.22.0", 6770),
|
|
24
|
+
("26.21.1", 6763),
|
|
25
|
+
("26.20.2", 6758),
|
|
26
|
+
("26.20.1", 6740),
|
|
27
|
+
("26.19.3", 6734),
|
|
28
|
+
("26.19.2", 6732),
|
|
29
|
+
("26.19.1", 6729),
|
|
30
|
+
("26.19.0", 6727),
|
|
31
|
+
("26.18.4", 6724),
|
|
32
|
+
("26.18.2", 6720),
|
|
33
|
+
("26.18.1", 6716),
|
|
34
|
+
("26.18.0", 6715),
|
|
35
|
+
("26.17.1", 6712),
|
|
36
|
+
("26.16.4", 6704),
|
|
37
|
+
("26.16.3", 6702),
|
|
38
|
+
("26.16.2", 6701),
|
|
39
|
+
("26.16.1", 6700),
|
|
40
|
+
("26.16.0", 6698),
|
|
41
|
+
("26.15.3", 6695),
|
|
42
|
+
("26.15.1", 6690),
|
|
43
|
+
("26.15.0", 6689),
|
|
15
44
|
("26.14.1", 6686),
|
|
16
45
|
("26.14.0", 6685),
|
|
17
46
|
("26.13.0", 6683),
|
|
18
47
|
("26.12.2", 6681),
|
|
19
48
|
("26.12.1", 6679),
|
|
20
|
-
("26.12.0",
|
|
21
|
-
("26.11.3",
|
|
49
|
+
("26.12.0", 6676),
|
|
50
|
+
("26.11.3", 6670),
|
|
22
51
|
("26.11.2", 6669),
|
|
23
52
|
("26.11.1", 6665),
|
|
24
|
-
("26.
|
|
53
|
+
("26.10.1", 6653),
|
|
54
|
+
("26.10.0", 6648),
|
|
55
|
+
("26.9.1", 6643),
|
|
25
56
|
)
|
|
26
57
|
ANDROID_DEVICES: tuple[tuple[str, str, str, str], ...] = (
|
|
27
58
|
("Samsung SM-A525F", "Android 13", "405dpi 405dpi 1080x2400", "arm64-v8a"),
|
|
@@ -73,9 +104,12 @@ LOCALE_TIMEZONES: tuple[tuple[str, str], ...] = (
|
|
|
73
104
|
("ru", "Asia/Yakutsk"),
|
|
74
105
|
("ru", "Asia/Vladivostok"),
|
|
75
106
|
)
|
|
76
|
-
WEB_APP_VERSION = "26.
|
|
107
|
+
WEB_APP_VERSION = "26.7.15"
|
|
77
108
|
WEB_SCREEN = "1080x1920 1.0x"
|
|
78
109
|
|
|
110
|
+
PREFERRED_VERSION = [version for version in APP_VERSIONS if version[1] >= MIN_PREFERRED_BUILD]
|
|
111
|
+
LEGACY_VERSIONS = [version for version in APP_VERSIONS if version[1] < MIN_PREFERRED_BUILD]
|
|
112
|
+
|
|
79
113
|
|
|
80
114
|
class DeviceConfig(BaseModel):
|
|
81
115
|
mt_instance_id: str
|
|
@@ -119,6 +153,8 @@ class ClientConfig(BaseModel):
|
|
|
119
153
|
log_level: str = "INFO"
|
|
120
154
|
telemetry: bool = False
|
|
121
155
|
|
|
156
|
+
interactive: bool = True
|
|
157
|
+
|
|
122
158
|
store: StoreProtocol | None = None
|
|
123
159
|
|
|
124
160
|
sync: SyncOverrides = Field(default_factory=SyncOverrides)
|
|
@@ -176,7 +212,7 @@ class ExtraConfig(BaseModel):
|
|
|
176
212
|
|
|
177
213
|
host: str = "api.oneme.ru"
|
|
178
214
|
port: int = 443
|
|
179
|
-
url: str = "wss://
|
|
215
|
+
url: str = "wss://api.oneme.ru/websocket"
|
|
180
216
|
use_ssl: bool = True
|
|
181
217
|
proxy: str | None = None
|
|
182
218
|
reconnect: bool = True
|
|
@@ -201,7 +237,8 @@ class ExtraConfig(BaseModel):
|
|
|
201
237
|
Returns:
|
|
202
238
|
Случайная, но правдоподобная конфигурация Android-клиента Max.
|
|
203
239
|
"""
|
|
204
|
-
|
|
240
|
+
versions = PREFERRED_VERSION if random() < 0.9 else LEGACY_VERSIONS
|
|
241
|
+
app_version, build_number = choice(versions)
|
|
205
242
|
device_name, os_version, screen, arch = choice(ANDROID_DEVICES)
|
|
206
243
|
locale, timezone = choice(LOCALE_TIMEZONES)
|
|
207
244
|
|
pymax/connection/connection.py
CHANGED
|
@@ -37,6 +37,8 @@ class ConnectionManager:
|
|
|
37
37
|
|
|
38
38
|
self._recv_task: asyncio.Task[None] | None = None
|
|
39
39
|
self._event_tasks: set[asyncio.Task[None]] = set()
|
|
40
|
+
self._closed_event = asyncio.Event()
|
|
41
|
+
self._closed_event.set()
|
|
40
42
|
|
|
41
43
|
async def open(self) -> None:
|
|
42
44
|
if self._is_open:
|
|
@@ -48,6 +50,7 @@ class ConnectionManager:
|
|
|
48
50
|
self._is_open = True
|
|
49
51
|
self._connection_lost = False
|
|
50
52
|
self._close_reported = False
|
|
53
|
+
self._closed_event.clear()
|
|
51
54
|
|
|
52
55
|
self._recv_task = asyncio.create_task(self._recv_loop())
|
|
53
56
|
logger.debug("receive loop started")
|
|
@@ -58,25 +61,28 @@ class ConnectionManager:
|
|
|
58
61
|
return
|
|
59
62
|
|
|
60
63
|
logger.info("closing connection")
|
|
61
|
-
if self._recv_task:
|
|
62
|
-
if not self._recv_task.done():
|
|
63
|
-
self._recv_task.cancel()
|
|
64
|
-
with suppress(asyncio.CancelledError, Exception):
|
|
65
|
-
await self._recv_task
|
|
66
|
-
logger.debug("receive loop stopped")
|
|
67
|
-
self._recv_task = None
|
|
68
|
-
|
|
69
|
-
for task in tuple(self._event_tasks):
|
|
70
|
-
if not task.done():
|
|
71
|
-
task.cancel()
|
|
72
|
-
for task in tuple(self._event_tasks):
|
|
73
|
-
with suppress(asyncio.CancelledError, Exception):
|
|
74
|
-
await task
|
|
75
|
-
self._event_tasks.clear()
|
|
76
|
-
|
|
77
|
-
self.requests.cancel_all()
|
|
78
|
-
await self.transport.close()
|
|
79
64
|
self._is_open = False
|
|
65
|
+
try:
|
|
66
|
+
if self._recv_task:
|
|
67
|
+
if not self._recv_task.done():
|
|
68
|
+
self._recv_task.cancel()
|
|
69
|
+
with suppress(asyncio.CancelledError, Exception):
|
|
70
|
+
await self._recv_task
|
|
71
|
+
logger.debug("receive loop stopped")
|
|
72
|
+
self._recv_task = None
|
|
73
|
+
|
|
74
|
+
for task in tuple(self._event_tasks):
|
|
75
|
+
if not task.done():
|
|
76
|
+
task.cancel()
|
|
77
|
+
for task in tuple(self._event_tasks):
|
|
78
|
+
with suppress(asyncio.CancelledError, Exception):
|
|
79
|
+
await task
|
|
80
|
+
self._event_tasks.clear()
|
|
81
|
+
|
|
82
|
+
self.requests.cancel_all()
|
|
83
|
+
await self.transport.close()
|
|
84
|
+
finally:
|
|
85
|
+
self._closed_event.set()
|
|
80
86
|
logger.info("connection closed")
|
|
81
87
|
|
|
82
88
|
async def fail(self, exc: Exception | None = None) -> None:
|
|
@@ -147,6 +153,11 @@ class ConnectionManager:
|
|
|
147
153
|
|
|
148
154
|
try:
|
|
149
155
|
await self._recv_task
|
|
156
|
+
except asyncio.CancelledError:
|
|
157
|
+
if self._is_open:
|
|
158
|
+
raise
|
|
159
|
+
await self._closed_event.wait()
|
|
160
|
+
return
|
|
150
161
|
except Exception as e:
|
|
151
162
|
if self._connection_lost:
|
|
152
163
|
raise ConnectionError("Connection lost") from e
|
pymax/dispatch/enums.py
CHANGED
pymax/dispatch/mapping.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
from pymax.api.binding import bind_api_model
|
|
7
7
|
from pymax.protocol import InboundFrame, Opcode
|
|
8
8
|
from pymax.protocol.enums import Command
|
|
9
|
-
from pymax.types import Chat, MessageDeleteEvent
|
|
9
|
+
from pymax.types import AudioUploadSignal, Chat, MessageDeleteEvent
|
|
10
10
|
from pymax.types.domain import Message
|
|
11
11
|
from pymax.types.events import (
|
|
12
12
|
FileUploadSignal,
|
|
@@ -100,4 +100,6 @@ class EventMapper:
|
|
|
100
100
|
return VideoUploadSignal.model_validate(frame.payload)
|
|
101
101
|
elif event_type == EventType.FILE_READY:
|
|
102
102
|
return FileUploadSignal.model_validate(frame.payload)
|
|
103
|
+
elif event_type == EventType.VOICE_READY:
|
|
104
|
+
return AudioUploadSignal.model_validate(frame.payload)
|
|
103
105
|
return frame
|
pymax/dispatch/resolvers.py
CHANGED
|
@@ -3,7 +3,7 @@ from pydantic import ValidationError
|
|
|
3
3
|
from pymax.logging import get_logger
|
|
4
4
|
from pymax.protocol import InboundFrame
|
|
5
5
|
from pymax.protocol.enums import Opcode
|
|
6
|
-
from pymax.types import Message
|
|
6
|
+
from pymax.types import AudioUploadSignal, Message
|
|
7
7
|
from pymax.types.domain.enums import MessageStatus
|
|
8
8
|
from pymax.types.events import FileUploadSignal, VideoUploadSignal
|
|
9
9
|
|
|
@@ -49,6 +49,12 @@ def resolve_attach(frame: InboundFrame) -> EventType | None:
|
|
|
49
49
|
except ValidationError:
|
|
50
50
|
logger.debug("attach event is not a video upload signal")
|
|
51
51
|
|
|
52
|
+
try:
|
|
53
|
+
AudioUploadSignal.model_validate(frame.payload)
|
|
54
|
+
return EventType.VOICE_READY
|
|
55
|
+
except ValidationError:
|
|
56
|
+
logger.debug("attach event is not a voice upload signal")
|
|
57
|
+
|
|
52
58
|
return None
|
|
53
59
|
|
|
54
60
|
|
pymax/files/__init__.py
CHANGED
pymax/files/video.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator
|
|
2
|
+
from io import BytesIO
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from .base import BaseFile
|
|
5
6
|
|
|
7
|
+
try:
|
|
8
|
+
from tinytag import TinyTag
|
|
9
|
+
except ImportError:
|
|
10
|
+
TinyTag = None
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
class Video(BaseFile):
|
|
8
14
|
"""Видео для отправки в сообщение.
|
|
@@ -72,3 +78,55 @@ class Video(BaseFile):
|
|
|
72
78
|
Async generator с байтами.
|
|
73
79
|
"""
|
|
74
80
|
return super().iter_chunks(size)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class VideoNote(Video):
|
|
84
|
+
"""Круглое видеосообщение для отправки.
|
|
85
|
+
|
|
86
|
+
Принимает те же источники, что и ``Video``. Длительность задается в
|
|
87
|
+
миллисекундах. Если она не передана, требуется extra ``video`` для
|
|
88
|
+
автоматического определения длительности.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
raw: bytes | None = None,
|
|
94
|
+
*,
|
|
95
|
+
url: str | None = None,
|
|
96
|
+
path: str | None = None,
|
|
97
|
+
name: str | None = None,
|
|
98
|
+
duration: int | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
self.duration = duration
|
|
101
|
+
super().__init__(raw, url=url, path=path, name=name)
|
|
102
|
+
|
|
103
|
+
async def get_duration(self) -> int:
|
|
104
|
+
if self.duration is not None:
|
|
105
|
+
return self.duration
|
|
106
|
+
|
|
107
|
+
if not TinyTag:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
"Automatic video duration detection requires the 'video' extra. "
|
|
110
|
+
"Install it with `uv add 'maxapi-python[video]'` "
|
|
111
|
+
"or pass duration manually."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if self.raw:
|
|
115
|
+
tag = TinyTag.get(
|
|
116
|
+
filename=self.name,
|
|
117
|
+
file_obj=BytesIO(self.raw),
|
|
118
|
+
tags=False,
|
|
119
|
+
duration=True,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
tag = TinyTag.get(
|
|
123
|
+
filename=self.name,
|
|
124
|
+
file_obj=BytesIO(await self.read()),
|
|
125
|
+
tags=False,
|
|
126
|
+
duration=True,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if tag.duration is None:
|
|
130
|
+
raise ValueError("Failed to determine video duration.")
|
|
131
|
+
|
|
132
|
+
return round(tag.duration * 1000)
|
pymax/files/voice.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import BaseFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Voice(BaseFile):
|
|
8
|
+
"""Голосовое сообщение в формате OGG для отправки.
|
|
9
|
+
|
|
10
|
+
Принимает ``path``, ``url`` или ``raw``. Для ``raw`` необходимо явно
|
|
11
|
+
передать имя файла.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
raw: bytes | None = None,
|
|
17
|
+
*,
|
|
18
|
+
path: str | None = None,
|
|
19
|
+
url: str | None = None,
|
|
20
|
+
name: str | None = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.name: str = name or ""
|
|
23
|
+
if not self.name and path:
|
|
24
|
+
self.name = Path(path).name
|
|
25
|
+
elif not self.name and url:
|
|
26
|
+
self.name = Path(url).name
|
|
27
|
+
|
|
28
|
+
if not self.name:
|
|
29
|
+
raise ValueError("Either name, url or path must be provided.")
|
|
30
|
+
super().__init__(raw=raw, url=url, path=path, name=self.name)
|
|
31
|
+
|
|
32
|
+
async def read(self) -> bytes:
|
|
33
|
+
return await super().read()
|
|
34
|
+
|
|
35
|
+
async def size(self) -> int:
|
|
36
|
+
return await super().size()
|
|
37
|
+
|
|
38
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
39
|
+
return super().iter_chunks(size)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .fingerprint import FingerprintGenerator
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
import struct
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .models import ApkBuildFingerprint
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FingerprintGenerator:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
) -> None:
|
|
14
|
+
self.path = resources.files("pymax._data") / "apk_fingerprints.json"
|
|
15
|
+
self.data = self.load_fingerprints()
|
|
16
|
+
|
|
17
|
+
def load_fingerprints(self) -> Any:
|
|
18
|
+
with self.path.open("r", encoding="utf-8") as f:
|
|
19
|
+
return json.load(f)
|
|
20
|
+
|
|
21
|
+
def generate_fingerprint(
|
|
22
|
+
self,
|
|
23
|
+
version: str,
|
|
24
|
+
device_id: str,
|
|
25
|
+
calls_seed: int,
|
|
26
|
+
arch: str = "arm64-v8a",
|
|
27
|
+
) -> bytes | None:
|
|
28
|
+
data = self.data.get(version)
|
|
29
|
+
if not data:
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
model = ApkBuildFingerprint.model_validate(data)
|
|
33
|
+
|
|
34
|
+
seed_bytes = struct.pack(">q", calls_seed)
|
|
35
|
+
device_bytes = device_id.encode("utf-8")
|
|
36
|
+
|
|
37
|
+
h1 = hashlib.sha256(
|
|
38
|
+
bytes.fromhex(model.certificate_meta_sha256) + seed_bytes + device_bytes
|
|
39
|
+
).digest()
|
|
40
|
+
h2 = hashlib.sha256(
|
|
41
|
+
bytes.fromhex(model.dex_meta_sha256) + seed_bytes + device_bytes
|
|
42
|
+
).digest()
|
|
43
|
+
h3 = hashlib.sha256(
|
|
44
|
+
bytes.fromhex(model.so_meta_sha256[arch]) + seed_bytes + device_bytes
|
|
45
|
+
).digest()
|
|
46
|
+
|
|
47
|
+
return h1 + h2 + h3
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApkBuildFingerprint(BaseModel):
|
|
5
|
+
signature_scheme: str
|
|
6
|
+
certificate_count: int
|
|
7
|
+
certificate_meta_sha256: str
|
|
8
|
+
certificate_sha256: list[str]
|
|
9
|
+
dex_meta_sha256: str
|
|
10
|
+
so_meta_sha256: dict[str, str]
|
|
11
|
+
build_number: int
|
pymax/infra/auth.py
CHANGED
pymax/infra/bots.py
CHANGED
|
@@ -9,14 +9,14 @@ class BotsMixin(IClientProtocol):
|
|
|
9
9
|
async def get_bot_init_data(
|
|
10
10
|
self,
|
|
11
11
|
bot_id: int,
|
|
12
|
-
chat_id: int,
|
|
12
|
+
chat_id: int | None = None,
|
|
13
13
|
start_param: str | None = None,
|
|
14
14
|
) -> InitData:
|
|
15
|
-
"""Получает начальные данные для
|
|
15
|
+
"""Получает начальные данные для запуска бота.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
18
18
|
bot_id: Идентификатор бота.
|
|
19
|
-
chat_id:
|
|
19
|
+
chat_id: Необязательный ID чата, в котором бот будет использоваться.
|
|
20
20
|
start_param: Необязательный параметр, передаваемый при запуске
|
|
21
21
|
бота.
|
|
22
22
|
|
pymax/infra/chat.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from pymax.api.chats import ChannelPermissions
|
|
1
2
|
from pymax.types import Chat, Member, Message
|
|
2
3
|
|
|
3
4
|
from .protocol import IClientProtocol
|
|
@@ -211,6 +212,28 @@ class ChatMixin(IClientProtocol):
|
|
|
211
212
|
"""
|
|
212
213
|
return await self._app.api.chats.get_chat(chat_id)
|
|
213
214
|
|
|
215
|
+
async def get_chat_members(
|
|
216
|
+
self,
|
|
217
|
+
chat_id: int,
|
|
218
|
+
marker: int | None = None,
|
|
219
|
+
count: int = 50,
|
|
220
|
+
) -> tuple[list[Member], int]:
|
|
221
|
+
"""Возвращает страницу участников чата по ID.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
chat_id: ID чата.
|
|
225
|
+
marker: Маркер страницы. Если ``None``, запрашивается первая
|
|
226
|
+
страница.
|
|
227
|
+
count: Максимальное количество участников в ответе.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
``(members, next_marker)``. Если участников больше, чем
|
|
231
|
+
уместилось в ответ, ``next_marker`` ненулевой — передайте его в
|
|
232
|
+
следующий вызов, чтобы получить следующую страницу. ``0``
|
|
233
|
+
означает, что дальше страниц нет.
|
|
234
|
+
"""
|
|
235
|
+
return await self._app.api.chats.get_chat_members(chat_id, marker, count)
|
|
236
|
+
|
|
214
237
|
async def leave_group(self, chat_id: int) -> None:
|
|
215
238
|
"""Выходит из группы.
|
|
216
239
|
|
|
@@ -372,3 +395,18 @@ class ChatMixin(IClientProtocol):
|
|
|
372
395
|
Канал, в который вступил клиент.
|
|
373
396
|
"""
|
|
374
397
|
return await self._app.api.chats.join_channel(link=link)
|
|
398
|
+
|
|
399
|
+
async def add_admin(
|
|
400
|
+
self,
|
|
401
|
+
chat_id: int,
|
|
402
|
+
user_id: int,
|
|
403
|
+
permissions: list[ChannelPermissions],
|
|
404
|
+
) -> None:
|
|
405
|
+
"""Назначает пользователя администратором канала.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
chat_id: ID канала.
|
|
409
|
+
user_id: ID пользователя.
|
|
410
|
+
permissions: Непустой список прав администратора.
|
|
411
|
+
"""
|
|
412
|
+
return await self._app.api.chats.add_admin(chat_id, user_id, permissions)
|
pymax/infra/message.py
CHANGED
|
@@ -3,6 +3,7 @@ from pymax.api.messages.service import SendAttachments
|
|
|
3
3
|
from pymax.types import (
|
|
4
4
|
FileRequest,
|
|
5
5
|
Message,
|
|
6
|
+
PollState,
|
|
6
7
|
ReactionInfo,
|
|
7
8
|
ReadState,
|
|
8
9
|
VideoRequest,
|
|
@@ -17,23 +18,26 @@ class MessageMixin(IClientProtocol):
|
|
|
17
18
|
async def send_message(
|
|
18
19
|
self,
|
|
19
20
|
chat_id: int,
|
|
20
|
-
text: str,
|
|
21
|
+
text: str | None = None,
|
|
21
22
|
reply_to: int | None = None,
|
|
22
23
|
attachments: SendAttachments = None,
|
|
23
24
|
*,
|
|
24
25
|
notify: bool = True,
|
|
25
|
-
) -> Message
|
|
26
|
+
) -> Message:
|
|
26
27
|
"""Отправляет сообщение в чат.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
29
30
|
chat_id: ID чата.
|
|
30
|
-
text: Текст сообщения.
|
|
31
|
+
text: Текст сообщения. Можно не передавать при наличии вложений.
|
|
31
32
|
reply_to: ID сообщения для ответа.
|
|
32
|
-
attachments: Файлы,
|
|
33
|
+
attachments: Файлы, фотографии, видео или опросы для отправки.
|
|
33
34
|
notify: Отправить ли получателям push-уведомление.
|
|
34
35
|
|
|
35
36
|
Returns:
|
|
36
|
-
Отправленное
|
|
37
|
+
Отправленное сообщение.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: Если не переданы ни текст, ни вложения.
|
|
37
41
|
"""
|
|
38
42
|
return await self._app.api.messages.send_message(
|
|
39
43
|
chat_id,
|
|
@@ -69,7 +73,7 @@ class MessageMixin(IClientProtocol):
|
|
|
69
73
|
source_chat_id: int | None = None,
|
|
70
74
|
*,
|
|
71
75
|
notify: bool = True,
|
|
72
|
-
) -> Message
|
|
76
|
+
) -> Message:
|
|
73
77
|
"""Пересылает существующее сообщение в чат.
|
|
74
78
|
|
|
75
79
|
Args:
|
|
@@ -80,7 +84,7 @@ class MessageMixin(IClientProtocol):
|
|
|
80
84
|
notify: Отправить ли получателям push-уведомление.
|
|
81
85
|
|
|
82
86
|
Returns:
|
|
83
|
-
Пересланное
|
|
87
|
+
Пересланное сообщение.
|
|
84
88
|
"""
|
|
85
89
|
return await self._app.api.messages.forward_message(
|
|
86
90
|
chat_id=chat_id,
|
|
@@ -112,7 +116,7 @@ class MessageMixin(IClientProtocol):
|
|
|
112
116
|
self,
|
|
113
117
|
chat_id: int,
|
|
114
118
|
message_id: int,
|
|
115
|
-
text: str,
|
|
119
|
+
text: str | None = None,
|
|
116
120
|
attachments: SendAttachments = None,
|
|
117
121
|
) -> Message:
|
|
118
122
|
"""Редактирует текст и вложения сообщения.
|
|
@@ -121,7 +125,7 @@ class MessageMixin(IClientProtocol):
|
|
|
121
125
|
chat_id: ID чата.
|
|
122
126
|
message_id: ID сообщения.
|
|
123
127
|
text: Новый текст сообщения с поддержкой markdown.
|
|
124
|
-
attachments: Новые файлы,
|
|
128
|
+
attachments: Новые файлы, фотографии, видео или опросы для сообщения.
|
|
125
129
|
|
|
126
130
|
Returns:
|
|
127
131
|
Отредактированное сообщение.
|
|
@@ -145,7 +149,7 @@ class MessageMixin(IClientProtocol):
|
|
|
145
149
|
get_chat: bool = False,
|
|
146
150
|
get_messages: bool = True,
|
|
147
151
|
interactive: bool = False,
|
|
148
|
-
) -> list[Message]
|
|
152
|
+
) -> list[Message]:
|
|
149
153
|
"""Загружает историю сообщений чата.
|
|
150
154
|
|
|
151
155
|
Args:
|
|
@@ -162,7 +166,7 @@ class MessageMixin(IClientProtocol):
|
|
|
162
166
|
interactive: Пометить запрос как интерактивный.
|
|
163
167
|
|
|
164
168
|
Returns:
|
|
165
|
-
|
|
169
|
+
Список сообщений. Если сервер не вернул сообщения, список пуст.
|
|
166
170
|
"""
|
|
167
171
|
return await self._app.api.messages.fetch_history(
|
|
168
172
|
chat_id=chat_id,
|
|
@@ -344,3 +348,28 @@ class MessageMixin(IClientProtocol):
|
|
|
344
348
|
message_id=message_id,
|
|
345
349
|
chat_id=chat_id,
|
|
346
350
|
)
|
|
351
|
+
|
|
352
|
+
async def vote_poll(
|
|
353
|
+
self,
|
|
354
|
+
chat_id: int,
|
|
355
|
+
message_id: int,
|
|
356
|
+
poll_id: int,
|
|
357
|
+
answer_ids: list[int],
|
|
358
|
+
) -> PollState:
|
|
359
|
+
"""Отправляет выбранные ответы в опросе.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
chat_id: ID чата с опросом.
|
|
363
|
+
message_id: ID сообщения с опросом.
|
|
364
|
+
poll_id: ID опроса из ``PollAttachment.poll_id``.
|
|
365
|
+
answer_ids: ID выбранных вариантов ответа.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Обновленное состояние опроса.
|
|
369
|
+
"""
|
|
370
|
+
return await self._app.api.messages.vote_poll(
|
|
371
|
+
chat_id=chat_id,
|
|
372
|
+
message_id=message_id,
|
|
373
|
+
poll_id=poll_id,
|
|
374
|
+
answer_ids=answer_ids,
|
|
375
|
+
)
|
pymax/infra/self.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
+
from pymax.api.self import PrivacySettingsUpdate
|
|
4
|
+
from pymax.files import Photo
|
|
3
5
|
from pymax.types import FolderList, FolderUpdate
|
|
4
6
|
|
|
5
7
|
from .protocol import IClientProtocol
|
|
@@ -19,7 +21,7 @@ class SelfMixin(IClientProtocol):
|
|
|
19
21
|
first_name: str,
|
|
20
22
|
last_name: str | None = None,
|
|
21
23
|
description: str | None = None,
|
|
22
|
-
photo:
|
|
24
|
+
photo: Photo | None = None,
|
|
23
25
|
*,
|
|
24
26
|
photo_token: str | None = None,
|
|
25
27
|
) -> bool:
|
|
@@ -133,3 +135,25 @@ class SelfMixin(IClientProtocol):
|
|
|
133
135
|
``True``, если сервер принял запрос на выход.
|
|
134
136
|
"""
|
|
135
137
|
return await self._app.api.account.logout()
|
|
138
|
+
|
|
139
|
+
def set_presence(self, *, online: bool) -> None:
|
|
140
|
+
"""Устанавливает статус присутствия текущего аккаунта.
|
|
141
|
+
|
|
142
|
+
Статус применяется не мгновенно, а при следующем запросе login/ping.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
online: ``True``, если нужно установить статус "в сети", иначе
|
|
146
|
+
``False``.
|
|
147
|
+
"""
|
|
148
|
+
self._app.api.account.set_presence(online)
|
|
149
|
+
|
|
150
|
+
async def change_profile_settings(self, settings: PrivacySettingsUpdate) -> bool:
|
|
151
|
+
"""Обновляет настройки приватности текущего аккаунта.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
settings: Объект с новыми настройками приватности.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
``True``, если сервер принял запрос.
|
|
158
|
+
"""
|
|
159
|
+
return await self._app.api.account.change_profile_settings(settings=settings)
|
pymax/protocol/enums.py
CHANGED
|
@@ -18,6 +18,7 @@ class Opcode(int, Enum):
|
|
|
18
18
|
RECONNECT = 3
|
|
19
19
|
LOG = 5
|
|
20
20
|
SESSION_INIT = 6
|
|
21
|
+
LOGIN2 = 8
|
|
21
22
|
PROFILE = 16
|
|
22
23
|
AUTH_REQUEST = 17
|
|
23
24
|
AUTH = 18
|
|
@@ -152,6 +153,18 @@ class Opcode(int, Enum):
|
|
|
152
153
|
PROFILE_DELETE = 199
|
|
153
154
|
PROFILE_DELETE_TIME = 200
|
|
154
155
|
TRANSCRIBE_MEDIA = 202
|
|
156
|
+
STORIES_LIST = 208
|
|
157
|
+
STORIES_LIST_BY_OWNER_ID = 209
|
|
158
|
+
STORIES_GET_BY_OWNER_ID = 210
|
|
159
|
+
STORIES_GET_STATS = 211
|
|
160
|
+
STORIES_GET_DETAILED_STATS = 212
|
|
161
|
+
STORIES_REACT = 213
|
|
162
|
+
STORIES_MARK = 214
|
|
163
|
+
STORIES_SEND = 215
|
|
164
|
+
NOTIF_STORIES_UPDATE = 216
|
|
165
|
+
STORIES_EDIT = 217
|
|
166
|
+
STORIES_DELETE = 218
|
|
167
|
+
STORIES_GET_BY_STORY_ID = 220
|
|
155
168
|
ORG_INFO = 256
|
|
156
169
|
CHAT_REACTIONS_SETTINGS_SET = 257
|
|
157
170
|
REACTIONS_SETTINGS_GET_BY_CHAT_ID = 258
|