maxion 0.1.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.
- maxion/__init__.py +82 -0
- maxion/client.py +508 -0
- maxion/enums.py +125 -0
- maxion/errors.py +73 -0
- maxion/filters.py +228 -0
- maxion/handlers.py +97 -0
- maxion/parser.py +103 -0
- maxion/raw/__init__.py +125 -0
- maxion/raw/client.py +552 -0
- maxion/raw/const.py +70 -0
- maxion/raw/device.py +247 -0
- maxion/raw/enums.py +222 -0
- maxion/raw/errors.py +100 -0
- maxion/raw/events.py +349 -0
- maxion/raw/filters.py +213 -0
- maxion/raw/methods/__init__.py +47 -0
- maxion/raw/methods/assets.py +139 -0
- maxion/raw/methods/auth.py +438 -0
- maxion/raw/methods/base.py +39 -0
- maxion/raw/methods/calls.py +218 -0
- maxion/raw/methods/chats.py +689 -0
- maxion/raw/methods/contacts.py +172 -0
- maxion/raw/methods/media.py +272 -0
- maxion/raw/methods/messages.py +471 -0
- maxion/raw/methods/misc.py +54 -0
- maxion/raw/methods/profile.py +131 -0
- maxion/raw/methods/reactions.py +94 -0
- maxion/raw/methods/stories.py +214 -0
- maxion/raw/opcodes.py +269 -0
- maxion/raw/protocol.py +190 -0
- maxion/raw/router.py +135 -0
- maxion/raw/session.py +84 -0
- maxion/raw/transport/__init__.py +17 -0
- maxion/raw/transport/base.py +47 -0
- maxion/raw/transport/tcp.py +105 -0
- maxion/raw/transport/ws.py +100 -0
- maxion/raw/types/__init__.py +66 -0
- maxion/raw/types/attach.py +240 -0
- maxion/raw/types/base.py +98 -0
- maxion/raw/types/chat.py +239 -0
- maxion/raw/types/message.py +210 -0
- maxion/raw/types/misc.py +192 -0
- maxion/raw/types/user.py +198 -0
- maxion/raw/utils.py +236 -0
- maxion/types.py +447 -0
- maxion-0.1.0.dist-info/METADATA +355 -0
- maxion-0.1.0.dist-info/RECORD +50 -0
- maxion-0.1.0.dist-info/WHEEL +5 -0
- maxion-0.1.0.dist-info/licenses/LICENSE +21 -0
- maxion-0.1.0.dist-info/top_level.txt +1 -0
maxion/__init__.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""maxion — библиотека-клиент мессенджера MAX.
|
|
2
|
+
|
|
3
|
+
Работа от лица обычного аккаунта (юзербот), а не через Bot API::
|
|
4
|
+
|
|
5
|
+
from maxion import Client, filters
|
|
6
|
+
|
|
7
|
+
app = Client("my_account", phone_number="+79991234567")
|
|
8
|
+
|
|
9
|
+
@app.on_message(filters.command("start") & filters.private)
|
|
10
|
+
async def start(client, message):
|
|
11
|
+
await message.reply("привет")
|
|
12
|
+
|
|
13
|
+
app.run()
|
|
14
|
+
|
|
15
|
+
Слоя два:
|
|
16
|
+
|
|
17
|
+
* удобный — :class:`Client`, :mod:`maxion.filters`, :mod:`maxion.types`:
|
|
18
|
+
обработчики с фильтрами, модели с связанными методами, разметка текста;
|
|
19
|
+
* низкий — :mod:`maxion.raw`: все 153 опкода внутреннего протокола, кадры и
|
|
20
|
+
транспорты, сверенные с APK ``ru.oneme.app`` 26.29.1. Доступен как
|
|
21
|
+
``app.raw``, произвольный опкод — через ``app.invoke()``.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from . import enums, errors, filters, handlers, raw, types
|
|
25
|
+
from .client import Client, ContinuePropagation, StopPropagation, compose, idle
|
|
26
|
+
from .enums import (
|
|
27
|
+
ChatAction,
|
|
28
|
+
ChatMemberStatus,
|
|
29
|
+
ChatType,
|
|
30
|
+
MessageEntityType,
|
|
31
|
+
ParseMode,
|
|
32
|
+
UserStatus,
|
|
33
|
+
)
|
|
34
|
+
from .errors import FloodWait, RPCError, SessionPasswordNeeded, Unauthorized
|
|
35
|
+
from .handlers import (
|
|
36
|
+
DeletedMessagesHandler,
|
|
37
|
+
EditedMessageHandler,
|
|
38
|
+
MessageHandler,
|
|
39
|
+
RawUpdateHandler,
|
|
40
|
+
)
|
|
41
|
+
from .types import Chat, ChatMember, Dialog, Message, MessageEntity, User
|
|
42
|
+
|
|
43
|
+
__version__ = "0.1.0"
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Client",
|
|
47
|
+
"idle",
|
|
48
|
+
"compose",
|
|
49
|
+
"StopPropagation",
|
|
50
|
+
"ContinuePropagation",
|
|
51
|
+
# модули
|
|
52
|
+
"filters",
|
|
53
|
+
"types",
|
|
54
|
+
"enums",
|
|
55
|
+
"errors",
|
|
56
|
+
"handlers",
|
|
57
|
+
"raw",
|
|
58
|
+
# типы
|
|
59
|
+
"User",
|
|
60
|
+
"Chat",
|
|
61
|
+
"Message",
|
|
62
|
+
"MessageEntity",
|
|
63
|
+
"ChatMember",
|
|
64
|
+
"Dialog",
|
|
65
|
+
# перечисления
|
|
66
|
+
"ChatType",
|
|
67
|
+
"ParseMode",
|
|
68
|
+
"ChatAction",
|
|
69
|
+
"MessageEntityType",
|
|
70
|
+
"UserStatus",
|
|
71
|
+
"ChatMemberStatus",
|
|
72
|
+
# ошибки
|
|
73
|
+
"RPCError",
|
|
74
|
+
"FloodWait",
|
|
75
|
+
"Unauthorized",
|
|
76
|
+
"SessionPasswordNeeded",
|
|
77
|
+
# обработчики
|
|
78
|
+
"MessageHandler",
|
|
79
|
+
"EditedMessageHandler",
|
|
80
|
+
"DeletedMessagesHandler",
|
|
81
|
+
"RawUpdateHandler",
|
|
82
|
+
]
|
maxion/client.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Client — высокоуровневый клиент MAX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, AsyncIterator, Callable, Sequence
|
|
12
|
+
|
|
13
|
+
from .enums import ChatAction, ChatMemberStatus, ParseMode
|
|
14
|
+
from .filters import Filter
|
|
15
|
+
from .handlers import (
|
|
16
|
+
DeletedMessagesHandler,
|
|
17
|
+
EditedMessageHandler,
|
|
18
|
+
Handler,
|
|
19
|
+
MessageHandler,
|
|
20
|
+
RawUpdateHandler,
|
|
21
|
+
)
|
|
22
|
+
from .parser import parse
|
|
23
|
+
from .raw.client import MaxClient
|
|
24
|
+
from .raw.device import Device
|
|
25
|
+
from .types import Chat, ChatMember, Dialog, Message, User
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StopPropagation(Exception):
|
|
31
|
+
"""Прекратить обработку события целиком."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ContinuePropagation(Exception):
|
|
35
|
+
"""Передать событие следующему обработчику в той же группе."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Client:
|
|
39
|
+
"""Высокоуровневый клиент MAX.
|
|
40
|
+
|
|
41
|
+
::
|
|
42
|
+
|
|
43
|
+
app = Client("my_account", phone_number="+79991234567")
|
|
44
|
+
|
|
45
|
+
@app.on_message(filters.command("start") & filters.private)
|
|
46
|
+
async def start(client, message):
|
|
47
|
+
await message.reply("привет")
|
|
48
|
+
|
|
49
|
+
app.run()
|
|
50
|
+
|
|
51
|
+
Низкоуровневый клиент со всеми 153 опкодами доступен как :attr:`raw`.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
name: str = "my_account",
|
|
57
|
+
*,
|
|
58
|
+
phone_number: str | None = None,
|
|
59
|
+
password: str | None = None,
|
|
60
|
+
workdir: str | os.PathLike[str] = ".",
|
|
61
|
+
transport: str = "tcp",
|
|
62
|
+
device_model: str | None = None,
|
|
63
|
+
app_version: str | None = None,
|
|
64
|
+
parse_mode: ParseMode = ParseMode.DEFAULT,
|
|
65
|
+
device: Device | str | None = None,
|
|
66
|
+
**kwargs: Any,
|
|
67
|
+
):
|
|
68
|
+
self.name = name
|
|
69
|
+
self.phone_number = phone_number
|
|
70
|
+
self.password = password
|
|
71
|
+
self.workdir = Path(workdir)
|
|
72
|
+
self.parse_mode = parse_mode
|
|
73
|
+
|
|
74
|
+
if device is None:
|
|
75
|
+
device = Device.for_transport(
|
|
76
|
+
"ANDROID" if transport in ("tcp", "tls", "mobile", "app") else "WEB"
|
|
77
|
+
)
|
|
78
|
+
elif isinstance(device, str):
|
|
79
|
+
device = Device.for_transport(device.upper())
|
|
80
|
+
if device_model:
|
|
81
|
+
device.device_name = device_model
|
|
82
|
+
if app_version:
|
|
83
|
+
device.app_version = app_version
|
|
84
|
+
|
|
85
|
+
self._raw = MaxClient(
|
|
86
|
+
self.workdir / f"{name}.session",
|
|
87
|
+
transport=transport,
|
|
88
|
+
device=device,
|
|
89
|
+
**kwargs,
|
|
90
|
+
)
|
|
91
|
+
self._groups: dict[int, list[Handler]] = defaultdict(list)
|
|
92
|
+
self._started = False
|
|
93
|
+
|
|
94
|
+
self._raw.router.on("raw")(self._feed)
|
|
95
|
+
|
|
96
|
+
# --- доступ к слоям --------------------------------------------------
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def raw(self) -> MaxClient:
|
|
100
|
+
"""Низкоуровневый клиент: все опкоды, транспорт, сессия."""
|
|
101
|
+
return self._raw
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def me(self) -> User | None:
|
|
105
|
+
"""Свой профиль, если уже вошли."""
|
|
106
|
+
return User(self, self._raw.me) if self._raw.me else None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_connected(self) -> bool:
|
|
110
|
+
return self._raw.is_connected
|
|
111
|
+
|
|
112
|
+
# --- жизненный цикл ---------------------------------------------------
|
|
113
|
+
|
|
114
|
+
async def start(self) -> "Client":
|
|
115
|
+
"""Подключается и логинится; повторный вызов безвреден."""
|
|
116
|
+
if self._started:
|
|
117
|
+
return self
|
|
118
|
+
await self._raw.connect()
|
|
119
|
+
await self._raw.start(phone=self.phone_number, password=self.password)
|
|
120
|
+
self._started = True
|
|
121
|
+
log.info("Вошли как %s", self._raw.me.name if self._raw.me else "?")
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
async def stop(self) -> "Client":
|
|
125
|
+
await self._raw.disconnect()
|
|
126
|
+
self._started = False
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> "Client":
|
|
130
|
+
return await self.start()
|
|
131
|
+
|
|
132
|
+
async def __aexit__(self, *exc_info) -> None:
|
|
133
|
+
await self.stop()
|
|
134
|
+
|
|
135
|
+
def run(self, coroutine=None) -> Any:
|
|
136
|
+
"""Блокирующий запуск: поднимает клиент и держит его до остановки."""
|
|
137
|
+
|
|
138
|
+
async def main():
|
|
139
|
+
await self.start()
|
|
140
|
+
try:
|
|
141
|
+
if coroutine is not None:
|
|
142
|
+
return await coroutine
|
|
143
|
+
await self._raw.run_until_disconnected()
|
|
144
|
+
finally:
|
|
145
|
+
if self._started:
|
|
146
|
+
await self.stop()
|
|
147
|
+
|
|
148
|
+
return asyncio.run(main())
|
|
149
|
+
|
|
150
|
+
async def idle(self) -> None:
|
|
151
|
+
"""Ждёт, пока соединение живо."""
|
|
152
|
+
await self._raw.run_until_disconnected()
|
|
153
|
+
|
|
154
|
+
# --- обработчики ------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def add_handler(self, handler: Handler, group: int = 0) -> Handler:
|
|
157
|
+
"""Регистрирует обработчик в группе (меньше — раньше)."""
|
|
158
|
+
self._groups[group].append(handler)
|
|
159
|
+
return handler
|
|
160
|
+
|
|
161
|
+
def remove_handler(self, handler: Handler, group: int = 0) -> None:
|
|
162
|
+
if handler in self._groups.get(group, []):
|
|
163
|
+
self._groups[group].remove(handler)
|
|
164
|
+
|
|
165
|
+
def _decorator(self, handler_cls: type[Handler]):
|
|
166
|
+
def outer(filters: Filter | None = None, group: int = 0):
|
|
167
|
+
def decorator(func):
|
|
168
|
+
self.add_handler(handler_cls(func, filters), group)
|
|
169
|
+
return func
|
|
170
|
+
|
|
171
|
+
return decorator
|
|
172
|
+
|
|
173
|
+
return outer
|
|
174
|
+
|
|
175
|
+
def on_message(self, filters: Filter | None = None, group: int = 0):
|
|
176
|
+
return self._decorator(MessageHandler)(filters, group)
|
|
177
|
+
|
|
178
|
+
def on_edited_message(self, filters: Filter | None = None, group: int = 0):
|
|
179
|
+
return self._decorator(EditedMessageHandler)(filters, group)
|
|
180
|
+
|
|
181
|
+
def on_deleted_messages(self, filters: Filter | None = None, group: int = 0):
|
|
182
|
+
return self._decorator(DeletedMessagesHandler)(filters, group)
|
|
183
|
+
|
|
184
|
+
def on_raw_update(self, filters: Filter | None = None, group: int = 0):
|
|
185
|
+
return self._decorator(RawUpdateHandler)(filters, group)
|
|
186
|
+
|
|
187
|
+
# --- диспетчер --------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
async def _feed(self, update) -> None:
|
|
190
|
+
"""Принимает событие низкоуровневого роутера и раздаёт обработчикам."""
|
|
191
|
+
event = update.event
|
|
192
|
+
payload: Any = update
|
|
193
|
+
if event == "message":
|
|
194
|
+
payload = Message(self, update.message)
|
|
195
|
+
if getattr(update, "is_edit", False):
|
|
196
|
+
event = "edited_message"
|
|
197
|
+
|
|
198
|
+
for group in sorted(self._groups):
|
|
199
|
+
for handler in self._groups[group]:
|
|
200
|
+
if handler.event not in (event, "raw"):
|
|
201
|
+
continue
|
|
202
|
+
target = update if handler.event == "raw" else payload
|
|
203
|
+
try:
|
|
204
|
+
if not await handler.check(self, target):
|
|
205
|
+
continue
|
|
206
|
+
except Exception:
|
|
207
|
+
log.exception("Ошибка в фильтре %s", handler)
|
|
208
|
+
continue
|
|
209
|
+
try:
|
|
210
|
+
await handler.callback(self, target)
|
|
211
|
+
except ContinuePropagation:
|
|
212
|
+
continue
|
|
213
|
+
except StopPropagation:
|
|
214
|
+
return
|
|
215
|
+
except Exception:
|
|
216
|
+
log.exception("Ошибка в обработчике %s", handler)
|
|
217
|
+
break # в группе срабатывает первый подошедший
|
|
218
|
+
|
|
219
|
+
# --- сообщения --------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def _parse(self, text: str, parse_mode: ParseMode | str | None):
|
|
222
|
+
return parse(text, parse_mode if parse_mode is not None else self.parse_mode)
|
|
223
|
+
|
|
224
|
+
async def send_message(
|
|
225
|
+
self,
|
|
226
|
+
chat_id: int,
|
|
227
|
+
text: str,
|
|
228
|
+
*,
|
|
229
|
+
parse_mode: ParseMode | str | None = None,
|
|
230
|
+
entities: Sequence[dict[str, Any]] | None = None,
|
|
231
|
+
reply_to_message_id: str | None = None,
|
|
232
|
+
disable_notification: bool = False,
|
|
233
|
+
**kwargs: Any,
|
|
234
|
+
) -> Message:
|
|
235
|
+
"""Отправляет текстовое сообщение."""
|
|
236
|
+
body, parsed = self._parse(text, parse_mode)
|
|
237
|
+
raw = await self._raw.send_message(
|
|
238
|
+
chat_id,
|
|
239
|
+
body,
|
|
240
|
+
elements=list(entities) if entities is not None else parsed,
|
|
241
|
+
reply_to=reply_to_message_id,
|
|
242
|
+
notify=not disable_notification,
|
|
243
|
+
**kwargs,
|
|
244
|
+
)
|
|
245
|
+
return Message(self, raw)
|
|
246
|
+
|
|
247
|
+
async def edit_message_text(
|
|
248
|
+
self,
|
|
249
|
+
chat_id: int,
|
|
250
|
+
message_id: str,
|
|
251
|
+
text: str,
|
|
252
|
+
*,
|
|
253
|
+
parse_mode: ParseMode | str | None = None,
|
|
254
|
+
**kwargs: Any,
|
|
255
|
+
) -> Message:
|
|
256
|
+
body, parsed = self._parse(text, parse_mode)
|
|
257
|
+
raw = await self._raw.edit_message(
|
|
258
|
+
chat_id, message_id, body, elements=parsed, **kwargs
|
|
259
|
+
)
|
|
260
|
+
return Message(self, raw)
|
|
261
|
+
|
|
262
|
+
async def delete_messages(
|
|
263
|
+
self, chat_id: int, message_ids: Sequence[str] | str, *, revoke: bool = True
|
|
264
|
+
) -> None:
|
|
265
|
+
"""``revoke=True`` удаляет у всех, ``False`` — только у себя."""
|
|
266
|
+
await self._raw.delete_messages(chat_id, message_ids, for_me=not revoke)
|
|
267
|
+
|
|
268
|
+
async def forward_messages(
|
|
269
|
+
self, chat_id: int, from_chat_id: int, message_ids: Sequence[str] | str
|
|
270
|
+
) -> list[Message]:
|
|
271
|
+
if isinstance(message_ids, str):
|
|
272
|
+
message_ids = [message_ids]
|
|
273
|
+
raw = await self._raw.forward_messages(chat_id, from_chat_id, message_ids)
|
|
274
|
+
return [Message(self, item) for item in raw]
|
|
275
|
+
|
|
276
|
+
async def get_messages(
|
|
277
|
+
self, chat_id: int, message_ids: Sequence[str] | str
|
|
278
|
+
) -> Message | list[Message]:
|
|
279
|
+
single = isinstance(message_ids, str)
|
|
280
|
+
raw = await self._raw.get_messages(chat_id, message_ids)
|
|
281
|
+
messages = [Message(self, item) for item in raw]
|
|
282
|
+
if single:
|
|
283
|
+
return messages[0] if messages else None # type: ignore[return-value]
|
|
284
|
+
return messages
|
|
285
|
+
|
|
286
|
+
async def get_chat_history(
|
|
287
|
+
self, chat_id: int, *, limit: int = 100, offset_date: int | None = None
|
|
288
|
+
) -> AsyncIterator[Message]:
|
|
289
|
+
"""Асинхронный генератор сообщений — от новых к старым."""
|
|
290
|
+
produced = 0
|
|
291
|
+
async for raw in self._raw.iter_history(chat_id, limit=limit):
|
|
292
|
+
if offset_date is not None and (raw.get("time") or 0) > offset_date:
|
|
293
|
+
continue
|
|
294
|
+
yield Message(self, raw)
|
|
295
|
+
produced += 1
|
|
296
|
+
if produced >= limit:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
async def search_messages(
|
|
300
|
+
self, query: str, *, chat_id: int | None = None, limit: int = 30
|
|
301
|
+
) -> dict[str, Any]:
|
|
302
|
+
return await self._raw.search_messages(query, chat_id=chat_id, count=limit)
|
|
303
|
+
|
|
304
|
+
async def read_chat_history(self, chat_id: int, message_id: str) -> None:
|
|
305
|
+
await self._raw.read_message(chat_id, message_id)
|
|
306
|
+
|
|
307
|
+
async def send_reaction(
|
|
308
|
+
self, chat_id: int, message_id: str, emoji: str = "❤️"
|
|
309
|
+
) -> Any:
|
|
310
|
+
return await self._raw.set_reaction(chat_id, message_id, emoji)
|
|
311
|
+
|
|
312
|
+
async def send_chat_action(
|
|
313
|
+
self, chat_id: int, action: ChatAction | str = ChatAction.TYPING
|
|
314
|
+
) -> None:
|
|
315
|
+
raw = action.raw if isinstance(action, ChatAction) else str(action)
|
|
316
|
+
await self._raw.send_typing(chat_id, raw)
|
|
317
|
+
|
|
318
|
+
# --- медиа ------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
async def send_photo(
|
|
321
|
+
self, chat_id: int, photo, caption: str = "", **kwargs: Any
|
|
322
|
+
) -> Message:
|
|
323
|
+
return Message(self, await self._raw.send_photo(chat_id, photo, caption, **kwargs))
|
|
324
|
+
|
|
325
|
+
async def send_video(
|
|
326
|
+
self, chat_id: int, video, caption: str = "", **kwargs: Any
|
|
327
|
+
) -> Message:
|
|
328
|
+
return Message(self, await self._raw.send_video(chat_id, video, caption, **kwargs))
|
|
329
|
+
|
|
330
|
+
async def send_document(
|
|
331
|
+
self, chat_id: int, document, caption: str = "", **kwargs: Any
|
|
332
|
+
) -> Message:
|
|
333
|
+
return Message(self, await self._raw.send_file(chat_id, document, caption, **kwargs))
|
|
334
|
+
|
|
335
|
+
async def send_sticker(self, chat_id: int, sticker_id: int, **kwargs: Any) -> Message:
|
|
336
|
+
return Message(self, await self._raw.send_sticker(chat_id, sticker_id, **kwargs))
|
|
337
|
+
|
|
338
|
+
async def download_media(
|
|
339
|
+
self, message: Message, *, file_name: str | os.PathLike[str] | None = None
|
|
340
|
+
) -> Path | None:
|
|
341
|
+
"""Скачивает первое вложение сообщения."""
|
|
342
|
+
attach = None
|
|
343
|
+
for candidate in message._raw.attaches:
|
|
344
|
+
if candidate.type in ("PHOTO", "VIDEO", "FILE", "AUDIO"):
|
|
345
|
+
attach = candidate
|
|
346
|
+
break
|
|
347
|
+
if attach is None:
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
chat_id, message_id = message._raw.chat_id, message.id
|
|
351
|
+
if attach.type == "FILE":
|
|
352
|
+
url = await self._raw.get_file_url(chat_id, message_id, attach.file_id)
|
|
353
|
+
default = attach.name or f"{attach.file_id}.bin"
|
|
354
|
+
elif attach.type == "VIDEO":
|
|
355
|
+
urls = await self._raw.get_video_url(chat_id, message_id, attach.video_id)
|
|
356
|
+
url = next(iter(urls.values()), None)
|
|
357
|
+
default = f"{attach.video_id}.mp4"
|
|
358
|
+
elif attach.type == "AUDIO":
|
|
359
|
+
data = await self._raw.get_audio_url(chat_id, message_id, attach.audio_id)
|
|
360
|
+
url = data.get("url")
|
|
361
|
+
default = f"{attach.audio_id}.ogg"
|
|
362
|
+
else:
|
|
363
|
+
url, default = attach.url, f"{attach.photo_id}.jpg"
|
|
364
|
+
if not url:
|
|
365
|
+
return None
|
|
366
|
+
return await self._raw.download(url, file_name or default)
|
|
367
|
+
|
|
368
|
+
# --- чаты -------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
async def get_chat(self, chat_id: int) -> Chat | None:
|
|
371
|
+
raw = await self._raw.get_chat(chat_id)
|
|
372
|
+
return Chat(self, raw) if raw else None
|
|
373
|
+
|
|
374
|
+
async def get_dialogs(self, *, limit: int | None = None) -> AsyncIterator[Dialog]:
|
|
375
|
+
async for raw in self._raw.iter_chats(limit=limit):
|
|
376
|
+
yield Dialog(self, raw)
|
|
377
|
+
|
|
378
|
+
async def join_chat(self, link: str) -> Chat | None:
|
|
379
|
+
raw = await self._raw.join_chat(link)
|
|
380
|
+
return Chat(self, raw) if raw else None
|
|
381
|
+
|
|
382
|
+
async def leave_chat(self, chat_id: int) -> None:
|
|
383
|
+
await self._raw.leave_chat(chat_id)
|
|
384
|
+
|
|
385
|
+
async def set_chat_title(self, chat_id: int, title: str) -> None:
|
|
386
|
+
await self._raw.set_chat_title(chat_id, title)
|
|
387
|
+
|
|
388
|
+
async def set_chat_description(self, chat_id: int, description: str) -> None:
|
|
389
|
+
await self._raw.set_chat_description(chat_id, description)
|
|
390
|
+
|
|
391
|
+
async def pin_chat_message(
|
|
392
|
+
self, chat_id: int, message_id: str, *, disable_notification: bool = False
|
|
393
|
+
) -> None:
|
|
394
|
+
await self._raw.pin_message(chat_id, message_id, notify=not disable_notification)
|
|
395
|
+
|
|
396
|
+
async def unpin_chat_message(self, chat_id: int) -> None:
|
|
397
|
+
await self._raw.unpin_message(chat_id)
|
|
398
|
+
|
|
399
|
+
async def get_chat_members(
|
|
400
|
+
self, chat_id: int, *, limit: int = 200, query: str | None = None
|
|
401
|
+
) -> list[ChatMember]:
|
|
402
|
+
raw = await self._raw.get_members(chat_id, count=limit, query=query)
|
|
403
|
+
return [ChatMember(self, item) for item in raw]
|
|
404
|
+
|
|
405
|
+
async def add_chat_members(
|
|
406
|
+
self, chat_id: int, user_ids: Sequence[int] | int
|
|
407
|
+
) -> None:
|
|
408
|
+
await self._raw.add_members(chat_id, user_ids)
|
|
409
|
+
|
|
410
|
+
async def ban_chat_member(self, chat_id: int, user_id: int) -> None:
|
|
411
|
+
await self._raw.block_members(chat_id, user_id)
|
|
412
|
+
|
|
413
|
+
async def unban_chat_member(self, chat_id: int, user_id: int) -> None:
|
|
414
|
+
await self._raw.update_members(chat_id, user_id, "unblock")
|
|
415
|
+
|
|
416
|
+
async def promote_chat_member(
|
|
417
|
+
self, chat_id: int, user_id: int, *, permissions: list[str] | None = None
|
|
418
|
+
) -> None:
|
|
419
|
+
await self._raw.promote_members(chat_id, user_id, permissions)
|
|
420
|
+
|
|
421
|
+
async def demote_chat_member(self, chat_id: int, user_id: int) -> None:
|
|
422
|
+
await self._raw.demote_members(chat_id, user_id)
|
|
423
|
+
|
|
424
|
+
async def get_chat_member(self, chat_id: int, user_id: int) -> ChatMember | None:
|
|
425
|
+
for member in await self.get_chat_members(chat_id):
|
|
426
|
+
if member.user_id == user_id:
|
|
427
|
+
return member
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
# --- пользователи ------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
async def get_me(self) -> User | None:
|
|
433
|
+
raw = await self._raw.get_me()
|
|
434
|
+
return User(self, raw) if raw else None
|
|
435
|
+
|
|
436
|
+
async def get_users(
|
|
437
|
+
self, user_ids: Sequence[int] | int
|
|
438
|
+
) -> User | list[User]:
|
|
439
|
+
single = isinstance(user_ids, int)
|
|
440
|
+
raw = await self._raw.get_contacts(user_ids)
|
|
441
|
+
users = [User(self, item) for item in raw]
|
|
442
|
+
if single:
|
|
443
|
+
return users[0] if users else None # type: ignore[return-value]
|
|
444
|
+
return users
|
|
445
|
+
|
|
446
|
+
async def get_contacts(self) -> list[User]:
|
|
447
|
+
return [User(self, raw) async for raw in self._raw.iter_contacts()]
|
|
448
|
+
|
|
449
|
+
async def block_user(self, user_id: int) -> None:
|
|
450
|
+
await self._raw.block_user(user_id)
|
|
451
|
+
|
|
452
|
+
async def unblock_user(self, user_id: int) -> None:
|
|
453
|
+
await self._raw.unblock_user(user_id)
|
|
454
|
+
|
|
455
|
+
async def resolve_phone(self, phone: str) -> User | None:
|
|
456
|
+
"""Ищет пользователя по номеру телефона."""
|
|
457
|
+
raw = await self._raw.get_contact_by_phone(phone)
|
|
458
|
+
return User(self, raw) if raw else None
|
|
459
|
+
|
|
460
|
+
async def set_profile_name(
|
|
461
|
+
self, first_name: str, last_name: str | None = None
|
|
462
|
+
) -> User:
|
|
463
|
+
raw = await self._raw.update_profile(
|
|
464
|
+
first_name=first_name, last_name=last_name
|
|
465
|
+
)
|
|
466
|
+
return User(self, raw)
|
|
467
|
+
|
|
468
|
+
# --- прочее -----------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
async def invoke(self, opcode, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
471
|
+
"""Вызывает произвольный опкод протокола напрямую."""
|
|
472
|
+
return await self._raw.call(opcode, payload)
|
|
473
|
+
|
|
474
|
+
def __repr__(self) -> str:
|
|
475
|
+
state = "запущен" if self._started else "остановлен"
|
|
476
|
+
return f"<Client {self.name!r} {state}>"
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
async def idle() -> None:
|
|
480
|
+
"""Блокируется до Ctrl+C."""
|
|
481
|
+
stop = asyncio.Event()
|
|
482
|
+
loop = asyncio.get_running_loop()
|
|
483
|
+
import signal
|
|
484
|
+
|
|
485
|
+
for sig in (getattr(signal, "SIGINT", None), getattr(signal, "SIGTERM", None)):
|
|
486
|
+
if sig is None:
|
|
487
|
+
continue
|
|
488
|
+
try:
|
|
489
|
+
loop.add_signal_handler(sig, stop.set)
|
|
490
|
+
except (NotImplementedError, RuntimeError): # Windows
|
|
491
|
+
pass
|
|
492
|
+
await stop.wait()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def compose(clients: Sequence[Client]) -> Any:
|
|
496
|
+
"""Запускает несколько клиентов разом и ждёт остановки."""
|
|
497
|
+
|
|
498
|
+
async def main():
|
|
499
|
+
await asyncio.gather(*(client.start() for client in clients))
|
|
500
|
+
try:
|
|
501
|
+
await idle()
|
|
502
|
+
finally:
|
|
503
|
+
await asyncio.gather(*(client.stop() for client in clients))
|
|
504
|
+
|
|
505
|
+
return asyncio.run(main())
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
__all__ = ["Client", "idle", "compose", "StopPropagation", "ContinuePropagation"]
|