wintergram-userbot 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.
- wintergram_userbot/__init__.py +47 -0
- wintergram_userbot/api/__init__.py +1 -0
- wintergram_userbot/api/functions.py +262 -0
- wintergram_userbot/api/updates.py +195 -0
- wintergram_userbot/client.py +332 -0
- wintergram_userbot/crypto/__init__.py +51 -0
- wintergram_userbot/crypto/aes.py +90 -0
- wintergram_userbot/crypto/auth_key.py +84 -0
- wintergram_userbot/crypto/dh.py +162 -0
- wintergram_userbot/crypto/rsa.py +134 -0
- wintergram_userbot/crypto/srp.py +138 -0
- wintergram_userbot/errors.py +124 -0
- wintergram_userbot/network/__init__.py +1 -0
- wintergram_userbot/network/connection.py +96 -0
- wintergram_userbot/network/mtproto.py +764 -0
- wintergram_userbot/network/transport.py +186 -0
- wintergram_userbot/session/__init__.py +1 -0
- wintergram_userbot/session/session.py +92 -0
- wintergram_userbot/session/storage.py +118 -0
- wintergram_userbot/tl/__init__.py +45 -0
- wintergram_userbot/tl/schema_resolver.py +159 -0
- wintergram_userbot/tl/schemas/__init__.py +1 -0
- wintergram_userbot/tl/schemas/data/core.json +1 -0
- wintergram_userbot/tl/schemas/data/latest.json +48098 -0
- wintergram_userbot/tl/schemas/data/v113.json +1 -0
- wintergram_userbot/tl/schemas/data/v119.json +1 -0
- wintergram_userbot/tl/schemas/data/v128.json +1 -0
- wintergram_userbot/tl/schemas/data/v133.json +1 -0
- wintergram_userbot/tl/serializer.py +406 -0
- wintergram_userbot/tl/types.py +67 -0
- wintergram_userbot-0.1.0.dist-info/METADATA +165 -0
- wintergram_userbot-0.1.0.dist-info/RECORD +35 -0
- wintergram_userbot-0.1.0.dist-info/WHEEL +5 -0
- wintergram_userbot-0.1.0.dist-info/licenses/LICENSE +21 -0
- wintergram_userbot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""wintergram_userbot - переписанный с нуля асинхронный MTProto-клиент.
|
|
2
|
+
|
|
3
|
+
Пишется для ПРИВАТНОГО MTProto-сервера (не официального Telegram).
|
|
4
|
+
Поддерживаются слои (layers) от 167 до 228 включительно.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from .client import WinterGramClient
|
|
10
|
+
from .errors import (
|
|
11
|
+
RPCError,
|
|
12
|
+
FloodWaitError,
|
|
13
|
+
AuthKeyError,
|
|
14
|
+
LayerMismatchError,
|
|
15
|
+
ConnectionClosedError,
|
|
16
|
+
UnknownLayerError,
|
|
17
|
+
AuthorizationExpiredError,
|
|
18
|
+
TimeoutError,
|
|
19
|
+
TransportError,
|
|
20
|
+
SessionPasswordNeededError,
|
|
21
|
+
PhoneCodeInvalidError,
|
|
22
|
+
PhoneNumberUnoccupiedError,
|
|
23
|
+
)
|
|
24
|
+
from .tl.types import TL, TLObject
|
|
25
|
+
from .api.updates import NewMessage, MessageEdited, MessageDeleted
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"WinterGramClient",
|
|
29
|
+
"RPCError",
|
|
30
|
+
"FloodWaitError",
|
|
31
|
+
"AuthKeyError",
|
|
32
|
+
"LayerMismatchError",
|
|
33
|
+
"ConnectionClosedError",
|
|
34
|
+
"UnknownLayerError",
|
|
35
|
+
"AuthorizationExpiredError",
|
|
36
|
+
"TimeoutError",
|
|
37
|
+
"TransportError",
|
|
38
|
+
"SessionPasswordNeededError",
|
|
39
|
+
"PhoneCodeInvalidError",
|
|
40
|
+
"PhoneNumberUnoccupiedError",
|
|
41
|
+
"TL",
|
|
42
|
+
"TLObject",
|
|
43
|
+
"NewMessage",
|
|
44
|
+
"MessageEdited",
|
|
45
|
+
"MessageDeleted",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Высокоуровневый API юзербота."""
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Высокоуровневый API: auth, sendMessage, getHistory, получения метаданных.
|
|
2
|
+
|
|
3
|
+
Каждый метод сам выбирает type_id и поля через SchemaResolver (по текущему слою
|
|
4
|
+
соединения), поэтому один и тот же вызов работает на схемах 167..228.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from ..errors import (
|
|
12
|
+
RPCError,
|
|
13
|
+
SessionPasswordNeededError,
|
|
14
|
+
PhoneCodeInvalidError,
|
|
15
|
+
PhoneNumberUnoccupiedError,
|
|
16
|
+
)
|
|
17
|
+
from ..crypto.srp import compute_password_check
|
|
18
|
+
|
|
19
|
+
__log__ = logging.getLogger("wintergram.api.functions")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Functions:
|
|
23
|
+
"""Обёртка над MTProtoConnection с понятными методами."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, conn, api_id=2040, api_hash="b18441a1ff607e10a989891a5462e627"):
|
|
26
|
+
self.conn = conn
|
|
27
|
+
self.api_id = api_id
|
|
28
|
+
self.api_hash = api_hash
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def schema(self):
|
|
32
|
+
return self.conn._schema
|
|
33
|
+
|
|
34
|
+
# ------------------------------------------------------------------ #
|
|
35
|
+
# авторизация
|
|
36
|
+
# ------------------------------------------------------------------ #
|
|
37
|
+
|
|
38
|
+
async def send_code(self, phone: str):
|
|
39
|
+
"""auth.sendCode. Возвращает auth.sentCode (phone_code_hash и пр.)."""
|
|
40
|
+
code_settings_desc = self.schema.by_name("codeSettings")
|
|
41
|
+
if code_settings_desc is None:
|
|
42
|
+
raise RPCError(0, "codeSettings not available in schema")
|
|
43
|
+
_, obj = await self.conn.rpc("auth.sendCode", {
|
|
44
|
+
"phone_number": phone,
|
|
45
|
+
"api_id": self.api_id,
|
|
46
|
+
"api_hash": self.api_hash,
|
|
47
|
+
"settings": {"_id": code_settings_desc["id"]},
|
|
48
|
+
})
|
|
49
|
+
return obj
|
|
50
|
+
|
|
51
|
+
async def sign_in(self, phone: str, code: str, phone_code_hash: str):
|
|
52
|
+
"""auth.signIn (code вводится пользователем в рантайме)."""
|
|
53
|
+
try:
|
|
54
|
+
result = await self.conn.rpc("auth.signIn", {
|
|
55
|
+
"phone_number": phone,
|
|
56
|
+
"phone_code_hash": phone_code_hash,
|
|
57
|
+
"phone_code": code,
|
|
58
|
+
})
|
|
59
|
+
except RPCError as e:
|
|
60
|
+
if e.message == "SESSION_PASSWORD_NEEDED":
|
|
61
|
+
raise SessionPasswordNeededError(e.code, e.message)
|
|
62
|
+
if e.message == "PHONE_CODE_INVALID":
|
|
63
|
+
raise PhoneCodeInvalidError(e.code, e.message)
|
|
64
|
+
if e.message in ("PHONE_NUMBER_UNOCCUPIED", "KEY_UNOCCUPIED"):
|
|
65
|
+
raise PhoneNumberUnoccupiedError(e.code, e.message)
|
|
66
|
+
if e.message == "PHONE_CODE_EXPIRED":
|
|
67
|
+
from ..errors import PhoneCodeExpiredError
|
|
68
|
+
raise PhoneCodeExpiredError(e.code, e.message)
|
|
69
|
+
raise
|
|
70
|
+
self._store_auth(result)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
async def sign_up(self, phone: str, code: str, phone_code_hash: str,
|
|
74
|
+
first_name: str, last_name: str = ""):
|
|
75
|
+
"""auth.signUp."""
|
|
76
|
+
result = await self.conn.rpc("auth.signUp", {
|
|
77
|
+
"phone_number": phone,
|
|
78
|
+
"phone_code_hash": phone_code_hash,
|
|
79
|
+
"first_name": first_name,
|
|
80
|
+
"last_name": last_name,
|
|
81
|
+
})
|
|
82
|
+
self._store_auth(result)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
async def get_password(self):
|
|
86
|
+
"""account.getPassword — параметры для 2FA."""
|
|
87
|
+
return await self.conn.rpc("account.getPassword", {})
|
|
88
|
+
|
|
89
|
+
async def check_password(self, password: str, password_obj):
|
|
90
|
+
"""auth.checkPassword (2FA) — считает SRP по текущей схеме."""
|
|
91
|
+
_, pw = password_obj
|
|
92
|
+
algo = pw.get("current_algo")
|
|
93
|
+
if algo is None:
|
|
94
|
+
raise RPCError(0, "server has no current_algo; 2FA flow unclear")
|
|
95
|
+
salt1 = algo.get("salt1")
|
|
96
|
+
salt2 = algo.get("salt2")
|
|
97
|
+
g = algo.get("g")
|
|
98
|
+
p = algo.get("p")
|
|
99
|
+
srp_b = pw.get("srp_B")
|
|
100
|
+
srp_id = pw.get("srp_id")
|
|
101
|
+
if not (salt1 and salt2 and g and p and srp_b):
|
|
102
|
+
raise RPCError(0, "incomplete SRP params from account.getPassword")
|
|
103
|
+
|
|
104
|
+
req = compute_password_check(password, srp_id, srp_b, salt1, salt2, g, p)
|
|
105
|
+
input_desc = self.schema.by_name("inputCheckPasswordSRP")
|
|
106
|
+
password_value = {
|
|
107
|
+
"_id": input_desc["id"],
|
|
108
|
+
"srp_id": req["srp_id"],
|
|
109
|
+
"A": req["A"],
|
|
110
|
+
"M1": req["M1"],
|
|
111
|
+
}
|
|
112
|
+
result = await self.conn.rpc("auth.checkPassword", {"password": password_value})
|
|
113
|
+
self._store_auth(result)
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------------ #
|
|
117
|
+
# пользователи
|
|
118
|
+
# ------------------------------------------------------------------ #
|
|
119
|
+
|
|
120
|
+
def input_peer_self(self):
|
|
121
|
+
desc = self.schema.by_name("inputPeerSelf")
|
|
122
|
+
return {"_id": desc["id"]} if desc else None
|
|
123
|
+
|
|
124
|
+
def input_peer_user(self, user_id: int, access_hash: int):
|
|
125
|
+
desc = self.schema.by_name("inputPeerUser")
|
|
126
|
+
if desc is None:
|
|
127
|
+
return None
|
|
128
|
+
return {"_id": desc["id"], "user_id": user_id, "access_hash": access_hash}
|
|
129
|
+
|
|
130
|
+
def input_peer_chat(self, chat_id: int):
|
|
131
|
+
desc = self.schema.by_name("inputPeerChat")
|
|
132
|
+
if desc is None:
|
|
133
|
+
return None
|
|
134
|
+
return {"_id": desc["id"], "chat_id": chat_id}
|
|
135
|
+
|
|
136
|
+
def input_peer_channel(self, channel_id: int, access_hash: int):
|
|
137
|
+
desc = self.schema.by_name("inputPeerChannel")
|
|
138
|
+
if desc is None:
|
|
139
|
+
return None
|
|
140
|
+
return {"_id": desc["id"], "channel_id": channel_id, "access_hash": access_hash}
|
|
141
|
+
|
|
142
|
+
def input_user(self, user_id: int, access_hash: int):
|
|
143
|
+
desc = self.schema.by_name("inputUser")
|
|
144
|
+
if desc is None:
|
|
145
|
+
return None
|
|
146
|
+
return {"_id": desc["id"], "user_id": user_id, "access_hash": access_hash}
|
|
147
|
+
|
|
148
|
+
def input_user_self(self):
|
|
149
|
+
desc = self.schema.by_name("inputUserSelf")
|
|
150
|
+
return {"_id": desc["id"]} if desc else None
|
|
151
|
+
|
|
152
|
+
async def get_me(self):
|
|
153
|
+
"""Возвращает текущего юзера. На приват-сервере users.getUsers может
|
|
154
|
+
отсутствовать — тогда берём user, сохранённый при auth.signIn/checkPassword."""
|
|
155
|
+
if self.schema.by_name("users.getUsers") is not None:
|
|
156
|
+
_, obj = await self.conn.rpc("users.getUsers", {
|
|
157
|
+
"id": [self.input_user_self()],
|
|
158
|
+
})
|
|
159
|
+
# ответ может быть Vector<User> напрямую или обёрнут в объект
|
|
160
|
+
if isinstance(obj, list):
|
|
161
|
+
users = obj
|
|
162
|
+
else:
|
|
163
|
+
users = obj.get("users") or obj if isinstance(obj, list) else []
|
|
164
|
+
if not users:
|
|
165
|
+
# если users.getUsers вернул пустоту, используем кешированного юзера
|
|
166
|
+
user = self.conn.session.user
|
|
167
|
+
if user is None:
|
|
168
|
+
raise RPCError(0, "users.getUsers returned no users and no cached user")
|
|
169
|
+
self.conn.session.user_id = user.get("id")
|
|
170
|
+
return user
|
|
171
|
+
user = users[0]
|
|
172
|
+
self.conn.session.user = user
|
|
173
|
+
self.conn.session.user_id = user.get("id")
|
|
174
|
+
return user
|
|
175
|
+
user = self.conn.session.user
|
|
176
|
+
if user is None:
|
|
177
|
+
raise RPCError(0, "users.getUsers not in schema (layer %d) and no cached user"
|
|
178
|
+
% self.conn._schema.layer)
|
|
179
|
+
self.conn.session.user_id = user.get("id")
|
|
180
|
+
return user
|
|
181
|
+
|
|
182
|
+
async def get_users(self, ids):
|
|
183
|
+
"""users.getUsers([InputUser...])."""
|
|
184
|
+
return await self.conn.rpc("users.getUsers", {"id": ids})
|
|
185
|
+
|
|
186
|
+
# ------------------------------------------------------------------ #
|
|
187
|
+
# отправка / история
|
|
188
|
+
# ------------------------------------------------------------------ #
|
|
189
|
+
|
|
190
|
+
async def send_message(self, peer, text: str, random_id=None, reply_to=None):
|
|
191
|
+
"""messages.sendMessage(peer, text, random_id, reply_to)."""
|
|
192
|
+
params = {
|
|
193
|
+
"peer": peer,
|
|
194
|
+
"message": text,
|
|
195
|
+
"random_id": random_id if random_id is not None else int(
|
|
196
|
+
int.from_bytes(os.urandom(8), "little", signed=True)
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
if reply_to is not None:
|
|
200
|
+
# разные эры: reply_to_msg_id (int) или reply_to (InputReplyTo)
|
|
201
|
+
if "reply_to_msg_id" in self._param_names("messages.sendMessage"):
|
|
202
|
+
if isinstance(reply_to, int):
|
|
203
|
+
params["reply_to_msg_id"] = reply_to
|
|
204
|
+
if "reply_to" in self._param_names("messages.sendMessage"):
|
|
205
|
+
if isinstance(reply_to, int):
|
|
206
|
+
reply_desc = self.schema.by_name("inputReplyToMessage")
|
|
207
|
+
params["reply_to"] = {"_id": reply_desc["id"], "reply_to_msg_id": reply_to}
|
|
208
|
+
return await self.conn.rpc("messages.sendMessage", params)
|
|
209
|
+
|
|
210
|
+
async def edit_message(self, peer, msg_id: int, text: str):
|
|
211
|
+
"""messages.editMessage - редактирует существующее сообщение."""
|
|
212
|
+
params = {
|
|
213
|
+
"peer": peer,
|
|
214
|
+
"id": msg_id,
|
|
215
|
+
"message": text,
|
|
216
|
+
}
|
|
217
|
+
return await self.conn.rpc("messages.editMessage", params)
|
|
218
|
+
|
|
219
|
+
async def get_history(self, peer, limit=50):
|
|
220
|
+
"""messages.getHistory."""
|
|
221
|
+
return await self.conn.rpc("messages.getHistory", {
|
|
222
|
+
"peer": peer,
|
|
223
|
+
"offset_id": 0,
|
|
224
|
+
"offset_date": 0,
|
|
225
|
+
"add_offset": 0,
|
|
226
|
+
"limit": int(limit),
|
|
227
|
+
"max_id": 0,
|
|
228
|
+
"min_id": 0,
|
|
229
|
+
"hash": 0,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------ #
|
|
233
|
+
# состояние / различие
|
|
234
|
+
# ------------------------------------------------------------------ #
|
|
235
|
+
|
|
236
|
+
async def get_state(self):
|
|
237
|
+
return await self.conn.rpc("updates.getState", {})
|
|
238
|
+
|
|
239
|
+
async def get_difference(self, pts, date=0, qts=0, pts_total_limit=None):
|
|
240
|
+
params = {
|
|
241
|
+
"pts": int(pts),
|
|
242
|
+
"pts_total_limit": pts_total_limit,
|
|
243
|
+
"date": int(date),
|
|
244
|
+
"qts": int(qts),
|
|
245
|
+
}
|
|
246
|
+
if "pts_total_limit" not in self._param_names("updates.getDifference"):
|
|
247
|
+
params.pop("pts_total_limit", None)
|
|
248
|
+
return await self.conn.rpc("updates.getDifference", params)
|
|
249
|
+
|
|
250
|
+
# ------------------------------------------------------------------ #
|
|
251
|
+
def _param_names(self, method_name):
|
|
252
|
+
desc = self.schema.by_name(method_name)
|
|
253
|
+
if desc is None:
|
|
254
|
+
return []
|
|
255
|
+
return [p[0] for p in desc["params"]]
|
|
256
|
+
|
|
257
|
+
def _store_auth(self, result):
|
|
258
|
+
_, obj = result
|
|
259
|
+
user = obj.get("user") if hasattr(obj, "get") else (obj or {}).get("user")
|
|
260
|
+
if user:
|
|
261
|
+
self.conn.session.user_id = user.get("id")
|
|
262
|
+
self.conn.session.user = user
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Обработка updates: разбор, события, роутинг в зарегистрированные хендлеры.
|
|
2
|
+
|
|
3
|
+
События создаются независимо от эры схемы: поля достаются из разобранного
|
|
4
|
+
дерева TLObject (dict-like), поэтому одна логика работает и на слое 167,
|
|
5
|
+
и на слое 228.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ..tl.types import TLObject
|
|
11
|
+
|
|
12
|
+
__log__ = logging.getLogger("wintergram.api.updates")
|
|
13
|
+
|
|
14
|
+
# имена апдейтов (по stdout 'name' поля в схемах)
|
|
15
|
+
_UPDATE_NEW_MESSAGE = {"updateNewMessage", "updateNewChannelMessage"}
|
|
16
|
+
_UPDATE_EDIT = {"updateEditMessage", "updateEditChannelMessage"}
|
|
17
|
+
_UPDATE_DELETE = {"updateDeleteMessages", "updateDeleteChannelMessages"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _as_dict(obj):
|
|
21
|
+
if obj is None:
|
|
22
|
+
return {}
|
|
23
|
+
if isinstance(obj, dict):
|
|
24
|
+
return obj
|
|
25
|
+
if isinstance(obj, TLObject):
|
|
26
|
+
return obj.as_dict()
|
|
27
|
+
return dict(obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Event:
|
|
31
|
+
"""Базовое событие."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, raw):
|
|
34
|
+
self.raw = raw # исходный TLObject (или dict)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def raw_dict(self):
|
|
38
|
+
return _as_dict(self.raw)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NewMessage(Event):
|
|
42
|
+
"""Новое сообщение.
|
|
43
|
+
|
|
44
|
+
Поля: id, text, date, chat_id, sender_id, out, reply_to_msg_id, raw.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, raw, id=None, text=None, date=None, chat_id=None,
|
|
48
|
+
sender_id=None, out=False, from_channel=False):
|
|
49
|
+
super().__init__(raw)
|
|
50
|
+
self.id = id
|
|
51
|
+
self.text = text
|
|
52
|
+
self.date = date
|
|
53
|
+
self.chat_id = chat_id
|
|
54
|
+
self.sender_id = sender_id
|
|
55
|
+
self.out = bool(out)
|
|
56
|
+
self.from_channel = from_channel
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_private(self):
|
|
60
|
+
return not self.from_channel and self.chat_id is not None and self.sender_id is not None
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_channel(self):
|
|
64
|
+
return self.from_channel
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
return "<NewMessage id=%s chat=%s from=%s text=%r>" % (
|
|
68
|
+
self.id, self.chat_id, self.sender_id, (self.text or "")[:40]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MessageEdited(NewMessage):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MessageDeleted(Event):
|
|
77
|
+
|
|
78
|
+
def __init__(self, raw, deleted_ids=(), chat_id=None, channel_id=None):
|
|
79
|
+
super().__init__(raw)
|
|
80
|
+
self.deleted_ids = list(deleted_ids)
|
|
81
|
+
self.chat_id = chat_id
|
|
82
|
+
self.channel_id = channel_id
|
|
83
|
+
|
|
84
|
+
def __repr__(self):
|
|
85
|
+
return "<MessageDeleted ids=%s>" % (self.deleted_ids,)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class MessageHandler:
|
|
89
|
+
"""Реестр хендлеров по типу события."""
|
|
90
|
+
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self._handlers = {} # "NewMessage" -> list[callable]
|
|
93
|
+
|
|
94
|
+
def on(self, event_type):
|
|
95
|
+
name = getattr(event_type, "__name__", str(event_type))
|
|
96
|
+
def decorator(func):
|
|
97
|
+
self._handlers.setdefault(name, []).append(func)
|
|
98
|
+
return func
|
|
99
|
+
return decorator
|
|
100
|
+
|
|
101
|
+
async def dispatch(self, events):
|
|
102
|
+
results = []
|
|
103
|
+
for ev in events:
|
|
104
|
+
name = type(ev).__name__
|
|
105
|
+
for handler in self._handlers.get(name, []):
|
|
106
|
+
try:
|
|
107
|
+
r = handler(ev)
|
|
108
|
+
if hasattr(r, "__await__"):
|
|
109
|
+
await r
|
|
110
|
+
results.append(r)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
__log__.error("handler %s raised: %r", getattr(handler, "__qualname__", handler), e)
|
|
113
|
+
return results
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class UpdatesProcessor:
|
|
117
|
+
"""Превращает входящий объект Updates в список событий."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, schema):
|
|
120
|
+
self.schema = schema
|
|
121
|
+
|
|
122
|
+
def process(self, obj) -> list:
|
|
123
|
+
d = _as_dict(obj)
|
|
124
|
+
name = d.get("_")
|
|
125
|
+
events = []
|
|
126
|
+
if name in ("updates", "updatesCombined", "updateShort"):
|
|
127
|
+
inner = d.get("update")
|
|
128
|
+
if inner is not None:
|
|
129
|
+
events += self.process(inner)
|
|
130
|
+
for u in (d.get("updates") or []):
|
|
131
|
+
events += self.process(u)
|
|
132
|
+
return events
|
|
133
|
+
if name == "updateShortMessage":
|
|
134
|
+
return [NewMessage(
|
|
135
|
+
raw=d, id=d.get("id"), text=d.get("message"),
|
|
136
|
+
date=d.get("date"), chat_id=d.get("user_id"),
|
|
137
|
+
sender_id=d.get("user_id") if not d.get("out") else None,
|
|
138
|
+
out=d.get("out", False),
|
|
139
|
+
)]
|
|
140
|
+
if name == "updateShortChatMessage":
|
|
141
|
+
return [NewMessage(
|
|
142
|
+
raw=d, id=d.get("id"), text=d.get("message"),
|
|
143
|
+
date=d.get("date"), chat_id=d.get("chat_id"),
|
|
144
|
+
sender_id=d.get("from_id"), out=d.get("out", False),
|
|
145
|
+
)]
|
|
146
|
+
if name == "updateShortSentMessage":
|
|
147
|
+
return [] # подтверждение собственной отправки
|
|
148
|
+
if name in _UPDATE_NEW_MESSAGE:
|
|
149
|
+
message = d.get("message") or {}
|
|
150
|
+
md = _as_dict(message)
|
|
151
|
+
chat_id = self._chat_id(md)
|
|
152
|
+
sender = self._sender_id(md)
|
|
153
|
+
return [NewMessage(
|
|
154
|
+
raw=d, id=md.get("id") or d.get("id"), text=md.get("message"),
|
|
155
|
+
date=md.get("date"), chat_id=chat_id, sender_id=sender,
|
|
156
|
+
out=md.get("out", False), from_channel=name == "updateNewChannelMessage",
|
|
157
|
+
)]
|
|
158
|
+
if name in _UPDATE_EDIT:
|
|
159
|
+
message = d.get("message") or {}
|
|
160
|
+
md = _as_dict(message)
|
|
161
|
+
chat_id = self._chat_id(md)
|
|
162
|
+
sender = self._sender_id(md)
|
|
163
|
+
return [MessageEdited(
|
|
164
|
+
raw=d, id=md.get("id"), text=md.get("message"),
|
|
165
|
+
date=md.get("date"), chat_id=chat_id, sender_id=sender,
|
|
166
|
+
out=md.get("out", False), from_channel=name == "updateEditChannelMessage",
|
|
167
|
+
)]
|
|
168
|
+
if name in _UPDATE_DELETE:
|
|
169
|
+
return [MessageDeleted(
|
|
170
|
+
raw=d,
|
|
171
|
+
deleted_ids=d.get("messages") or [],
|
|
172
|
+
chat_id=(d.get("channel_id") if "channel_id" in d else None),
|
|
173
|
+
channel_id=d.get("channel_id"),
|
|
174
|
+
)]
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _chat_id(message_dict):
|
|
179
|
+
to_id = message_dict.get("to_id") or {}
|
|
180
|
+
td = _as_dict(to_id)
|
|
181
|
+
if td:
|
|
182
|
+
return td.get("user_id") or td.get("chat_id") or td.get("channel_id")
|
|
183
|
+
peer_id = message_dict.get("peer_id")
|
|
184
|
+
if isinstance(peer_id, (dict, TLObject)):
|
|
185
|
+
pd = _as_dict(peer_id)
|
|
186
|
+
return pd.get("user_id") or pd.get("chat_id") or pd.get("channel_id")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _sender_id(message_dict):
|
|
191
|
+
from_id = message_dict.get("from_id")
|
|
192
|
+
if isinstance(from_id, (dict, TLObject)):
|
|
193
|
+
fd = _as_dict(from_id)
|
|
194
|
+
return fd.get("user_id") or fd.get("channel_id") or fd.get("chat_id")
|
|
195
|
+
return from_id
|