maxapi-python 2.3.1__py3-none-any.whl → 2.4.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.
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/METADATA +8 -4
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/RECORD +79 -66
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/WHEEL +1 -1
- pymax/__init__.py +9 -2
- pymax/_data/apk_fingerprints.json +767 -0
- pymax/_data/rootca_ssl_rsa2022.crt +33 -0
- pymax/api/auth/enums.py +1 -0
- pymax/api/auth/payloads.py +28 -2
- pymax/api/auth/service.py +100 -3
- pymax/api/chats/__init__.py +1 -0
- pymax/api/chats/enums.py +15 -0
- pymax/api/chats/payloads.py +18 -2
- pymax/api/chats/service.py +43 -1
- pymax/api/messages/__init__.py +1 -1
- pymax/api/messages/payloads.py +21 -7
- pymax/api/messages/service.py +161 -24
- 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 +39 -1
- pymax/api/uploads/service.py +247 -55
- pymax/app.py +91 -18
- pymax/auth/__init__.py +2 -0
- pymax/auth/exceptions.py +9 -0
- pymax/auth/providers.py +6 -2
- pymax/auth/sms.py +11 -1
- pymax/base.py +90 -8
- pymax/client.py +39 -10
- pymax/client_web.py +17 -14
- pymax/config.py +98 -19
- 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/base.py +58 -1
- pymax/files/video.py +51 -1
- pymax/files/voice.py +53 -0
- pymax/fingerprint/__init__.py +1 -0
- pymax/fingerprint/fingerprint.py +30 -0
- pymax/fingerprint/models.py +12 -0
- pymax/infra/auth.py +4 -0
- pymax/infra/bots.py +3 -3
- pymax/infra/chat.py +47 -5
- pymax/infra/message.py +49 -14
- pymax/infra/protocol.py +5 -1
- pymax/infra/self.py +25 -1
- pymax/protocol/enums.py +13 -0
- pymax/protocol/tcp/payload.py +21 -1
- pymax/session/__init__.py +1 -1
- pymax/session/models.py +30 -0
- pymax/session/protocol.py +33 -7
- pymax/session/store.py +73 -5
- pymax/transport/tcp.py +17 -4
- 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/call.py +7 -1
- pymax/types/domain/attachments/contact.py +3 -3
- pymax/types/domain/attachments/control.py +3 -0
- pymax/types/domain/attachments/enums.py +33 -1
- pymax/types/domain/attachments/poll.py +111 -0
- pymax/types/domain/attachments/sticker.py +1 -1
- pymax/types/domain/attachments/video.py +32 -11
- pymax/types/domain/chat.py +9 -9
- pymax/types/domain/enums.py +7 -0
- pymax/types/domain/handshake.py +16 -0
- pymax/types/domain/login.py +30 -1
- pymax/types/domain/message.py +108 -27
- 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
- pymax/versions/__init__.py +2 -0
- pymax/versions/catalog.py +104 -0
- pymax/versions/exceptions.py +10 -0
- {maxapi_python-2.3.1.dist-info → maxapi_python-2.4.1.dist-info}/licenses/LICENSE +0 -0
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/base.py
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
3
|
from collections.abc import AsyncGenerator
|
|
4
|
+
from io import BytesIO
|
|
4
5
|
|
|
5
6
|
import aiofiles
|
|
6
7
|
import aiohttp
|
|
7
8
|
|
|
9
|
+
try:
|
|
10
|
+
from tinytag import TinyTag
|
|
11
|
+
except ImportError:
|
|
12
|
+
TinyTag = None
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
class BaseFile(ABC):
|
|
10
16
|
def __init__(
|
|
@@ -56,7 +62,7 @@ class BaseFile(ABC):
|
|
|
56
62
|
|
|
57
63
|
if self.url:
|
|
58
64
|
async with aiohttp.ClientSession() as session: # noqa: SIM117
|
|
59
|
-
async with session.head(self.url) as resp:
|
|
65
|
+
async with session.head(self.url, allow_redirects=True) as resp:
|
|
60
66
|
return int(resp.headers["Content-Length"])
|
|
61
67
|
else:
|
|
62
68
|
raise ValueError("Path or Url must be provided")
|
|
@@ -85,3 +91,54 @@ class BaseFile(ABC):
|
|
|
85
91
|
resp.raise_for_status()
|
|
86
92
|
async for chunk in resp.content.iter_chunked(size):
|
|
87
93
|
yield chunk
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TimedMediaFile(BaseFile):
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
raw: bytes | None = None,
|
|
100
|
+
*,
|
|
101
|
+
path: str | None,
|
|
102
|
+
url: str | None,
|
|
103
|
+
name: str | None,
|
|
104
|
+
duration: int | None,
|
|
105
|
+
) -> None:
|
|
106
|
+
self.duration = duration
|
|
107
|
+
super().__init__(raw, path=path, url=url, name=name)
|
|
108
|
+
|
|
109
|
+
async def get_duration(self) -> int:
|
|
110
|
+
if self.duration is not None:
|
|
111
|
+
return self.duration
|
|
112
|
+
|
|
113
|
+
if not TinyTag:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"Automatic video duration detection requires the 'video' extra. "
|
|
116
|
+
"Install it with `uv add 'maxapi-python[video]'` "
|
|
117
|
+
"or pass duration manually."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if self.raw:
|
|
121
|
+
tag = TinyTag.get(
|
|
122
|
+
filename=self.name,
|
|
123
|
+
file_obj=BytesIO(self.raw),
|
|
124
|
+
tags=False,
|
|
125
|
+
duration=True,
|
|
126
|
+
)
|
|
127
|
+
elif self.path:
|
|
128
|
+
tag = TinyTag.get(
|
|
129
|
+
filename=self.path,
|
|
130
|
+
tags=False,
|
|
131
|
+
duration=True,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
tag = TinyTag.get(
|
|
135
|
+
filename=self.name,
|
|
136
|
+
file_obj=BytesIO(await self.read()),
|
|
137
|
+
tags=False,
|
|
138
|
+
duration=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if tag.duration is None:
|
|
142
|
+
raise ValueError("Failed to determine video duration.")
|
|
143
|
+
|
|
144
|
+
return round(tag.duration * 1000)
|
pymax/files/video.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from .base import BaseFile
|
|
4
|
+
from .base import BaseFile, TimedMediaFile
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Video(BaseFile):
|
|
@@ -72,3 +72,53 @@ class Video(BaseFile):
|
|
|
72
72
|
Async generator с байтами.
|
|
73
73
|
"""
|
|
74
74
|
return super().iter_chunks(size)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class VideoNote(TimedMediaFile):
|
|
78
|
+
"""Круглое видеосообщение для отправки.
|
|
79
|
+
|
|
80
|
+
Принимает те же источники, что и ``Video``. Длительность задается в
|
|
81
|
+
миллисекундах. Если она не передана, требуется extra ``video`` для
|
|
82
|
+
автоматического определения длительности.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
raw: Байты видеосообщения.
|
|
86
|
+
url: URL видеосообщения.
|
|
87
|
+
path: Локальный путь к видеосообщению.
|
|
88
|
+
name: Имя файла. Обязательно для ``raw``.
|
|
89
|
+
duration: Длительность в миллисекундах. ``None`` включает
|
|
90
|
+
автоматическое определение через extra ``video``.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
RuntimeError: При определении длительности без extra ``video``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
raw: bytes | None = None,
|
|
99
|
+
*,
|
|
100
|
+
url: str | None = None,
|
|
101
|
+
path: str | None = None,
|
|
102
|
+
name: str | None = None,
|
|
103
|
+
duration: int | None = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
self.duration = duration
|
|
106
|
+
self.name: str = name or ""
|
|
107
|
+
if not self.name and path:
|
|
108
|
+
self.name = Path(path).name
|
|
109
|
+
elif not self.name and url:
|
|
110
|
+
self.name = Path(url).name
|
|
111
|
+
|
|
112
|
+
if not self.name:
|
|
113
|
+
raise ValueError("Either name, url or path must be provided.")
|
|
114
|
+
|
|
115
|
+
super().__init__(raw, url=url, path=path, name=self.name, duration=self.duration)
|
|
116
|
+
|
|
117
|
+
async def read(self) -> bytes:
|
|
118
|
+
return await super().read()
|
|
119
|
+
|
|
120
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
121
|
+
return super().iter_chunks(size)
|
|
122
|
+
|
|
123
|
+
async def size(self) -> int:
|
|
124
|
+
return await super().size()
|
pymax/files/voice.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import TimedMediaFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Voice(TimedMediaFile):
|
|
8
|
+
"""Голосовое сообщение в формате OGG для отправки.
|
|
9
|
+
|
|
10
|
+
Принимает ``path``, ``url`` или ``raw``. Для ``raw`` необходимо явно
|
|
11
|
+
передать имя файла. Длительность задается в миллисекундах; если она не
|
|
12
|
+
передана, требуется extra ``video`` для автоматического определения.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
raw: Байты голосового сообщения.
|
|
16
|
+
path: Локальный путь к OGG-файлу.
|
|
17
|
+
url: URL OGG-файла.
|
|
18
|
+
name: Имя файла. Обязательно для ``raw``.
|
|
19
|
+
duration: Длительность в миллисекундах. ``None`` включает
|
|
20
|
+
автоматическое определение через extra ``video``.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
RuntimeError: При определении длительности без extra ``video``.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
raw: bytes | None = None,
|
|
29
|
+
*,
|
|
30
|
+
path: str | None = None,
|
|
31
|
+
url: str | None = None,
|
|
32
|
+
name: str | None = None,
|
|
33
|
+
duration: int | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.name: str = name or ""
|
|
36
|
+
self.duration = duration
|
|
37
|
+
if not self.name and path:
|
|
38
|
+
self.name = Path(path).name
|
|
39
|
+
elif not self.name and url:
|
|
40
|
+
self.name = Path(url).name
|
|
41
|
+
|
|
42
|
+
if not self.name:
|
|
43
|
+
raise ValueError("Either name, url or path must be provided.")
|
|
44
|
+
super().__init__(raw=raw, url=url, path=path, name=self.name, duration=self.duration)
|
|
45
|
+
|
|
46
|
+
async def read(self) -> bytes:
|
|
47
|
+
return await super().read()
|
|
48
|
+
|
|
49
|
+
async def size(self) -> int:
|
|
50
|
+
return await super().size()
|
|
51
|
+
|
|
52
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
53
|
+
return super().iter_chunks(size)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .fingerprint import FingerprintGenerator
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import struct
|
|
3
|
+
|
|
4
|
+
from .models import ApkBuildFingerprint
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FingerprintGenerator:
|
|
8
|
+
def __init__(self, version_data: ApkBuildFingerprint) -> None:
|
|
9
|
+
self.data = version_data
|
|
10
|
+
|
|
11
|
+
def generate_fingerprint(
|
|
12
|
+
self,
|
|
13
|
+
device_id: str,
|
|
14
|
+
calls_seed: int,
|
|
15
|
+
arch: str = "arm64-v8a",
|
|
16
|
+
) -> bytes | None:
|
|
17
|
+
seed_bytes = struct.pack(">q", calls_seed)
|
|
18
|
+
device_bytes = device_id.encode("utf-8")
|
|
19
|
+
|
|
20
|
+
h1 = hashlib.sha256(
|
|
21
|
+
bytes.fromhex(self.data.certificate_meta_sha256) + seed_bytes + device_bytes
|
|
22
|
+
).digest()
|
|
23
|
+
h2 = hashlib.sha256(
|
|
24
|
+
bytes.fromhex(self.data.dex_meta_sha256) + seed_bytes + device_bytes
|
|
25
|
+
).digest()
|
|
26
|
+
h3 = hashlib.sha256(
|
|
27
|
+
bytes.fromhex(self.data.so_meta_sha256[arch]) + seed_bytes + device_bytes
|
|
28
|
+
).digest()
|
|
29
|
+
|
|
30
|
+
return h1 + h2 + h3
|
|
@@ -0,0 +1,12 @@
|
|
|
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_arm64_v8a: str
|
|
11
|
+
so_meta_sha256: dict[str, str]
|
|
12
|
+
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,5 @@
|
|
|
1
|
+
from pymax.api.chats import ChannelPermissions
|
|
2
|
+
from pymax.files import Photo
|
|
1
3
|
from pymax.types import Chat, Member, Message
|
|
2
4
|
|
|
3
5
|
from .protocol import IClientProtocol
|
|
@@ -130,20 +132,23 @@ class ChatMixin(IClientProtocol):
|
|
|
130
132
|
async def change_group_profile(
|
|
131
133
|
self,
|
|
132
134
|
chat_id: int,
|
|
133
|
-
name: str | None,
|
|
135
|
+
name: str | None = None,
|
|
134
136
|
description: str | None = None,
|
|
137
|
+
photo: Photo | None = None,
|
|
135
138
|
) -> None:
|
|
136
|
-
"""Обновляет
|
|
139
|
+
"""Обновляет название, описание или фотографию группы.
|
|
140
|
+
|
|
141
|
+
``None`` исключает соответствующее поле из запроса. Фотография
|
|
142
|
+
автоматически загружается перед обновлением профиля.
|
|
137
143
|
|
|
138
144
|
Args:
|
|
139
145
|
chat_id: ID группы.
|
|
140
146
|
name: Новое название.
|
|
141
147
|
description: Новое описание.
|
|
148
|
+
photo: Новая фотография группы.
|
|
142
149
|
"""
|
|
143
150
|
await self._app.api.chats.change_group_profile(
|
|
144
|
-
chat_id=chat_id,
|
|
145
|
-
name=name,
|
|
146
|
-
description=description,
|
|
151
|
+
chat_id=chat_id, name=name, description=description, photo=photo
|
|
147
152
|
)
|
|
148
153
|
|
|
149
154
|
async def join_group(self, link: str) -> Chat:
|
|
@@ -211,6 +216,28 @@ class ChatMixin(IClientProtocol):
|
|
|
211
216
|
"""
|
|
212
217
|
return await self._app.api.chats.get_chat(chat_id)
|
|
213
218
|
|
|
219
|
+
async def get_chat_members(
|
|
220
|
+
self,
|
|
221
|
+
chat_id: int,
|
|
222
|
+
marker: int | None = None,
|
|
223
|
+
count: int = 50,
|
|
224
|
+
) -> tuple[list[Member], int]:
|
|
225
|
+
"""Возвращает страницу участников чата по ID.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
chat_id: ID чата.
|
|
229
|
+
marker: Маркер страницы. Если ``None``, запрашивается первая
|
|
230
|
+
страница.
|
|
231
|
+
count: Максимальное количество участников в ответе.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
``(members, next_marker)``. Если участников больше, чем
|
|
235
|
+
уместилось в ответ, ``next_marker`` ненулевой — передайте его в
|
|
236
|
+
следующий вызов, чтобы получить следующую страницу. ``0``
|
|
237
|
+
означает, что дальше страниц нет.
|
|
238
|
+
"""
|
|
239
|
+
return await self._app.api.chats.get_chat_members(chat_id, marker, count)
|
|
240
|
+
|
|
214
241
|
async def leave_group(self, chat_id: int) -> None:
|
|
215
242
|
"""Выходит из группы.
|
|
216
243
|
|
|
@@ -372,3 +399,18 @@ class ChatMixin(IClientProtocol):
|
|
|
372
399
|
Канал, в который вступил клиент.
|
|
373
400
|
"""
|
|
374
401
|
return await self._app.api.chats.join_channel(link=link)
|
|
402
|
+
|
|
403
|
+
async def add_admin(
|
|
404
|
+
self,
|
|
405
|
+
chat_id: int,
|
|
406
|
+
user_id: int,
|
|
407
|
+
permissions: list[ChannelPermissions],
|
|
408
|
+
) -> None:
|
|
409
|
+
"""Назначает пользователя администратором канала.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
chat_id: ID канала.
|
|
413
|
+
user_id: ID пользователя.
|
|
414
|
+
permissions: Непустой список прав администратора.
|
|
415
|
+
"""
|
|
416
|
+
return await self._app.api.chats.add_admin(chat_id, user_id, permissions)
|
pymax/infra/message.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
from pymax.api.messages import DateTimeUnion
|
|
1
2
|
from pymax.api.messages.enums import ItemType
|
|
2
3
|
from pymax.api.messages.service import SendAttachments
|
|
3
4
|
from pymax.types import (
|
|
4
5
|
FileRequest,
|
|
5
6
|
Message,
|
|
7
|
+
PollState,
|
|
6
8
|
ReactionInfo,
|
|
7
9
|
ReadState,
|
|
8
10
|
VideoRequest,
|
|
@@ -17,23 +19,30 @@ class MessageMixin(IClientProtocol):
|
|
|
17
19
|
async def send_message(
|
|
18
20
|
self,
|
|
19
21
|
chat_id: int,
|
|
20
|
-
text: str,
|
|
22
|
+
text: str | None = None,
|
|
21
23
|
reply_to: int | None = None,
|
|
22
24
|
attachments: SendAttachments = None,
|
|
23
25
|
*,
|
|
24
26
|
notify: bool = True,
|
|
25
|
-
|
|
27
|
+
send_at: DateTimeUnion | None = None,
|
|
28
|
+
) -> Message:
|
|
26
29
|
"""Отправляет сообщение в чат.
|
|
27
30
|
|
|
28
31
|
Args:
|
|
29
32
|
chat_id: ID чата.
|
|
30
|
-
text: Текст сообщения.
|
|
33
|
+
text: Текст сообщения. Можно не передавать при наличии вложений.
|
|
31
34
|
reply_to: ID сообщения для ответа.
|
|
32
|
-
attachments: Файлы,
|
|
35
|
+
attachments: Файлы, фотографии, видео или опросы для отправки.
|
|
33
36
|
notify: Отправить ли получателям push-уведомление.
|
|
37
|
+
send_at: Время отложенной отправки: ``datetime`` для абсолютного
|
|
38
|
+
времени, ``timedelta`` для задержки или Unix time в секундах
|
|
39
|
+
как ``int``. ``None`` и ``0`` отправляют сообщение сразу.
|
|
34
40
|
|
|
35
41
|
Returns:
|
|
36
|
-
Отправленное
|
|
42
|
+
Отправленное сообщение.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: Если не переданы ни текст, ни вложения.
|
|
37
46
|
"""
|
|
38
47
|
return await self._app.api.messages.send_message(
|
|
39
48
|
chat_id,
|
|
@@ -41,6 +50,7 @@ class MessageMixin(IClientProtocol):
|
|
|
41
50
|
reply_to,
|
|
42
51
|
attachments,
|
|
43
52
|
notify=notify,
|
|
53
|
+
send_at=send_at,
|
|
44
54
|
)
|
|
45
55
|
|
|
46
56
|
async def get_message(
|
|
@@ -69,7 +79,7 @@ class MessageMixin(IClientProtocol):
|
|
|
69
79
|
source_chat_id: int | None = None,
|
|
70
80
|
*,
|
|
71
81
|
notify: bool = True,
|
|
72
|
-
) -> Message
|
|
82
|
+
) -> Message:
|
|
73
83
|
"""Пересылает существующее сообщение в чат.
|
|
74
84
|
|
|
75
85
|
Args:
|
|
@@ -80,7 +90,7 @@ class MessageMixin(IClientProtocol):
|
|
|
80
90
|
notify: Отправить ли получателям push-уведомление.
|
|
81
91
|
|
|
82
92
|
Returns:
|
|
83
|
-
Пересланное
|
|
93
|
+
Пересланное сообщение.
|
|
84
94
|
"""
|
|
85
95
|
return await self._app.api.messages.forward_message(
|
|
86
96
|
chat_id=chat_id,
|
|
@@ -112,7 +122,7 @@ class MessageMixin(IClientProtocol):
|
|
|
112
122
|
self,
|
|
113
123
|
chat_id: int,
|
|
114
124
|
message_id: int,
|
|
115
|
-
text: str,
|
|
125
|
+
text: str | None = None,
|
|
116
126
|
attachments: SendAttachments = None,
|
|
117
127
|
) -> Message:
|
|
118
128
|
"""Редактирует текст и вложения сообщения.
|
|
@@ -121,7 +131,7 @@ class MessageMixin(IClientProtocol):
|
|
|
121
131
|
chat_id: ID чата.
|
|
122
132
|
message_id: ID сообщения.
|
|
123
133
|
text: Новый текст сообщения с поддержкой markdown.
|
|
124
|
-
attachments: Новые файлы,
|
|
134
|
+
attachments: Новые файлы, фотографии, видео или опросы для сообщения.
|
|
125
135
|
|
|
126
136
|
Returns:
|
|
127
137
|
Отредактированное сообщение.
|
|
@@ -145,7 +155,7 @@ class MessageMixin(IClientProtocol):
|
|
|
145
155
|
get_chat: bool = False,
|
|
146
156
|
get_messages: bool = True,
|
|
147
157
|
interactive: bool = False,
|
|
148
|
-
) -> list[Message]
|
|
158
|
+
) -> list[Message]:
|
|
149
159
|
"""Загружает историю сообщений чата.
|
|
150
160
|
|
|
151
161
|
Args:
|
|
@@ -162,7 +172,7 @@ class MessageMixin(IClientProtocol):
|
|
|
162
172
|
interactive: Пометить запрос как интерактивный.
|
|
163
173
|
|
|
164
174
|
Returns:
|
|
165
|
-
|
|
175
|
+
Список сообщений. Если сервер не вернул сообщения, список пуст.
|
|
166
176
|
"""
|
|
167
177
|
return await self._app.api.messages.fetch_history(
|
|
168
178
|
chat_id=chat_id,
|
|
@@ -268,7 +278,7 @@ class MessageMixin(IClientProtocol):
|
|
|
268
278
|
async def add_reaction(
|
|
269
279
|
self,
|
|
270
280
|
chat_id: int,
|
|
271
|
-
message_id:
|
|
281
|
+
message_id: int,
|
|
272
282
|
reaction: str,
|
|
273
283
|
) -> ReactionInfo | None:
|
|
274
284
|
"""Добавляет реакцию к сообщению.
|
|
@@ -290,7 +300,7 @@ class MessageMixin(IClientProtocol):
|
|
|
290
300
|
async def get_reactions(
|
|
291
301
|
self,
|
|
292
302
|
chat_id: int,
|
|
293
|
-
message_ids: list[
|
|
303
|
+
message_ids: list[int],
|
|
294
304
|
) -> dict[str, ReactionInfo] | None:
|
|
295
305
|
"""Возвращает реакции для нескольких сообщений.
|
|
296
306
|
|
|
@@ -310,7 +320,7 @@ class MessageMixin(IClientProtocol):
|
|
|
310
320
|
async def remove_reaction(
|
|
311
321
|
self,
|
|
312
322
|
chat_id: int,
|
|
313
|
-
message_id:
|
|
323
|
+
message_id: int,
|
|
314
324
|
) -> ReactionInfo | None:
|
|
315
325
|
"""Удаляет свою реакцию с сообщения.
|
|
316
326
|
|
|
@@ -344,3 +354,28 @@ class MessageMixin(IClientProtocol):
|
|
|
344
354
|
message_id=message_id,
|
|
345
355
|
chat_id=chat_id,
|
|
346
356
|
)
|
|
357
|
+
|
|
358
|
+
async def vote_poll(
|
|
359
|
+
self,
|
|
360
|
+
chat_id: int,
|
|
361
|
+
message_id: int,
|
|
362
|
+
poll_id: int,
|
|
363
|
+
answer_ids: list[int],
|
|
364
|
+
) -> PollState:
|
|
365
|
+
"""Отправляет выбранные ответы в опросе.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
chat_id: ID чата с опросом.
|
|
369
|
+
message_id: ID сообщения с опросом.
|
|
370
|
+
poll_id: ID опроса из ``PollAttachment.poll_id``.
|
|
371
|
+
answer_ids: ID выбранных вариантов ответа.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Обновленное состояние опроса.
|
|
375
|
+
"""
|
|
376
|
+
return await self._app.api.messages.vote_poll(
|
|
377
|
+
chat_id=chat_id,
|
|
378
|
+
message_id=message_id,
|
|
379
|
+
poll_id=poll_id,
|
|
380
|
+
answer_ids=answer_ids,
|
|
381
|
+
)
|
pymax/infra/protocol.py
CHANGED
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
|