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.
Files changed (35) hide show
  1. wintergram_userbot/__init__.py +47 -0
  2. wintergram_userbot/api/__init__.py +1 -0
  3. wintergram_userbot/api/functions.py +262 -0
  4. wintergram_userbot/api/updates.py +195 -0
  5. wintergram_userbot/client.py +332 -0
  6. wintergram_userbot/crypto/__init__.py +51 -0
  7. wintergram_userbot/crypto/aes.py +90 -0
  8. wintergram_userbot/crypto/auth_key.py +84 -0
  9. wintergram_userbot/crypto/dh.py +162 -0
  10. wintergram_userbot/crypto/rsa.py +134 -0
  11. wintergram_userbot/crypto/srp.py +138 -0
  12. wintergram_userbot/errors.py +124 -0
  13. wintergram_userbot/network/__init__.py +1 -0
  14. wintergram_userbot/network/connection.py +96 -0
  15. wintergram_userbot/network/mtproto.py +764 -0
  16. wintergram_userbot/network/transport.py +186 -0
  17. wintergram_userbot/session/__init__.py +1 -0
  18. wintergram_userbot/session/session.py +92 -0
  19. wintergram_userbot/session/storage.py +118 -0
  20. wintergram_userbot/tl/__init__.py +45 -0
  21. wintergram_userbot/tl/schema_resolver.py +159 -0
  22. wintergram_userbot/tl/schemas/__init__.py +1 -0
  23. wintergram_userbot/tl/schemas/data/core.json +1 -0
  24. wintergram_userbot/tl/schemas/data/latest.json +48098 -0
  25. wintergram_userbot/tl/schemas/data/v113.json +1 -0
  26. wintergram_userbot/tl/schemas/data/v119.json +1 -0
  27. wintergram_userbot/tl/schemas/data/v128.json +1 -0
  28. wintergram_userbot/tl/schemas/data/v133.json +1 -0
  29. wintergram_userbot/tl/serializer.py +406 -0
  30. wintergram_userbot/tl/types.py +67 -0
  31. wintergram_userbot-0.1.0.dist-info/METADATA +165 -0
  32. wintergram_userbot-0.1.0.dist-info/RECORD +35 -0
  33. wintergram_userbot-0.1.0.dist-info/WHEEL +5 -0
  34. wintergram_userbot-0.1.0.dist-info/licenses/LICENSE +21 -0
  35. 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