ferogram 0.1.4__cp313-abi3-android_24_arm64_v8a.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.
- ferogram/__init__.py +30 -0
- ferogram/_ferogram.abi3.so +0 -0
- ferogram/client.py +350 -0
- ferogram/filters.py +174 -0
- ferogram/logging.py +49 -0
- ferogram/py.typed +0 -0
- ferogram/raw/__init__.py +23 -0
- ferogram/raw/api/__init__.py +15 -0
- ferogram/raw/api/functions.py +16 -0
- ferogram/raw/api/types.py +16 -0
- ferogram/raw/codegen.py +228 -0
- ferogram/raw/generated/__init__.py +15 -0
- ferogram/raw/generated/_tl_schema.py +3895 -0
- ferogram/raw/generated/functions.py +19549 -0
- ferogram/raw/generated/types.py +41944 -0
- ferogram/raw/tl.py +252 -0
- ferogram/raw_api.tl +2944 -0
- ferogram-0.1.4.dist-info/METADATA +273 -0
- ferogram-0.1.4.dist-info/RECORD +23 -0
- ferogram-0.1.4.dist-info/WHEEL +4 -0
- ferogram-0.1.4.dist-info/licenses/LICENSE-APACHE +201 -0
- ferogram-0.1.4.dist-info/licenses/LICENSE-MIT +21 -0
- ferogram-0.1.4.dist-info/sboms/ferogram-py.cyclonedx.json +7308 -0
ferogram/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
_main_file = getattr(sys.modules.get("__main__"), "__file__", None)
|
|
18
|
+
if _main_file:
|
|
19
|
+
_name = os.path.splitext(os.path.basename(_main_file))[0]
|
|
20
|
+
if _name == "ferogram":
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"\n\nYour script is named 'ferogram.py' which shadows the ferogram package "
|
|
23
|
+
"and causes circular imports.\n"
|
|
24
|
+
"Rename your file to something like 'bot.py', 'main.py', or 'app.py'."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .client import Client
|
|
28
|
+
from . import filters
|
|
29
|
+
|
|
30
|
+
__all__ = ["Client", "filters"]
|
|
Binary file
|
ferogram/client.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import inspect
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any, Callable
|
|
21
|
+
|
|
22
|
+
from ._ferogram import Client as _RustClient, PasswordToken, User, Dialog
|
|
23
|
+
from ._ferogram import Message, CallbackQuery
|
|
24
|
+
from ._ferogram import (
|
|
25
|
+
MessageDeletion, InlineQuery, InlineSend, UserStatus,
|
|
26
|
+
ChatAction, ParticipantUpdate, JoinRequest, MessageReaction,
|
|
27
|
+
PollVote, BotStopped, RawUpdate,
|
|
28
|
+
)
|
|
29
|
+
from .raw import _tl
|
|
30
|
+
from .raw.generated._tl_schema import _SCHEMA_BY_CID
|
|
31
|
+
|
|
32
|
+
__all__ = ["Client"]
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger("ferogram")
|
|
35
|
+
|
|
36
|
+
_Handler = tuple[Callable, list[Callable]] # (func, filters)
|
|
37
|
+
|
|
38
|
+
_ALL_EVENTS = (
|
|
39
|
+
"message",
|
|
40
|
+
"edited_message",
|
|
41
|
+
"message_deleted",
|
|
42
|
+
"callback_query",
|
|
43
|
+
"inline_query",
|
|
44
|
+
"inline_send",
|
|
45
|
+
"user_status",
|
|
46
|
+
"chat_action",
|
|
47
|
+
"participant_update",
|
|
48
|
+
"join_request",
|
|
49
|
+
"message_reaction",
|
|
50
|
+
"poll_vote",
|
|
51
|
+
"bot_stopped",
|
|
52
|
+
"raw_update",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Client:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
session: str = "ferogram",
|
|
60
|
+
*,
|
|
61
|
+
api_id: int | None = None,
|
|
62
|
+
api_hash: str | None = None,
|
|
63
|
+
bot_token: str | None = None,
|
|
64
|
+
phone: str | None = None,
|
|
65
|
+
password: str | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
self.session = session
|
|
68
|
+
self.api_id = api_id or int(os.environ.get("API_ID", 0)) or None
|
|
69
|
+
self.api_hash = api_hash or os.environ.get("API_HASH")
|
|
70
|
+
self.bot_token = bot_token or os.environ.get("BOT_TOKEN")
|
|
71
|
+
self._phone = phone
|
|
72
|
+
self._password = password
|
|
73
|
+
self._raw: _RustClient | None = None
|
|
74
|
+
|
|
75
|
+
self._handlers: dict[str, list[_Handler]] = {e: [] for e in _ALL_EVENTS}
|
|
76
|
+
|
|
77
|
+
def _require_creds(self) -> tuple[int, str]:
|
|
78
|
+
if not self.api_id or not self.api_hash:
|
|
79
|
+
raise ValueError("api_id and api_hash required.")
|
|
80
|
+
return self.api_id, self.api_hash
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def _client(self) -> _RustClient:
|
|
84
|
+
if self._raw is None:
|
|
85
|
+
raise RuntimeError("Call await app.start() first.")
|
|
86
|
+
return self._raw
|
|
87
|
+
|
|
88
|
+
# handler decorators
|
|
89
|
+
|
|
90
|
+
def on_message(self, *filters: Callable) -> Callable:
|
|
91
|
+
"""Decorator: handle incoming messages."""
|
|
92
|
+
def decorator(func: Callable) -> Callable:
|
|
93
|
+
self._handlers["message"].append((func, list(filters)))
|
|
94
|
+
return func
|
|
95
|
+
return decorator
|
|
96
|
+
|
|
97
|
+
def on_edited_message(self, *filters: Callable) -> Callable:
|
|
98
|
+
"""Decorator: handle edited messages."""
|
|
99
|
+
def decorator(func: Callable) -> Callable:
|
|
100
|
+
self._handlers["edited_message"].append((func, list(filters)))
|
|
101
|
+
return func
|
|
102
|
+
return decorator
|
|
103
|
+
|
|
104
|
+
def on_message_deleted(self, *filters: Callable) -> Callable:
|
|
105
|
+
"""Decorator: handle message deletions."""
|
|
106
|
+
def decorator(func: Callable) -> Callable:
|
|
107
|
+
self._handlers["message_deleted"].append((func, list(filters)))
|
|
108
|
+
return func
|
|
109
|
+
return decorator
|
|
110
|
+
|
|
111
|
+
def on_callback_query(self, *filters: Callable) -> Callable:
|
|
112
|
+
"""Decorator: handle inline button presses."""
|
|
113
|
+
def decorator(func: Callable) -> Callable:
|
|
114
|
+
self._handlers["callback_query"].append((func, list(filters)))
|
|
115
|
+
return func
|
|
116
|
+
return decorator
|
|
117
|
+
|
|
118
|
+
def on_inline_query(self, *filters: Callable) -> Callable:
|
|
119
|
+
"""Decorator: handle @bot inline queries (bots only)."""
|
|
120
|
+
def decorator(func: Callable) -> Callable:
|
|
121
|
+
self._handlers["inline_query"].append((func, list(filters)))
|
|
122
|
+
return func
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
def on_inline_send(self, *filters: Callable) -> Callable:
|
|
126
|
+
"""Decorator: user chose an inline result (bots only)."""
|
|
127
|
+
def decorator(func: Callable) -> Callable:
|
|
128
|
+
self._handlers["inline_send"].append((func, list(filters)))
|
|
129
|
+
return func
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
def on_user_status(self, *filters: Callable) -> Callable:
|
|
133
|
+
"""Decorator: user came online or went offline."""
|
|
134
|
+
def decorator(func: Callable) -> Callable:
|
|
135
|
+
self._handlers["user_status"].append((func, list(filters)))
|
|
136
|
+
return func
|
|
137
|
+
return decorator
|
|
138
|
+
|
|
139
|
+
def on_chat_action(self, *filters: Callable) -> Callable:
|
|
140
|
+
"""Decorator: user is typing / uploading / recording."""
|
|
141
|
+
def decorator(func: Callable) -> Callable:
|
|
142
|
+
self._handlers["chat_action"].append((func, list(filters)))
|
|
143
|
+
return func
|
|
144
|
+
return decorator
|
|
145
|
+
|
|
146
|
+
def on_participant_update(self, *filters: Callable) -> Callable:
|
|
147
|
+
"""Decorator: member joined, left, was promoted, banned, etc."""
|
|
148
|
+
def decorator(func: Callable) -> Callable:
|
|
149
|
+
self._handlers["participant_update"].append((func, list(filters)))
|
|
150
|
+
return func
|
|
151
|
+
return decorator
|
|
152
|
+
|
|
153
|
+
def on_join_request(self, *filters: Callable) -> Callable:
|
|
154
|
+
"""Decorator: user requested to join via invite link (bots only)."""
|
|
155
|
+
def decorator(func: Callable) -> Callable:
|
|
156
|
+
self._handlers["join_request"].append((func, list(filters)))
|
|
157
|
+
return func
|
|
158
|
+
return decorator
|
|
159
|
+
|
|
160
|
+
def on_message_reaction(self, *filters: Callable) -> Callable:
|
|
161
|
+
"""Decorator: reaction added/removed on a bot message (bots only)."""
|
|
162
|
+
def decorator(func: Callable) -> Callable:
|
|
163
|
+
self._handlers["message_reaction"].append((func, list(filters)))
|
|
164
|
+
return func
|
|
165
|
+
return decorator
|
|
166
|
+
|
|
167
|
+
def on_poll_vote(self, *filters: Callable) -> Callable:
|
|
168
|
+
"""Decorator: user voted in a poll sent by the bot (bots only)."""
|
|
169
|
+
def decorator(func: Callable) -> Callable:
|
|
170
|
+
self._handlers["poll_vote"].append((func, list(filters)))
|
|
171
|
+
return func
|
|
172
|
+
return decorator
|
|
173
|
+
|
|
174
|
+
def on_bot_stopped(self, *filters: Callable) -> Callable:
|
|
175
|
+
"""Decorator: user stopped or restarted the bot."""
|
|
176
|
+
def decorator(func: Callable) -> Callable:
|
|
177
|
+
self._handlers["bot_stopped"].append((func, list(filters)))
|
|
178
|
+
return func
|
|
179
|
+
return decorator
|
|
180
|
+
|
|
181
|
+
def on_raw_update(self, *filters: Callable) -> Callable:
|
|
182
|
+
"""Decorator: receive RawUpdate for any TL update not mapped to a typed event.
|
|
183
|
+
|
|
184
|
+
Useful for handling obscure update types not yet covered by dedicated
|
|
185
|
+
handlers. The update object has .constructor_id (u32) and .type_name (str).
|
|
186
|
+
"""
|
|
187
|
+
def decorator(func: Callable) -> Callable:
|
|
188
|
+
self._handlers["raw_update"].append((func, list(filters)))
|
|
189
|
+
return func
|
|
190
|
+
return decorator
|
|
191
|
+
|
|
192
|
+
# dispatch
|
|
193
|
+
|
|
194
|
+
async def _dispatch(self, event_type: str, update: Any) -> None:
|
|
195
|
+
for func, fltrs in self._handlers.get(event_type, []):
|
|
196
|
+
if all(f(update) for f in fltrs):
|
|
197
|
+
try:
|
|
198
|
+
result = func(self, update)
|
|
199
|
+
if inspect.isawaitable(result):
|
|
200
|
+
await result
|
|
201
|
+
except Exception as exc:
|
|
202
|
+
_log.error("handler error in %s: %s", event_type, exc, exc_info=True)
|
|
203
|
+
|
|
204
|
+
# update loop
|
|
205
|
+
|
|
206
|
+
async def _run_updates(self) -> None:
|
|
207
|
+
_log.debug("update loop started")
|
|
208
|
+
while True:
|
|
209
|
+
result = await self._client.next_update()
|
|
210
|
+
if result is None:
|
|
211
|
+
_log.debug("update stream closed")
|
|
212
|
+
break
|
|
213
|
+
event_type, update = result
|
|
214
|
+
_log.debug("dispatching %s", event_type)
|
|
215
|
+
asyncio.create_task(self._dispatch(event_type, update))
|
|
216
|
+
|
|
217
|
+
# lifecycle
|
|
218
|
+
|
|
219
|
+
async def start(self) -> "Client":
|
|
220
|
+
if self._raw is not None:
|
|
221
|
+
return self
|
|
222
|
+
api_id, api_hash = self._require_creds()
|
|
223
|
+
_log.info("connecting (session=%r)", self.session + (".session" if not self.session.endswith(".session") else ""))
|
|
224
|
+
session_path = self.session if self.session.endswith(".session") else self.session + ".session"
|
|
225
|
+
self._raw = await _RustClient.builder(api_id, api_hash, session_path).connect()
|
|
226
|
+
if not await self._raw.is_authorized():
|
|
227
|
+
if self.bot_token:
|
|
228
|
+
await self._raw.bot_sign_in(self.bot_token)
|
|
229
|
+
_log.info("signed in as bot")
|
|
230
|
+
else:
|
|
231
|
+
await self._interactive_login()
|
|
232
|
+
await self._raw.save_session()
|
|
233
|
+
else:
|
|
234
|
+
_log.info("reusing existing session")
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
async def _interactive_login(self) -> None:
|
|
238
|
+
phone = self._phone or input("Phone (+countrycode): ")
|
|
239
|
+
token = await self._client.request_login_code(phone)
|
|
240
|
+
pw_token = await self._client.sign_in(token, input("Code: "))
|
|
241
|
+
if pw_token is not None:
|
|
242
|
+
hint = pw_token.hint
|
|
243
|
+
pwd = self._password or input(f"2FA password (hint: {hint}): " if hint else "2FA password: ")
|
|
244
|
+
await self._client.check_password(pw_token, pwd)
|
|
245
|
+
_log.info("signed in as user")
|
|
246
|
+
|
|
247
|
+
async def stop(self) -> None:
|
|
248
|
+
if self._raw:
|
|
249
|
+
await self._raw.sign_out()
|
|
250
|
+
self._raw = None
|
|
251
|
+
_log.info("signed out")
|
|
252
|
+
|
|
253
|
+
async def run_until_disconnected(self) -> None:
|
|
254
|
+
await self.start()
|
|
255
|
+
try:
|
|
256
|
+
await self._run_updates()
|
|
257
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
def run(self) -> None:
|
|
261
|
+
"""Blocking run: start, dispatch updates, stop on Ctrl-C."""
|
|
262
|
+
try:
|
|
263
|
+
asyncio.run(self.run_until_disconnected())
|
|
264
|
+
except KeyboardInterrupt:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
async def __aenter__(self) -> "Client":
|
|
268
|
+
return await self.start()
|
|
269
|
+
|
|
270
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
# messaging
|
|
274
|
+
|
|
275
|
+
async def send_message(self, peer: str, text: str) -> Message:
|
|
276
|
+
return await self._client.send_message(peer, text)
|
|
277
|
+
|
|
278
|
+
async def send_html(self, peer: str, html: str) -> Message:
|
|
279
|
+
return await self._client.send_html(peer, html)
|
|
280
|
+
|
|
281
|
+
async def send_markdown(self, peer: str, md: str) -> Message:
|
|
282
|
+
return await self._client.send_markdown(peer, md)
|
|
283
|
+
|
|
284
|
+
async def edit_message(self, peer: str, message_id: int, new_text: str) -> None:
|
|
285
|
+
await self._client.edit_message(peer, message_id, new_text)
|
|
286
|
+
|
|
287
|
+
async def delete_message(self, message_id: int, revoke: bool = True) -> None:
|
|
288
|
+
await self._client.delete_messages([message_id], revoke)
|
|
289
|
+
|
|
290
|
+
async def delete_messages(self, message_ids: list[int], revoke: bool = True) -> None:
|
|
291
|
+
await self._client.delete_messages(message_ids, revoke)
|
|
292
|
+
|
|
293
|
+
async def forward_messages(self, destination: str, source: str, message_ids: list[int]) -> None:
|
|
294
|
+
await self._client.forward_messages(destination, source, message_ids)
|
|
295
|
+
|
|
296
|
+
async def pin_message(self, peer: str, message_id: int) -> None:
|
|
297
|
+
await self._client.pin_message(peer, message_id)
|
|
298
|
+
|
|
299
|
+
async def unpin_message(self, peer: str, message_id: int) -> None:
|
|
300
|
+
await self._client.unpin_message(peer, message_id)
|
|
301
|
+
|
|
302
|
+
async def mark_as_read(self, peer: str) -> None:
|
|
303
|
+
await self._client.mark_as_read(peer)
|
|
304
|
+
|
|
305
|
+
async def send_reaction(self, peer: str, message_id: int, emoji: str) -> None:
|
|
306
|
+
"""Send a reaction emoji to a message."""
|
|
307
|
+
await self._client.send_reaction(peer, message_id, emoji)
|
|
308
|
+
|
|
309
|
+
# media
|
|
310
|
+
|
|
311
|
+
async def send_photo(self, peer: str, path: str, caption: str = "") -> Message:
|
|
312
|
+
import os
|
|
313
|
+
if not os.path.isfile(path):
|
|
314
|
+
raise FileNotFoundError(f"No such file: {path!r}")
|
|
315
|
+
return await self._client.send_photo(peer, path, caption)
|
|
316
|
+
|
|
317
|
+
async def send_document(self, peer: str, path: str, caption: str = "", mime_type: str | None = None) -> Message:
|
|
318
|
+
import os
|
|
319
|
+
if not os.path.isfile(path):
|
|
320
|
+
raise FileNotFoundError(f"No such file: {path!r}")
|
|
321
|
+
return await self._client.send_document(peer, path, caption, mime_type)
|
|
322
|
+
|
|
323
|
+
async def send_file(self, peer: str, path: str, caption: str = "", mime_type: str | None = None) -> Message:
|
|
324
|
+
import os
|
|
325
|
+
if not os.path.isfile(path):
|
|
326
|
+
raise FileNotFoundError(f"No such file: {path!r}")
|
|
327
|
+
return await self._client.send_file(peer, path, caption, mime_type)
|
|
328
|
+
|
|
329
|
+
# account
|
|
330
|
+
|
|
331
|
+
async def get_me(self) -> User:
|
|
332
|
+
return await self._client.get_me()
|
|
333
|
+
|
|
334
|
+
async def get_dialogs(self, limit: int = 100) -> list[Dialog]:
|
|
335
|
+
return await self._client.get_dialogs(limit)
|
|
336
|
+
|
|
337
|
+
async def export_session_string(self) -> str:
|
|
338
|
+
return await self._client.export_session_string()
|
|
339
|
+
|
|
340
|
+
# raw invoke
|
|
341
|
+
|
|
342
|
+
async def invoke(self, func: Any) -> dict:
|
|
343
|
+
"""Invoke a raw TL function object. Returns a deserialized dict."""
|
|
344
|
+
tl_bytes = func.to_bytes()
|
|
345
|
+
resp_bytes = await self._client.invoke_raw(tl_bytes)
|
|
346
|
+
return _tl.deserialize(resp_bytes, _SCHEMA_BY_CID)
|
|
347
|
+
|
|
348
|
+
def __repr__(self) -> str:
|
|
349
|
+
state = "connected" if self._raw else "disconnected"
|
|
350
|
+
return f"Client(session={self.session!r}, {state})"
|
ferogram/filters.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
# Filters for use with handler decorators.
|
|
15
|
+
# Each filter is a callable: filter(update) -> bool
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import re
|
|
19
|
+
from typing import Callable, Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Filter = Callable[[Any], bool]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make(fn: Callable) -> Filter:
|
|
26
|
+
return fn
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---- message filters ----
|
|
30
|
+
|
|
31
|
+
# passes for any update
|
|
32
|
+
all = _make(lambda _: True)
|
|
33
|
+
|
|
34
|
+
# only private chats
|
|
35
|
+
private = _make(lambda m: m.from_id is not None and m.chat_id == m.from_id)
|
|
36
|
+
|
|
37
|
+
# only group/channel chats (chat_id < 0)
|
|
38
|
+
group = _make(lambda m: m.chat_id < 0)
|
|
39
|
+
|
|
40
|
+
# message has text
|
|
41
|
+
text = _make(lambda m: bool(getattr(m, "text", None)))
|
|
42
|
+
|
|
43
|
+
# message has a photo
|
|
44
|
+
photo = _make(lambda m: getattr(m, "has_photo", False))
|
|
45
|
+
|
|
46
|
+
# message has any media
|
|
47
|
+
media = _make(lambda m: getattr(m, "has_media", False))
|
|
48
|
+
|
|
49
|
+
# outgoing message
|
|
50
|
+
outgoing = _make(lambda m: getattr(m, "outgoing", False))
|
|
51
|
+
|
|
52
|
+
# incoming message
|
|
53
|
+
incoming = _make(lambda m: not getattr(m, "outgoing", True))
|
|
54
|
+
|
|
55
|
+
# bot was mentioned in the message
|
|
56
|
+
mentioned = _make(lambda m: getattr(m, "mentioned", False))
|
|
57
|
+
|
|
58
|
+
# message is part of an album/grouped media
|
|
59
|
+
album = _make(lambda m: getattr(m, "grouped_id", None) is not None)
|
|
60
|
+
|
|
61
|
+
# message is a reply to another message
|
|
62
|
+
reply = _make(lambda m: getattr(m, "reply_to_message_id", None) is not None)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def command(*names: str, prefix: str = "/") -> Filter:
|
|
66
|
+
"""Match bot commands: /start, /help, etc."""
|
|
67
|
+
lower = {n.lstrip(prefix).lower() for n in names}
|
|
68
|
+
def check(m: Any) -> bool:
|
|
69
|
+
t = getattr(m, "text", None) or ""
|
|
70
|
+
if not t.startswith(prefix):
|
|
71
|
+
return False
|
|
72
|
+
cmd = t[len(prefix):].split()[0].split("@")[0].lower()
|
|
73
|
+
return cmd in lower
|
|
74
|
+
return check
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def regex(pattern: str | re.Pattern, flags: int = 0) -> Filter:
|
|
78
|
+
"""Match message text against a regex."""
|
|
79
|
+
compiled = re.compile(pattern, flags) if isinstance(pattern, str) else pattern
|
|
80
|
+
def check(m: Any) -> bool:
|
|
81
|
+
t = getattr(m, "text", None) or ""
|
|
82
|
+
return bool(compiled.search(t))
|
|
83
|
+
return check
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def user(*user_ids: int) -> Filter:
|
|
87
|
+
"""Only pass updates from specific user ids."""
|
|
88
|
+
ids = set(user_ids)
|
|
89
|
+
return _make(lambda m: getattr(m, "from_id", None) in ids)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def chat(*chat_ids: int) -> Filter:
|
|
93
|
+
"""Only pass updates from specific chat ids."""
|
|
94
|
+
ids = set(chat_ids)
|
|
95
|
+
return _make(lambda m: getattr(m, "chat_id", None) in ids)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---- callback query filters ----
|
|
99
|
+
|
|
100
|
+
def data(value: str) -> Filter:
|
|
101
|
+
"""Match callback_query data exactly."""
|
|
102
|
+
return _make(lambda q: getattr(q, "data", None) == value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def data_regex(pattern: str | re.Pattern, flags: int = 0) -> Filter:
|
|
106
|
+
"""Match callback_query data against a regex."""
|
|
107
|
+
compiled = re.compile(pattern, flags) if isinstance(pattern, str) else pattern
|
|
108
|
+
return _make(lambda q: bool(compiled.search(getattr(q, "data", "") or "")))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---- inline query filters ----
|
|
112
|
+
|
|
113
|
+
def inline(pattern: str | re.Pattern | None = None, flags: int = 0) -> Filter:
|
|
114
|
+
"""Match inline query text. Pass None to match any inline query."""
|
|
115
|
+
if pattern is None:
|
|
116
|
+
return _make(lambda q: True)
|
|
117
|
+
compiled = re.compile(pattern, flags) if isinstance(pattern, str) else pattern
|
|
118
|
+
return _make(lambda q: bool(compiled.search(getattr(q, "query", "") or "")))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---- user status filters ----
|
|
122
|
+
|
|
123
|
+
online = _make(lambda s: getattr(s, "online", False))
|
|
124
|
+
offline = _make(lambda s: not getattr(s, "online", True))
|
|
125
|
+
|
|
126
|
+
def status(value: str) -> Filter:
|
|
127
|
+
"""Match specific status string: 'online', 'offline', 'recently', etc."""
|
|
128
|
+
return _make(lambda s: getattr(s, "status", None) == value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---- chat action filters ----
|
|
132
|
+
|
|
133
|
+
def action(name: str) -> Filter:
|
|
134
|
+
"""Match a specific chat action string, e.g. 'typing', 'upload_photo'."""
|
|
135
|
+
return _make(lambda a: getattr(a, "action", None) == name)
|
|
136
|
+
|
|
137
|
+
typing = action("typing")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ---- reaction filters ----
|
|
141
|
+
|
|
142
|
+
def reaction(*emojis: str) -> Filter:
|
|
143
|
+
"""Match any of the given emoji in new_reactions."""
|
|
144
|
+
s = set(emojis)
|
|
145
|
+
return _make(lambda r: bool(set(getattr(r, "new_reactions", [])) & s))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---- raw update filters ----
|
|
149
|
+
|
|
150
|
+
def constructor(cid: int) -> Filter:
|
|
151
|
+
"""Match a RawUpdate by constructor_id (hex int, e.g. 0x9e84bc99)."""
|
|
152
|
+
return _make(lambda r: getattr(r, "constructor_id", None) == cid)
|
|
153
|
+
|
|
154
|
+
def update_type(name: str) -> Filter:
|
|
155
|
+
"""Match a RawUpdate by type_name string."""
|
|
156
|
+
return _make(lambda r: getattr(r, "type_name", None) == name)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ---- logic combinators ----
|
|
160
|
+
|
|
161
|
+
def and_(*filters: Filter) -> Filter:
|
|
162
|
+
return _make(lambda m: all(f(m) for f in filters))
|
|
163
|
+
|
|
164
|
+
def or_(*filters: Filter) -> Filter:
|
|
165
|
+
return _make(lambda m: any(f(m) for f in filters))
|
|
166
|
+
|
|
167
|
+
def not_(f: Filter) -> Filter:
|
|
168
|
+
return _make(lambda m: not f(m))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# aliases
|
|
172
|
+
AND = and_
|
|
173
|
+
OR = or_
|
|
174
|
+
NOT = not_
|
ferogram/logging.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
_LOG = logging.getLogger("ferogram")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def setup(level: int = logging.INFO, fmt: str | None = None) -> None:
|
|
21
|
+
"""Configure ferogram's logger to write to stderr.
|
|
22
|
+
|
|
23
|
+
Call once at startup, before app.run(), if you want log output.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
level : logging level constant, e.g. logging.DEBUG / logging.INFO
|
|
28
|
+
fmt : optional format string; defaults to a sensible one-line format
|
|
29
|
+
"""
|
|
30
|
+
if _LOG.handlers:
|
|
31
|
+
return
|
|
32
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
33
|
+
handler.setFormatter(logging.Formatter(
|
|
34
|
+
fmt or "%(asctime)s [%(levelname)s] ferogram: %(message)s",
|
|
35
|
+
datefmt="%H:%M:%S",
|
|
36
|
+
))
|
|
37
|
+
_LOG.addHandler(handler)
|
|
38
|
+
_LOG.setLevel(level)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_logger() -> logging.Logger:
|
|
42
|
+
return _LOG
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# module-level shortcuts
|
|
46
|
+
debug = _LOG.debug
|
|
47
|
+
info = _LOG.info
|
|
48
|
+
warning = _LOG.warning
|
|
49
|
+
error = _LOG.error
|
ferogram/py.typed
ADDED
|
File without changes
|
ferogram/raw/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
# ferogram.raw - direct access to the Telegram API
|
|
15
|
+
# Public import path:
|
|
16
|
+
# from ferogram.raw.api.functions import GetHistory
|
|
17
|
+
# from ferogram.raw.api.types import InputPeerUsername
|
|
18
|
+
|
|
19
|
+
from . import tl as _tl
|
|
20
|
+
from . import api
|
|
21
|
+
from .generated import functions, types
|
|
22
|
+
|
|
23
|
+
__all__ = ["api", "functions", "types", "_tl"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
from .functions import * # noqa
|
|
15
|
+
from .types import * # noqa
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
# Public import path for TL functions.
|
|
15
|
+
# Use: from ferogram.raw.api.functions import GetHistory
|
|
16
|
+
from ..generated.functions import * # noqa
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
|
|
2
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
# ferogram is a high-performance Telegram MTProto framework written in Rust.
|
|
5
|
+
# ferogram-py provides Python bindings built on top of the Rust core for
|
|
6
|
+
# building Telegram clients, bots, and applications with a simple API.
|
|
7
|
+
#
|
|
8
|
+
# Rust core: https://github.com/ankit-chaubey/ferogram
|
|
9
|
+
# Python bindings: https://github.com/ankit-chaubey/ferogram-py
|
|
10
|
+
#
|
|
11
|
+
# If you use or modify this code, keep this notice at the top of the file
|
|
12
|
+
# and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
|
|
13
|
+
|
|
14
|
+
# Public import path for TL types.
|
|
15
|
+
# Use: from ferogram.raw.api.types import InputPeerUsername
|
|
16
|
+
from ..generated.types import * # noqa
|