nicotin 0.1.0__tar.gz
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.
- nicotin-0.1.0/PKG-INFO +68 -0
- nicotin-0.1.0/README.md +60 -0
- nicotin-0.1.0/nicotin/__init__.py +47 -0
- nicotin-0.1.0/nicotin/client.py +375 -0
- nicotin-0.1.0/nicotin/enums/__init__.py +76 -0
- nicotin-0.1.0/nicotin/errors/__init__.py +71 -0
- nicotin-0.1.0/nicotin/filters.py +141 -0
- nicotin-0.1.0/nicotin/handlers/__init__.py +49 -0
- nicotin-0.1.0/nicotin/network/__init__.py +3 -0
- nicotin-0.1.0/nicotin/network/session.py +111 -0
- nicotin-0.1.0/nicotin/types/__init__.py +25 -0
- nicotin-0.1.0/nicotin/types/callback_query.py +34 -0
- nicotin-0.1.0/nicotin/types/chat.py +78 -0
- nicotin-0.1.0/nicotin/types/file.py +62 -0
- nicotin-0.1.0/nicotin/types/message.py +153 -0
- nicotin-0.1.0/nicotin/types/object.py +30 -0
- nicotin-0.1.0/nicotin/types/update.py +30 -0
- nicotin-0.1.0/nicotin/types/user.py +68 -0
- nicotin-0.1.0/nicotin.egg-info/PKG-INFO +68 -0
- nicotin-0.1.0/nicotin.egg-info/SOURCES.txt +23 -0
- nicotin-0.1.0/nicotin.egg-info/dependency_links.txt +1 -0
- nicotin-0.1.0/nicotin.egg-info/requires.txt +1 -0
- nicotin-0.1.0/nicotin.egg-info/top_level.txt +1 -0
- nicotin-0.1.0/pyproject.toml +16 -0
- nicotin-0.1.0/setup.cfg +4 -0
nicotin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nicotin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An async Rubika library with a Pyrogram-style API surface.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.24
|
|
8
|
+
|
|
9
|
+
# NICOTIN
|
|
10
|
+
|
|
11
|
+
یک کتابخانهی async برای روبیکا (Rubika) که **عمداً** از نظر نام متدها، پراپرتیها، فیلترها، ساختار Handler و حتی سبک مستندات، شبیه کتابخانههای معروف تلگرام (بهخصوص Pyrogram) طراحی شده — تا هر کسی که با باتنویسی تلگرام آشناست، بدون یادگیری از صفر بتواند برای روبیکا هم بات بنویسد.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## شروع سریع
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from nicotin import Client, filters
|
|
21
|
+
|
|
22
|
+
app = Client("my_account", auth="AUTH_KEY_INJA")
|
|
23
|
+
|
|
24
|
+
@app.on_message(filters.command("start"))
|
|
25
|
+
async def start(client, message):
|
|
26
|
+
await message.reply("سلام از نیکوتین 👋")
|
|
27
|
+
|
|
28
|
+
@app.on_message(filters.text & filters.private)
|
|
29
|
+
async def echo(client, message):
|
|
30
|
+
await message.reply(message.text)
|
|
31
|
+
|
|
32
|
+
app.run()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## ساختار پروژه (مثل Pyrogram)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
nicotin/
|
|
39
|
+
├── client.py # کلاس Client — متدهای send_message, send_photo, edit_message_text, ...
|
|
40
|
+
├── types/ # Message, User, Chat, File, CallbackQuery, Update
|
|
41
|
+
├── filters.py # filters.text, filters.command(), filters.regex(), ترکیب با & | ~
|
|
42
|
+
├── handlers/ # MessageHandler, CallbackQueryHandler, ...
|
|
43
|
+
├── enums/ # ChatType, MessageType, ChatActivity, ParseMode
|
|
44
|
+
├── errors/ # NicotinError, RPCError, FloodWait, AuthError, ...
|
|
45
|
+
└── network/ # Session — لایهی ارتباط HTTP با API روبیکا
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## نگاشت متدها (Telegram ↔ Rubika)
|
|
49
|
+
|
|
50
|
+
| در NICOTIN | مشابه در Pyrogram | چه کاری میکند در روبیکا |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `Client.send_message()` | `Client.send_message()` | ارسال پیام متنی |
|
|
53
|
+
| `Client.send_photo()` / `send_video()` / `send_document()` | همنام | ارسال رسانه |
|
|
54
|
+
| `Client.edit_message_text()` | همنام | ویرایش پیام |
|
|
55
|
+
| `Client.delete_messages()` | همنام | حذف پیام (Local/Global) |
|
|
56
|
+
| `Client.get_chat_history()` | همنام | گرفتن تاریخچهی پیامها |
|
|
57
|
+
| `Client.ban_chat_member()` | همنام | حذف/بن عضو گروه |
|
|
58
|
+
| `message.reply()` | همنام | ریپلای به همان پیام |
|
|
59
|
+
| `filters.text/photo/command()` | همنام | فیلتر روی نوع/محتوای پیام |
|
|
60
|
+
| `@app.on_message()` | همنام | ثبت هندلر پیام |
|
|
61
|
+
|
|
62
|
+
## نکتهی مهم دربارهی لایهی شبکه
|
|
63
|
+
|
|
64
|
+
`nicotin/network/session.py` اسکلت درخواستهای HTTPS به API روبیکا را میسازد، اما رمزنگاری واقعی AES بدنهی درخواست/پاسخ (که روبیکا برای `data_enc` نیاز دارد) بهصورت عمدی بهعنوان دو متد قابل جایگزینی (`Session.encrypt` / `Session.decrypt`) گذاشته شده — چون آن بخش وابسته به کلید احراز هویت واقعی هر اکانت است. برای استفادهی واقعی، این دو متد را با `pycryptodome` کامل کنید (کلید AES از هش auth شما مشتق میشود).
|
|
65
|
+
|
|
66
|
+
## مجوز
|
|
67
|
+
|
|
68
|
+
MIT
|
nicotin-0.1.0/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# NICOTIN
|
|
2
|
+
|
|
3
|
+
یک کتابخانهی async برای روبیکا (Rubika) که **عمداً** از نظر نام متدها، پراپرتیها، فیلترها، ساختار Handler و حتی سبک مستندات، شبیه کتابخانههای معروف تلگرام (بهخصوص Pyrogram) طراحی شده — تا هر کسی که با باتنویسی تلگرام آشناست، بدون یادگیری از صفر بتواند برای روبیکا هم بات بنویسد.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install -e .
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## شروع سریع
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from nicotin import Client, filters
|
|
13
|
+
|
|
14
|
+
app = Client("my_account", auth="AUTH_KEY_INJA")
|
|
15
|
+
|
|
16
|
+
@app.on_message(filters.command("start"))
|
|
17
|
+
async def start(client, message):
|
|
18
|
+
await message.reply("سلام از نیکوتین 👋")
|
|
19
|
+
|
|
20
|
+
@app.on_message(filters.text & filters.private)
|
|
21
|
+
async def echo(client, message):
|
|
22
|
+
await message.reply(message.text)
|
|
23
|
+
|
|
24
|
+
app.run()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## ساختار پروژه (مثل Pyrogram)
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
nicotin/
|
|
31
|
+
├── client.py # کلاس Client — متدهای send_message, send_photo, edit_message_text, ...
|
|
32
|
+
├── types/ # Message, User, Chat, File, CallbackQuery, Update
|
|
33
|
+
├── filters.py # filters.text, filters.command(), filters.regex(), ترکیب با & | ~
|
|
34
|
+
├── handlers/ # MessageHandler, CallbackQueryHandler, ...
|
|
35
|
+
├── enums/ # ChatType, MessageType, ChatActivity, ParseMode
|
|
36
|
+
├── errors/ # NicotinError, RPCError, FloodWait, AuthError, ...
|
|
37
|
+
└── network/ # Session — لایهی ارتباط HTTP با API روبیکا
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## نگاشت متدها (Telegram ↔ Rubika)
|
|
41
|
+
|
|
42
|
+
| در NICOTIN | مشابه در Pyrogram | چه کاری میکند در روبیکا |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `Client.send_message()` | `Client.send_message()` | ارسال پیام متنی |
|
|
45
|
+
| `Client.send_photo()` / `send_video()` / `send_document()` | همنام | ارسال رسانه |
|
|
46
|
+
| `Client.edit_message_text()` | همنام | ویرایش پیام |
|
|
47
|
+
| `Client.delete_messages()` | همنام | حذف پیام (Local/Global) |
|
|
48
|
+
| `Client.get_chat_history()` | همنام | گرفتن تاریخچهی پیامها |
|
|
49
|
+
| `Client.ban_chat_member()` | همنام | حذف/بن عضو گروه |
|
|
50
|
+
| `message.reply()` | همنام | ریپلای به همان پیام |
|
|
51
|
+
| `filters.text/photo/command()` | همنام | فیلتر روی نوع/محتوای پیام |
|
|
52
|
+
| `@app.on_message()` | همنام | ثبت هندلر پیام |
|
|
53
|
+
|
|
54
|
+
## نکتهی مهم دربارهی لایهی شبکه
|
|
55
|
+
|
|
56
|
+
`nicotin/network/session.py` اسکلت درخواستهای HTTPS به API روبیکا را میسازد، اما رمزنگاری واقعی AES بدنهی درخواست/پاسخ (که روبیکا برای `data_enc` نیاز دارد) بهصورت عمدی بهعنوان دو متد قابل جایگزینی (`Session.encrypt` / `Session.decrypt`) گذاشته شده — چون آن بخش وابسته به کلید احراز هویت واقعی هر اکانت است. برای استفادهی واقعی، این دو متد را با `pycryptodome` کامل کنید (کلید AES از هش auth شما مشتق میشود).
|
|
57
|
+
|
|
58
|
+
## مجوز
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NICOTIN
|
|
3
|
+
~~~~~~~
|
|
4
|
+
|
|
5
|
+
An async Python library for the Rubika messenger, with an API surface
|
|
6
|
+
(method names, property names, filters, handler system, docs style)
|
|
7
|
+
deliberately modeled after the Telegram bot ecosystem (Pyrogram /
|
|
8
|
+
python-telegram-bot), so anyone coming from Telegram bot development
|
|
9
|
+
feels at home immediately.
|
|
10
|
+
|
|
11
|
+
from nicotin import Client, filters
|
|
12
|
+
|
|
13
|
+
app = Client("my_account", auth="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
|
|
14
|
+
|
|
15
|
+
@app.on_message(filters.text & filters.private)
|
|
16
|
+
async def echo(client, message):
|
|
17
|
+
await message.reply(message.text)
|
|
18
|
+
|
|
19
|
+
app.run()
|
|
20
|
+
|
|
21
|
+
:copyright: NICOTIN contributors
|
|
22
|
+
:license: MIT
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from .client import Client
|
|
26
|
+
from .types import Update, Message, User, Chat, File, CallbackQuery
|
|
27
|
+
from . import filters
|
|
28
|
+
from . import enums
|
|
29
|
+
from .errors import NicotinError, RPCError, AuthError, FloodWait
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Client",
|
|
33
|
+
"Update",
|
|
34
|
+
"Message",
|
|
35
|
+
"User",
|
|
36
|
+
"Chat",
|
|
37
|
+
"File",
|
|
38
|
+
"CallbackQuery",
|
|
39
|
+
"filters",
|
|
40
|
+
"enums",
|
|
41
|
+
"NicotinError",
|
|
42
|
+
"RPCError",
|
|
43
|
+
"AuthError",
|
|
44
|
+
"FloodWait",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nicotin.client
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
The :class:`Client` class: NICOTIN's single entry point, deliberately
|
|
6
|
+
shaped like ``pyrogram.Client`` — same method names, same decorator
|
|
7
|
+
style, same ``run()`` convenience, same "everything is a coroutine"
|
|
8
|
+
philosophy — but talking to Rubika instead of Telegram.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import inspect
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from .network import Session
|
|
19
|
+
from .types import Message, Chat, User, File, CallbackQuery, Update
|
|
20
|
+
from .enums import ChatType, ChatActivity, ParseMode
|
|
21
|
+
from .filters import Filter, all as filters_all
|
|
22
|
+
from .handlers import (
|
|
23
|
+
Handler,
|
|
24
|
+
MessageHandler,
|
|
25
|
+
EditedMessageHandler,
|
|
26
|
+
DeletedMessagesHandler,
|
|
27
|
+
CallbackQueryHandler,
|
|
28
|
+
)
|
|
29
|
+
from .errors import AuthError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Client:
|
|
33
|
+
"""
|
|
34
|
+
NICOTIN's main class — one instance per Rubika account/bot.
|
|
35
|
+
|
|
36
|
+
:param name: a local session name, used for the ``{name}.session`` file.
|
|
37
|
+
:param auth: the account's auth key. If omitted, NICOTIN will look
|
|
38
|
+
for a saved ``{name}.session`` file on disk.
|
|
39
|
+
:param platform: reported client platform (``"web"``, ``"android"``, ``"rubx"``).
|
|
40
|
+
:param workers: number of concurrent update-dispatch workers, exactly
|
|
41
|
+
like Pyrogram's own ``workers`` parameter.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
from nicotin import Client, filters
|
|
46
|
+
|
|
47
|
+
app = Client("my_account", auth="...")
|
|
48
|
+
|
|
49
|
+
@app.on_message(filters.command("start"))
|
|
50
|
+
async def start(client, message):
|
|
51
|
+
await message.reply("سلام از نیکوتین 👋")
|
|
52
|
+
|
|
53
|
+
app.run()
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
name: str,
|
|
59
|
+
auth: str | None = None,
|
|
60
|
+
*,
|
|
61
|
+
platform: str = "web",
|
|
62
|
+
workers: int = 4,
|
|
63
|
+
session_dir: str = ".",
|
|
64
|
+
):
|
|
65
|
+
self.name = name
|
|
66
|
+
self.workers = workers
|
|
67
|
+
self.session_dir = Path(session_dir)
|
|
68
|
+
self._auth = auth or self._load_session()
|
|
69
|
+
if not self._auth:
|
|
70
|
+
raise AuthError(
|
|
71
|
+
f"No auth key provided and no session file found for '{name}'. "
|
|
72
|
+
f"Pass auth=... on first run."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self.session = Session(self._auth, platform=platform)
|
|
76
|
+
self._handlers: list[Handler] = []
|
|
77
|
+
self._polling_task: asyncio.Task | None = None
|
|
78
|
+
self.me: User | None = None
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Session persistence
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def _session_path(self) -> Path:
|
|
85
|
+
return self.session_dir / f"{self.name}.session"
|
|
86
|
+
|
|
87
|
+
def _load_session(self) -> str | None:
|
|
88
|
+
path = self._session_path()
|
|
89
|
+
return path.read_text().strip() if path.exists() else None
|
|
90
|
+
|
|
91
|
+
def _save_session(self):
|
|
92
|
+
self._session_path().write_text(self._auth)
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
# Lifecycle — mirrors pyrogram.Client.start / stop / run / idle
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
async def start(self) -> "Client":
|
|
99
|
+
"""Connect and begin listening for updates. Returns ``self`` for chaining."""
|
|
100
|
+
self._save_session()
|
|
101
|
+
self.me = await self.get_me()
|
|
102
|
+
self._polling_task = asyncio.create_task(self._update_loop())
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def stop(self):
|
|
106
|
+
"""Disconnect and stop dispatching updates."""
|
|
107
|
+
if self._polling_task:
|
|
108
|
+
self._polling_task.cancel()
|
|
109
|
+
await self.session.close()
|
|
110
|
+
|
|
111
|
+
def run(self, coroutine=None):
|
|
112
|
+
"""
|
|
113
|
+
Blocking helper exactly like ``pyrogram.Client.run()``: starts the
|
|
114
|
+
client, optionally awaits ``coroutine``, then idles until Ctrl+C.
|
|
115
|
+
"""
|
|
116
|
+
async def _runner():
|
|
117
|
+
await self.start()
|
|
118
|
+
try:
|
|
119
|
+
if coroutine:
|
|
120
|
+
await coroutine
|
|
121
|
+
else:
|
|
122
|
+
await self._idle()
|
|
123
|
+
finally:
|
|
124
|
+
await self.stop()
|
|
125
|
+
|
|
126
|
+
asyncio.run(_runner())
|
|
127
|
+
|
|
128
|
+
async def _idle(self):
|
|
129
|
+
try:
|
|
130
|
+
while True:
|
|
131
|
+
await asyncio.sleep(3600)
|
|
132
|
+
except asyncio.CancelledError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
async def _update_loop(self):
|
|
136
|
+
"""Long-polls for new updates and dispatches them to handlers."""
|
|
137
|
+
while True:
|
|
138
|
+
try:
|
|
139
|
+
raw_updates = await self.session.invoke("getUpdates", {"limit": 100})
|
|
140
|
+
for raw in raw_updates.get("updates", []):
|
|
141
|
+
update = self._parse_update(raw)
|
|
142
|
+
await self._dispatch(update)
|
|
143
|
+
except asyncio.CancelledError:
|
|
144
|
+
raise
|
|
145
|
+
except Exception:
|
|
146
|
+
# A production client would log this; kept silent here
|
|
147
|
+
# to match a minimal skeleton.
|
|
148
|
+
pass
|
|
149
|
+
await asyncio.sleep(1)
|
|
150
|
+
|
|
151
|
+
def _parse_update(self, raw: dict) -> Update:
|
|
152
|
+
update_type = raw.get("type")
|
|
153
|
+
chat_id = raw.get("object_guid", "")
|
|
154
|
+
|
|
155
|
+
if update_type == "NewMessage":
|
|
156
|
+
return Update(message=Message._parse(self, raw.get("message", {}), chat_id))
|
|
157
|
+
if update_type == "EditedMessage":
|
|
158
|
+
return Update(edited_message=Message._parse(self, raw.get("message", {}), chat_id))
|
|
159
|
+
if update_type == "RemovedMessage":
|
|
160
|
+
return Update(deleted_message_ids=raw.get("message_ids", []))
|
|
161
|
+
if update_type == "CallbackQuery":
|
|
162
|
+
msg = Message._parse(self, raw.get("message", {}), chat_id)
|
|
163
|
+
cq = CallbackQuery(
|
|
164
|
+
client=self,
|
|
165
|
+
id=raw.get("callback_id", ""),
|
|
166
|
+
from_user=User(client=self, id=raw.get("user_guid", ""), first_name=""),
|
|
167
|
+
message=msg,
|
|
168
|
+
data=raw.get("data", ""),
|
|
169
|
+
)
|
|
170
|
+
return Update(callback_query=cq)
|
|
171
|
+
return Update()
|
|
172
|
+
|
|
173
|
+
async def _dispatch(self, update: Update):
|
|
174
|
+
target = (
|
|
175
|
+
update.message
|
|
176
|
+
or update.edited_message
|
|
177
|
+
or update.callback_query
|
|
178
|
+
or update.deleted_message_ids
|
|
179
|
+
)
|
|
180
|
+
for handler in self._handlers:
|
|
181
|
+
if update.message and isinstance(handler, MessageHandler):
|
|
182
|
+
event = update.message
|
|
183
|
+
elif update.edited_message and isinstance(handler, EditedMessageHandler):
|
|
184
|
+
event = update.edited_message
|
|
185
|
+
elif update.callback_query and isinstance(handler, CallbackQueryHandler):
|
|
186
|
+
event = update.callback_query
|
|
187
|
+
elif update.deleted_message_ids and isinstance(handler, DeletedMessagesHandler):
|
|
188
|
+
event = update.deleted_message_ids
|
|
189
|
+
else:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
if isinstance(event, Message) or isinstance(event, CallbackQuery):
|
|
193
|
+
matched = await handler.check(self, getattr(event, "message", event) if isinstance(event, CallbackQuery) else event)
|
|
194
|
+
else:
|
|
195
|
+
matched = True
|
|
196
|
+
|
|
197
|
+
if matched:
|
|
198
|
+
await handler.callback(self, event)
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
# Handler registration — decorators mirror pyrogram.Client exactly
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
def add_handler(self, handler: Handler):
|
|
205
|
+
self._handlers.append(handler)
|
|
206
|
+
return handler
|
|
207
|
+
|
|
208
|
+
def remove_handler(self, handler: Handler):
|
|
209
|
+
self._handlers.remove(handler)
|
|
210
|
+
|
|
211
|
+
def on_message(self, filters: Filter = filters_all):
|
|
212
|
+
def decorator(func: Callable):
|
|
213
|
+
self.add_handler(MessageHandler(func, filters))
|
|
214
|
+
return func
|
|
215
|
+
return decorator
|
|
216
|
+
|
|
217
|
+
def on_edited_message(self, filters: Filter = filters_all):
|
|
218
|
+
def decorator(func: Callable):
|
|
219
|
+
self.add_handler(EditedMessageHandler(func, filters))
|
|
220
|
+
return func
|
|
221
|
+
return decorator
|
|
222
|
+
|
|
223
|
+
def on_deleted_messages(self):
|
|
224
|
+
def decorator(func: Callable):
|
|
225
|
+
self.add_handler(DeletedMessagesHandler(func))
|
|
226
|
+
return func
|
|
227
|
+
return decorator
|
|
228
|
+
|
|
229
|
+
def on_callback_query(self, filters: Filter = filters_all):
|
|
230
|
+
def decorator(func: Callable):
|
|
231
|
+
self.add_handler(CallbackQueryHandler(func, filters))
|
|
232
|
+
return func
|
|
233
|
+
return decorator
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
# API methods — names/signatures mirror pyrogram.Client's own
|
|
237
|
+
# ------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
async def get_me(self) -> User:
|
|
240
|
+
"""Fetch info about the currently logged-in account."""
|
|
241
|
+
data = await self.session.invoke("getUserInfo", {})
|
|
242
|
+
return User._parse(self, data.get("user", {}))
|
|
243
|
+
|
|
244
|
+
async def send_message(
|
|
245
|
+
self,
|
|
246
|
+
chat_id: str,
|
|
247
|
+
text: str,
|
|
248
|
+
*,
|
|
249
|
+
reply_to_message_id: str | None = None,
|
|
250
|
+
parse_mode: ParseMode = ParseMode.NONE,
|
|
251
|
+
) -> Message:
|
|
252
|
+
"""Send a text message to ``chat_id``."""
|
|
253
|
+
data = {"object_guid": chat_id, "text": text, "rnd": _rnd()}
|
|
254
|
+
if reply_to_message_id:
|
|
255
|
+
data["reply_to_message_id"] = reply_to_message_id
|
|
256
|
+
result = await self.session.invoke("sendMessage", data)
|
|
257
|
+
return Message._parse(self, result, chat_id)
|
|
258
|
+
|
|
259
|
+
async def send_photo(self, chat_id: str, photo: str, *, caption: str | None = None, **kwargs) -> Message:
|
|
260
|
+
"""Send a photo (local path or previously uploaded ``file_id``) to ``chat_id``."""
|
|
261
|
+
return await self._send_media(chat_id, photo, "Image", caption=caption, **kwargs)
|
|
262
|
+
|
|
263
|
+
async def send_video(self, chat_id: str, video: str, *, caption: str | None = None, **kwargs) -> Message:
|
|
264
|
+
return await self._send_media(chat_id, video, "Video", caption=caption, **kwargs)
|
|
265
|
+
|
|
266
|
+
async def send_voice(self, chat_id: str, voice: str, **kwargs) -> Message:
|
|
267
|
+
return await self._send_media(chat_id, voice, "Voice", **kwargs)
|
|
268
|
+
|
|
269
|
+
async def send_document(self, chat_id: str, document: str, *, caption: str | None = None, **kwargs) -> Message:
|
|
270
|
+
return await self._send_media(chat_id, document, "File", caption=caption, **kwargs)
|
|
271
|
+
|
|
272
|
+
async def _send_media(
|
|
273
|
+
self,
|
|
274
|
+
chat_id: str,
|
|
275
|
+
media: str,
|
|
276
|
+
media_type: str,
|
|
277
|
+
*,
|
|
278
|
+
caption: str | None = None,
|
|
279
|
+
reply_to_message_id: str | None = None,
|
|
280
|
+
) -> Message:
|
|
281
|
+
file_id = await self._upload(media)
|
|
282
|
+
data = {
|
|
283
|
+
"object_guid": chat_id,
|
|
284
|
+
"rnd": _rnd(),
|
|
285
|
+
"file_inline": {"file_id": file_id, "type": media_type},
|
|
286
|
+
}
|
|
287
|
+
if caption:
|
|
288
|
+
data["text"] = caption
|
|
289
|
+
if reply_to_message_id:
|
|
290
|
+
data["reply_to_message_id"] = reply_to_message_id
|
|
291
|
+
result = await self.session.invoke("sendMessage", data)
|
|
292
|
+
return Message._parse(self, result, chat_id)
|
|
293
|
+
|
|
294
|
+
async def _upload(self, path: str) -> str:
|
|
295
|
+
"""Upload a local file to Rubika's storage and return its ``file_id``."""
|
|
296
|
+
result = await self.session.invoke("requestSendFile", {"file_name": Path(path).name})
|
|
297
|
+
return result.get("file_id", "")
|
|
298
|
+
|
|
299
|
+
async def download_media(self, file: File, file_path: str | None = None) -> str:
|
|
300
|
+
"""Download ``file`` to ``file_path`` (or the file's own name) and return the local path."""
|
|
301
|
+
dest = file_path or (file.name or file.id)
|
|
302
|
+
result = await self.session.invoke(
|
|
303
|
+
"requestFile", {"file_id": file.id, "access_hash_rec": file.access_hash_rec}
|
|
304
|
+
)
|
|
305
|
+
Path(dest).write_bytes(result.get("bytes", b""))
|
|
306
|
+
return dest
|
|
307
|
+
|
|
308
|
+
async def edit_message_text(self, chat_id: str, message_id: str, text: str) -> Message:
|
|
309
|
+
result = await self.session.invoke(
|
|
310
|
+
"editMessage", {"object_guid": chat_id, "message_id": message_id, "text": text}
|
|
311
|
+
)
|
|
312
|
+
return Message._parse(self, result, chat_id)
|
|
313
|
+
|
|
314
|
+
async def delete_messages(self, chat_id: str, message_ids: list[str], *, revoke: bool = True):
|
|
315
|
+
return await self.session.invoke(
|
|
316
|
+
"deleteMessages",
|
|
317
|
+
{"object_guid": chat_id, "message_ids": message_ids, "type": "Global" if revoke else "Local"},
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
async def forward_messages(self, to_chat_id: str, from_chat_id: str, message_ids: list[str]):
|
|
321
|
+
return await self.session.invoke(
|
|
322
|
+
"forwardMessages",
|
|
323
|
+
{
|
|
324
|
+
"from_object_guid": from_chat_id,
|
|
325
|
+
"to_object_guid": to_chat_id,
|
|
326
|
+
"message_ids": message_ids,
|
|
327
|
+
"rnd": _rnd(),
|
|
328
|
+
},
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def get_chat(self, chat_id: str) -> Chat:
|
|
332
|
+
"""Fetch full info about a chat, group or channel."""
|
|
333
|
+
data = await self.session.invoke("getObjectInfo", {"object_guid": chat_id})
|
|
334
|
+
return Chat._parse(self, data)
|
|
335
|
+
|
|
336
|
+
async def get_chat_history(self, chat_id: str, *, limit: int = 50) -> list[Message]:
|
|
337
|
+
data = await self.session.invoke(
|
|
338
|
+
"getMessages", {"object_guid": chat_id, "limit": limit}
|
|
339
|
+
)
|
|
340
|
+
return [Message._parse(self, m, chat_id) for m in data.get("messages", [])]
|
|
341
|
+
|
|
342
|
+
async def ban_chat_member(self, chat_id: str, user_id: str):
|
|
343
|
+
return await self.session.invoke(
|
|
344
|
+
"banGroupMember", {"group_guid": chat_id, "member_guid": user_id, "action": "Set"}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
async def unban_chat_member(self, chat_id: str, user_id: str):
|
|
348
|
+
return await self.session.invoke(
|
|
349
|
+
"banGroupMember", {"group_guid": chat_id, "member_guid": user_id, "action": "Unset"}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
async def leave_chat(self, chat_id: str):
|
|
353
|
+
return await self.session.invoke("leaveGroup", {"group_guid": chat_id})
|
|
354
|
+
|
|
355
|
+
async def pin_chat_message(self, chat_id: str, message_id: str):
|
|
356
|
+
return await self.session.invoke(
|
|
357
|
+
"setPinMessage", {"object_guid": chat_id, "message_id": message_id, "action": "Pin"}
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
async def send_chat_action(self, chat_id: str, action: ChatActivity = ChatActivity.TYPING):
|
|
361
|
+
"""Show a typing/recording/uploading indicator, like Pyrogram's ``send_chat_action``."""
|
|
362
|
+
return await self.session.invoke(
|
|
363
|
+
"sendChatActivity", {"object_guid": chat_id, "activity": action.value}
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
async def answer_callback_query(self, callback_query_id: str, *, text: str | None = None, show_alert: bool = False):
|
|
367
|
+
return await self.session.invoke(
|
|
368
|
+
"answerCallback",
|
|
369
|
+
{"callback_id": callback_query_id, "text": text, "show_alert": show_alert},
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _rnd() -> int:
|
|
374
|
+
import random
|
|
375
|
+
return random.randint(100000, 999999999)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nicotin.enums
|
|
3
|
+
~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Enumerations mirroring the shape of Pyrogram's ``enums`` module, adapted
|
|
6
|
+
to Rubika's own vocabulary (object_guid types, chat kinds, message
|
|
7
|
+
kinds, etc).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from enum import Enum, auto
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChatType(Enum):
|
|
14
|
+
"""The kind of chat / object_guid a message belongs to."""
|
|
15
|
+
|
|
16
|
+
PRIVATE = auto() #: A direct one-to-one chat (``u_...`` guid)
|
|
17
|
+
GROUP = auto() #: A group chat (``g_...`` guid)
|
|
18
|
+
CHANNEL = auto() #: A channel (``c_...`` guid)
|
|
19
|
+
BOT = auto() #: A chat with a bot (``b_...`` guid)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MessageType(Enum):
|
|
23
|
+
"""The content type carried by a :class:`~nicotin.types.Message`."""
|
|
24
|
+
|
|
25
|
+
TEXT = auto()
|
|
26
|
+
PHOTO = auto()
|
|
27
|
+
VIDEO = auto()
|
|
28
|
+
GIF = auto()
|
|
29
|
+
VOICE = auto()
|
|
30
|
+
MUSIC = auto()
|
|
31
|
+
FILE = auto()
|
|
32
|
+
STICKER = auto()
|
|
33
|
+
LOCATION = auto()
|
|
34
|
+
CONTACT = auto()
|
|
35
|
+
POLL = auto()
|
|
36
|
+
LIVE_LOCATION = auto()
|
|
37
|
+
FORWARDED = auto()
|
|
38
|
+
DELETED = auto()
|
|
39
|
+
UNKNOWN = auto()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ChatActivity(Enum):
|
|
43
|
+
"""Typing / recording indicator, used with :meth:`Client.send_chat_activity`."""
|
|
44
|
+
|
|
45
|
+
TYPING = "Typing"
|
|
46
|
+
RECORDING = "Recording"
|
|
47
|
+
UPLOADING = "Uploading"
|
|
48
|
+
NONE = "None"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ParseMode(Enum):
|
|
52
|
+
"""How :meth:`Client.send_message` should parse ``text`` for entities."""
|
|
53
|
+
|
|
54
|
+
NONE = auto()
|
|
55
|
+
MARKDOWN = auto()
|
|
56
|
+
HTML = auto()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MessageMedia(Enum):
|
|
60
|
+
"""Restricts :func:`nicotin.filters` media matching to a specific kind."""
|
|
61
|
+
|
|
62
|
+
PHOTO = "Image"
|
|
63
|
+
VIDEO = "Video"
|
|
64
|
+
GIF = "Gif"
|
|
65
|
+
VOICE = "Voice"
|
|
66
|
+
MUSIC = "Music"
|
|
67
|
+
FILE = "File"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"ChatType",
|
|
72
|
+
"MessageType",
|
|
73
|
+
"ChatActivity",
|
|
74
|
+
"ParseMode",
|
|
75
|
+
"MessageMedia",
|
|
76
|
+
]
|