rubiflow 1.0.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.
- rubiflow-1.0.0/PKG-INFO +136 -0
- rubiflow-1.0.0/README.md +124 -0
- rubiflow-1.0.0/pyproject.toml +20 -0
- rubiflow-1.0.0/rubiflow/__init__.py +34 -0
- rubiflow-1.0.0/rubiflow/client.py +203 -0
- rubiflow-1.0.0/rubiflow/crypto.py +169 -0
- rubiflow-1.0.0/rubiflow/exceptions.py +64 -0
- rubiflow-1.0.0/rubiflow/methods/__init__.py +52 -0
- rubiflow-1.0.0/rubiflow/methods/auth.py +158 -0
- rubiflow-1.0.0/rubiflow/methods/channels.py +167 -0
- rubiflow-1.0.0/rubiflow/methods/chats.py +81 -0
- rubiflow-1.0.0/rubiflow/methods/files.py +100 -0
- rubiflow-1.0.0/rubiflow/methods/folders.py +62 -0
- rubiflow-1.0.0/rubiflow/methods/groups.py +176 -0
- rubiflow-1.0.0/rubiflow/methods/messages.py +313 -0
- rubiflow-1.0.0/rubiflow/methods/rubino.py +255 -0
- rubiflow-1.0.0/rubiflow/methods/settings.py +62 -0
- rubiflow-1.0.0/rubiflow/methods/users.py +110 -0
- rubiflow-1.0.0/rubiflow/methods/vod.py +59 -0
- rubiflow-1.0.0/rubiflow/methods/wallet.py +51 -0
- rubiflow-1.0.0/rubiflow/models/__init__.py +9 -0
- rubiflow-1.0.0/rubiflow/models/chat.py +42 -0
- rubiflow-1.0.0/rubiflow/models/message.py +68 -0
- rubiflow-1.0.0/rubiflow/models/user.py +50 -0
- rubiflow-1.0.0/rubiflow/network.py +50 -0
- rubiflow-1.0.0/rubiflow/session.py +162 -0
- rubiflow-1.0.0/rubiflow.egg-info/PKG-INFO +136 -0
- rubiflow-1.0.0/rubiflow.egg-info/SOURCES.txt +31 -0
- rubiflow-1.0.0/rubiflow.egg-info/dependency_links.txt +1 -0
- rubiflow-1.0.0/rubiflow.egg-info/requires.txt +2 -0
- rubiflow-1.0.0/rubiflow.egg-info/top_level.txt +1 -0
- rubiflow-1.0.0/setup.cfg +4 -0
- rubiflow-1.0.0/tests/test_rubiflow.py +178 -0
rubiflow-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rubiflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: High-Performance Async Python Framework for Rubika Messenger and SuperApp
|
|
5
|
+
Author: RubiFlow Developers
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: rubika,rubiflow,rubika-bot,async-rubika,messenger,superapp
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
11
|
+
Requires-Dist: cryptography>=41.0.0
|
|
12
|
+
|
|
13
|
+
# ⚡ RubiFlow (روبیفلو)
|
|
14
|
+
|
|
15
|
+
> **فریمورک فوقسریع، ناهمگام (Async) و مدرن پایتون برای پیامرسان و سوپراپ روبیکا**
|
|
16
|
+
> *Next-Generation High-Performance Async Python Framework for Rubika Messenger & SuperApp*
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🌟 چرا RubiFlow؟
|
|
21
|
+
|
|
22
|
+
- **نام کاملاً اختصاصی و جدید:** نامی تک و ثبتنشده در PyPI که به جریان روان، غیرهمزمان و پرسرعت اشاره دارد.
|
|
23
|
+
- **Async-First و با بالاترین بازده:** با موتور شبکه `aiohttp`، مدیریت Connection Pooling و رمزنگاری با توابع C در `cryptography`.
|
|
24
|
+
- **رعایت کامل استانداردهای PEP 8 و Clean Architecture:** استفاده دقیق از Type Hints، مدلهای دادهای (`dataclasses`) و مدیریت خطا با استثناهای اختصاصی.
|
|
25
|
+
- **شناسایی خودکار سشن فایرفاکس:** توانایی اتصال آنی به حساب فعال در فایرفاکس (بدون نیاز به دریافت کد پیامکی مجدد).
|
|
26
|
+
- **پشتیبانی از StringSession:** خروجی سشن به صورت یک رشته متنی فشرده و امن با پیشوند `RubiFlow$...` جهت استقرار روی سرورها.
|
|
27
|
+
- **پوشش جامع بیش از ۳۰۰ متد:** پشتیبانی از پیامرسان کامل، روبینو (Rubino)، فیلم و سریال (VOD) و کیف پول.
|
|
28
|
+
- **توابع کمکی و شیگرایی پیشرفته:** ارسال آسان پیام، ریپلای مستقیم با `message.reply("...")`، حذف و ویرایش، دریافت چتها و پیامها.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📦 نصب
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
یا پیشنیازها:
|
|
39
|
+
```bash
|
|
40
|
+
pip install aiohttp cryptography
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🚀 شروع سریع (Quickstart)
|
|
46
|
+
|
|
47
|
+
### ۱. اتصال خودکار از طریق سشن فایرفاکس
|
|
48
|
+
```python
|
|
49
|
+
import asyncio
|
|
50
|
+
from rubiflow import Client
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
# اتصال مستقیم به سشن مرورگر فایرفاکس شما
|
|
54
|
+
app = Client.from_firefox()
|
|
55
|
+
|
|
56
|
+
async with app:
|
|
57
|
+
me = await app.get_me()
|
|
58
|
+
print(f"Logged in as: {me.full_name} (@{me.username})")
|
|
59
|
+
|
|
60
|
+
# ارسال پیام
|
|
61
|
+
msg = await app.send_message("u0...", "سلام از روبیفلو!")
|
|
62
|
+
print(f"پیام ارسال شد با آیدی: {msg.message_id}")
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### ۲. ساخت بات پاسخگوی خودکار (Event-Driven Echo Bot)
|
|
70
|
+
```python
|
|
71
|
+
from rubiflow import Client, Message
|
|
72
|
+
|
|
73
|
+
app = Client.from_firefox()
|
|
74
|
+
|
|
75
|
+
@app.on_message()
|
|
76
|
+
async def handle_message(msg: Message):
|
|
77
|
+
if msg.text == "سلام":
|
|
78
|
+
await msg.reply("درود! پیام شما توسط بات RubiFlow دریافت شد.")
|
|
79
|
+
|
|
80
|
+
# اجرای مداوم بات
|
|
81
|
+
app.run()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### ۳. استفاده از رشته سشن (StringSession) برای سرور
|
|
87
|
+
```python
|
|
88
|
+
import asyncio
|
|
89
|
+
from rubiflow import Client
|
|
90
|
+
|
|
91
|
+
async def export_session():
|
|
92
|
+
app = Client.from_firefox()
|
|
93
|
+
async with app:
|
|
94
|
+
# دریافت رشته امن برای قرار دادن روی سرور یا متغیرهای محیطی
|
|
95
|
+
string_session = app.session.to_string()
|
|
96
|
+
print("Your StringSession:", string_session)
|
|
97
|
+
|
|
98
|
+
# بازیابی مجدد سشن
|
|
99
|
+
new_app = Client(session=string_session)
|
|
100
|
+
async with new_app:
|
|
101
|
+
me = await new_app.get_me()
|
|
102
|
+
print("Connected with StringSession as:", me.username)
|
|
103
|
+
|
|
104
|
+
asyncio.run(export_session())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 📂 ساختار ماژولار پروژه
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
rubika-lib/
|
|
113
|
+
├── pyproject.toml # پیکربندی استاندارد بسته پایتون
|
|
114
|
+
├── README.md # راهنمای کامل
|
|
115
|
+
├── rubiflow/
|
|
116
|
+
│ ├── __init__.py # اکسپورت کلاسها و توابع اصلی
|
|
117
|
+
│ ├── client.py # هسته کلاینت و متدهای سطح بالا
|
|
118
|
+
│ ├── crypto.py # موتور پرسرعت رمزنگاری روبیکا V6 (AES-256 + RSA-SHA256)
|
|
119
|
+
│ ├── session.py # مدیریت سشن (Firefox, StringSession, FileSession)
|
|
120
|
+
│ ├── network.py # ارتباطات ناهمگام با Connection Pooling
|
|
121
|
+
│ ├── exceptions.py # استثناهای اختصاصی (InvalidAuthError, ApiError, ...)
|
|
122
|
+
│ └── models/
|
|
123
|
+
│ ├── user.py # کلاس داده کاربر (User)
|
|
124
|
+
│ ├── chat.py # کلاس داده گفتگو (Chat)
|
|
125
|
+
│ └── message.py # کلاس داده پیام همراه متدهای reply, delete, edit
|
|
126
|
+
├── docs/
|
|
127
|
+
│ ├── RUBIKA_API_FULL_REFERENCE.md # کاتالوگ جامع بیش از ۳۰۰ متد روبیکا
|
|
128
|
+
│ └── RUBIKA_CRYPTO_AND_PROTOCOL.md # جزئیات مهندسی معکوس و رمزنگاری
|
|
129
|
+
└── examples/
|
|
130
|
+
└── rubiflow_quickstart.py # نمونه کد تست زنده و جامع
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🛡️ لایسنس
|
|
136
|
+
توسعه داده شده تحت لایسنس MIT.
|
rubiflow-1.0.0/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# ⚡ RubiFlow (روبیفلو)
|
|
2
|
+
|
|
3
|
+
> **فریمورک فوقسریع، ناهمگام (Async) و مدرن پایتون برای پیامرسان و سوپراپ روبیکا**
|
|
4
|
+
> *Next-Generation High-Performance Async Python Framework for Rubika Messenger & SuperApp*
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 🌟 چرا RubiFlow؟
|
|
9
|
+
|
|
10
|
+
- **نام کاملاً اختصاصی و جدید:** نامی تک و ثبتنشده در PyPI که به جریان روان، غیرهمزمان و پرسرعت اشاره دارد.
|
|
11
|
+
- **Async-First و با بالاترین بازده:** با موتور شبکه `aiohttp`، مدیریت Connection Pooling و رمزنگاری با توابع C در `cryptography`.
|
|
12
|
+
- **رعایت کامل استانداردهای PEP 8 و Clean Architecture:** استفاده دقیق از Type Hints، مدلهای دادهای (`dataclasses`) و مدیریت خطا با استثناهای اختصاصی.
|
|
13
|
+
- **شناسایی خودکار سشن فایرفاکس:** توانایی اتصال آنی به حساب فعال در فایرفاکس (بدون نیاز به دریافت کد پیامکی مجدد).
|
|
14
|
+
- **پشتیبانی از StringSession:** خروجی سشن به صورت یک رشته متنی فشرده و امن با پیشوند `RubiFlow$...` جهت استقرار روی سرورها.
|
|
15
|
+
- **پوشش جامع بیش از ۳۰۰ متد:** پشتیبانی از پیامرسان کامل، روبینو (Rubino)، فیلم و سریال (VOD) و کیف پول.
|
|
16
|
+
- **توابع کمکی و شیگرایی پیشرفته:** ارسال آسان پیام، ریپلای مستقیم با `message.reply("...")`، حذف و ویرایش، دریافت چتها و پیامها.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 نصب
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install -e .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
یا پیشنیازها:
|
|
27
|
+
```bash
|
|
28
|
+
pip install aiohttp cryptography
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 شروع سریع (Quickstart)
|
|
34
|
+
|
|
35
|
+
### ۱. اتصال خودکار از طریق سشن فایرفاکس
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
from rubiflow import Client
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
# اتصال مستقیم به سشن مرورگر فایرفاکس شما
|
|
42
|
+
app = Client.from_firefox()
|
|
43
|
+
|
|
44
|
+
async with app:
|
|
45
|
+
me = await app.get_me()
|
|
46
|
+
print(f"Logged in as: {me.full_name} (@{me.username})")
|
|
47
|
+
|
|
48
|
+
# ارسال پیام
|
|
49
|
+
msg = await app.send_message("u0...", "سلام از روبیفلو!")
|
|
50
|
+
print(f"پیام ارسال شد با آیدی: {msg.message_id}")
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### ۲. ساخت بات پاسخگوی خودکار (Event-Driven Echo Bot)
|
|
58
|
+
```python
|
|
59
|
+
from rubiflow import Client, Message
|
|
60
|
+
|
|
61
|
+
app = Client.from_firefox()
|
|
62
|
+
|
|
63
|
+
@app.on_message()
|
|
64
|
+
async def handle_message(msg: Message):
|
|
65
|
+
if msg.text == "سلام":
|
|
66
|
+
await msg.reply("درود! پیام شما توسط بات RubiFlow دریافت شد.")
|
|
67
|
+
|
|
68
|
+
# اجرای مداوم بات
|
|
69
|
+
app.run()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### ۳. استفاده از رشته سشن (StringSession) برای سرور
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from rubiflow import Client
|
|
78
|
+
|
|
79
|
+
async def export_session():
|
|
80
|
+
app = Client.from_firefox()
|
|
81
|
+
async with app:
|
|
82
|
+
# دریافت رشته امن برای قرار دادن روی سرور یا متغیرهای محیطی
|
|
83
|
+
string_session = app.session.to_string()
|
|
84
|
+
print("Your StringSession:", string_session)
|
|
85
|
+
|
|
86
|
+
# بازیابی مجدد سشن
|
|
87
|
+
new_app = Client(session=string_session)
|
|
88
|
+
async with new_app:
|
|
89
|
+
me = await new_app.get_me()
|
|
90
|
+
print("Connected with StringSession as:", me.username)
|
|
91
|
+
|
|
92
|
+
asyncio.run(export_session())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 📂 ساختار ماژولار پروژه
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
rubika-lib/
|
|
101
|
+
├── pyproject.toml # پیکربندی استاندارد بسته پایتون
|
|
102
|
+
├── README.md # راهنمای کامل
|
|
103
|
+
├── rubiflow/
|
|
104
|
+
│ ├── __init__.py # اکسپورت کلاسها و توابع اصلی
|
|
105
|
+
│ ├── client.py # هسته کلاینت و متدهای سطح بالا
|
|
106
|
+
│ ├── crypto.py # موتور پرسرعت رمزنگاری روبیکا V6 (AES-256 + RSA-SHA256)
|
|
107
|
+
│ ├── session.py # مدیریت سشن (Firefox, StringSession, FileSession)
|
|
108
|
+
│ ├── network.py # ارتباطات ناهمگام با Connection Pooling
|
|
109
|
+
│ ├── exceptions.py # استثناهای اختصاصی (InvalidAuthError, ApiError, ...)
|
|
110
|
+
│ └── models/
|
|
111
|
+
│ ├── user.py # کلاس داده کاربر (User)
|
|
112
|
+
│ ├── chat.py # کلاس داده گفتگو (Chat)
|
|
113
|
+
│ └── message.py # کلاس داده پیام همراه متدهای reply, delete, edit
|
|
114
|
+
├── docs/
|
|
115
|
+
│ ├── RUBIKA_API_FULL_REFERENCE.md # کاتالوگ جامع بیش از ۳۰۰ متد روبیکا
|
|
116
|
+
│ └── RUBIKA_CRYPTO_AND_PROTOCOL.md # جزئیات مهندسی معکوس و رمزنگاری
|
|
117
|
+
└── examples/
|
|
118
|
+
└── rubiflow_quickstart.py # نمونه کد تست زنده و جامع
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🛡️ لایسنس
|
|
124
|
+
توسعه داده شده تحت لایسنس MIT.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rubiflow"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "High-Performance Async Python Framework for Rubika Messenger and SuperApp"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "RubiFlow Developers" }]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aiohttp>=3.9.0",
|
|
15
|
+
"cryptography>=41.0.0"
|
|
16
|
+
]
|
|
17
|
+
keywords = ["rubika", "rubiflow", "rubika-bot", "async-rubika", "messenger", "superapp"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
include = ["rubiflow*"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RubiFlow: Next-Generation High-Performance Async Python Framework for Rubika.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import Client
|
|
6
|
+
from .crypto import RubiFlowCrypto
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
ApiError,
|
|
9
|
+
CryptoError,
|
|
10
|
+
InvalidAuthError,
|
|
11
|
+
NetworkError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
RubiFlowError,
|
|
14
|
+
)
|
|
15
|
+
from .models import Chat, Message, User
|
|
16
|
+
from .session import Session
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__author__ = "RubiFlow Team"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Client",
|
|
23
|
+
"Session",
|
|
24
|
+
"RubiFlowCrypto",
|
|
25
|
+
"User",
|
|
26
|
+
"Chat",
|
|
27
|
+
"Message",
|
|
28
|
+
"RubiFlowError",
|
|
29
|
+
"ApiError",
|
|
30
|
+
"InvalidAuthError",
|
|
31
|
+
"NetworkError",
|
|
32
|
+
"CryptoError",
|
|
33
|
+
"NotFoundError",
|
|
34
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core asynchronous Client for RubiFlow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from .crypto import RubiFlowCrypto
|
|
11
|
+
from .exceptions import RubiFlowError, check_api_status
|
|
12
|
+
from .methods import Methods
|
|
13
|
+
from .models import Message, User
|
|
14
|
+
from .network import AsyncTransport
|
|
15
|
+
from .session import Session
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("rubiflow")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Client(Methods):
|
|
21
|
+
"""
|
|
22
|
+
High-performance, asynchronous Rubika client framework.
|
|
23
|
+
Inherits 300+ methods covering Messenger, Rubino, VOD, and Wallet.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
DEFAULT_DC_URL = "https://messengerg2c332.iranlms.ir"
|
|
27
|
+
LIB_NAME = "RubiFlow"
|
|
28
|
+
LIB_VERSION = "1.0.0"
|
|
29
|
+
|
|
30
|
+
# Rubika protocol standard client signature
|
|
31
|
+
_INTERNAL_CLIENT = {
|
|
32
|
+
"app_name": "Main",
|
|
33
|
+
"app_version": "2.5.9",
|
|
34
|
+
"platform": "PWA",
|
|
35
|
+
"package": "m.rubika.ir",
|
|
36
|
+
"lang_code": "fa",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
session: Optional[Union[Session, str]] = None,
|
|
42
|
+
auth: Optional[str] = None,
|
|
43
|
+
private_key_pem: Optional[str] = None,
|
|
44
|
+
dc_url: Optional[str] = None,
|
|
45
|
+
timeout: float = 20.0,
|
|
46
|
+
):
|
|
47
|
+
if isinstance(session, str):
|
|
48
|
+
if session.startswith("RubiFlow$") or session.startswith("Rubix$"):
|
|
49
|
+
self.session = Session.from_string(session)
|
|
50
|
+
else:
|
|
51
|
+
self.session = Session.load_from_file(session)
|
|
52
|
+
elif isinstance(session, Session):
|
|
53
|
+
self.session = session
|
|
54
|
+
elif auth and private_key_pem:
|
|
55
|
+
self.session = Session(
|
|
56
|
+
auth=auth,
|
|
57
|
+
private_key_pem=private_key_pem,
|
|
58
|
+
dc_url=dc_url or self.DEFAULT_DC_URL,
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
# Temporary unauthenticated session
|
|
62
|
+
priv, _ = RubiFlowCrypto.generate_rsa_key_pair()
|
|
63
|
+
tmp = RubiFlowCrypto.generate_tmp_session(32)
|
|
64
|
+
self.session = Session(
|
|
65
|
+
auth=tmp,
|
|
66
|
+
private_key_pem=priv,
|
|
67
|
+
dc_url=dc_url or self.DEFAULT_DC_URL,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.transport = AsyncTransport(timeout_seconds=timeout)
|
|
71
|
+
self._me: Optional[User] = None
|
|
72
|
+
self._message_handlers: List[Callable[[Message], Any]] = []
|
|
73
|
+
self._is_running = False
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_firefox(cls) -> "Client":
|
|
77
|
+
"""
|
|
78
|
+
Creates a Client connected directly to the user's active session in Firefox.
|
|
79
|
+
"""
|
|
80
|
+
session = Session.from_firefox()
|
|
81
|
+
return cls(session=session)
|
|
82
|
+
|
|
83
|
+
async def __aenter__(self) -> "Client":
|
|
84
|
+
await self.start()
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
88
|
+
await self.stop()
|
|
89
|
+
|
|
90
|
+
async def start(self) -> None:
|
|
91
|
+
"""Initializes client and verifies session authentication."""
|
|
92
|
+
try:
|
|
93
|
+
me = await self.get_me()
|
|
94
|
+
self._me = me
|
|
95
|
+
logger.info("RubiFlow connected as @%s (%s)", me.username, me.guid)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
logger.warning("RubiFlow started with unverified session: %s", exc)
|
|
98
|
+
|
|
99
|
+
async def stop(self) -> None:
|
|
100
|
+
"""Closes transport and background tasks."""
|
|
101
|
+
self._is_running = False
|
|
102
|
+
await self.transport.close()
|
|
103
|
+
|
|
104
|
+
# --- Core RPC Dispatcher ---
|
|
105
|
+
|
|
106
|
+
async def execute(
|
|
107
|
+
self,
|
|
108
|
+
method: str,
|
|
109
|
+
input_data: Optional[Dict[str, Any]] = None,
|
|
110
|
+
api_version: str = "6",
|
|
111
|
+
) -> Dict[str, Any]:
|
|
112
|
+
"""
|
|
113
|
+
Executes an encrypted and RSA-signed Rubika V6 RPC method.
|
|
114
|
+
"""
|
|
115
|
+
inner_payload = {
|
|
116
|
+
"method": method,
|
|
117
|
+
"input": input_data or {},
|
|
118
|
+
"client": self._INTERNAL_CLIENT,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# 1. AES encrypt inner payload with derived secret
|
|
122
|
+
data_enc = RubiFlowCrypto.encrypt_aes(inner_payload, self.session.auth)
|
|
123
|
+
|
|
124
|
+
# 2. RSA-SHA256 signature
|
|
125
|
+
sign = RubiFlowCrypto.sign_rsa_sha256(data_enc, self.session.private_key_pem)
|
|
126
|
+
|
|
127
|
+
# 3. Disguise auth token
|
|
128
|
+
disguised_auth = RubiFlowCrypto.disguise_auth(self.session.auth)
|
|
129
|
+
|
|
130
|
+
request_body = {
|
|
131
|
+
"api_version": api_version,
|
|
132
|
+
"auth": disguised_auth,
|
|
133
|
+
"data_enc": data_enc,
|
|
134
|
+
"sign": sign,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
response = await self.transport.post_json(self.session.dc_url, request_body)
|
|
138
|
+
|
|
139
|
+
if "data_enc" in response:
|
|
140
|
+
decrypted = RubiFlowCrypto.decrypt_aes(response["data_enc"], self.session.auth)
|
|
141
|
+
if isinstance(decrypted, str):
|
|
142
|
+
decrypted = json.loads(decrypted)
|
|
143
|
+
check_api_status(decrypted)
|
|
144
|
+
return decrypted
|
|
145
|
+
|
|
146
|
+
check_api_status(response)
|
|
147
|
+
return response
|
|
148
|
+
|
|
149
|
+
async def invoke(self, method: str, **kwargs) -> Dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Directly invokes any Rubika RPC method with arbitrary input arguments.
|
|
152
|
+
Example: await app.invoke('sendMessage', object_guid='u0...', text='Hello')
|
|
153
|
+
"""
|
|
154
|
+
return await self.execute(method, kwargs)
|
|
155
|
+
|
|
156
|
+
# --- Event Handling & Bot Loop ---
|
|
157
|
+
|
|
158
|
+
def on_message(self) -> Callable[[Callable[[Message], Any]], Callable[[Message], Any]]:
|
|
159
|
+
"""Decorator to register an asynchronous handler for incoming messages."""
|
|
160
|
+
def decorator(func: Callable[[Message], Any]) -> Callable[[Message], Any]:
|
|
161
|
+
self._message_handlers.append(func)
|
|
162
|
+
return func
|
|
163
|
+
return decorator
|
|
164
|
+
|
|
165
|
+
async def _polling_loop(self) -> None:
|
|
166
|
+
"""Background poller for message updates."""
|
|
167
|
+
while self._is_running:
|
|
168
|
+
try:
|
|
169
|
+
chats = await self.get_chats()
|
|
170
|
+
for chat in chats:
|
|
171
|
+
if chat.unread_count > 0:
|
|
172
|
+
msgs = await self.get_messages(chat.guid, limit=chat.unread_count)
|
|
173
|
+
for msg in msgs:
|
|
174
|
+
for handler in self._message_handlers:
|
|
175
|
+
if asyncio.iscoroutinefunction(handler):
|
|
176
|
+
await handler(msg)
|
|
177
|
+
else:
|
|
178
|
+
handler(msg)
|
|
179
|
+
await asyncio.sleep(2)
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
logger.error("Polling error: %s", exc)
|
|
182
|
+
await asyncio.sleep(5)
|
|
183
|
+
|
|
184
|
+
def run(self, coro: Optional[Any] = None) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Convenience runner for scripts and bot processes.
|
|
187
|
+
"""
|
|
188
|
+
loop = asyncio.get_event_loop()
|
|
189
|
+
if coro is not None:
|
|
190
|
+
loop.run_until_complete(coro)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
async def _main():
|
|
194
|
+
async with self:
|
|
195
|
+
self._is_running = True
|
|
196
|
+
await self._polling_loop()
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
loop.run_until_complete(_main())
|
|
200
|
+
except (KeyboardInterrupt, SystemExit):
|
|
201
|
+
pass
|
|
202
|
+
finally:
|
|
203
|
+
loop.run_until_complete(self.stop())
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cryptographic engine for RubiFlow (Rubika V6 Protocol).
|
|
3
|
+
Implements AES-256-CBC, custom key derivation, substitution auth disguising,
|
|
4
|
+
and RSA-SHA256 asymmetric signatures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
import json
|
|
9
|
+
import random
|
|
10
|
+
import string
|
|
11
|
+
from typing import Any, Dict, Union
|
|
12
|
+
|
|
13
|
+
from cryptography.hazmat.primitives import hashes, padding
|
|
14
|
+
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
16
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
17
|
+
from cryptography.hazmat.primitives.serialization import (
|
|
18
|
+
Encoding,
|
|
19
|
+
NoEncryption,
|
|
20
|
+
PrivateFormat,
|
|
21
|
+
PublicFormat,
|
|
22
|
+
load_pem_private_key,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from .exceptions import CryptoError
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RubiFlowCrypto:
|
|
29
|
+
"""Cryptographic operations required by the Rubika V6 protocol."""
|
|
30
|
+
|
|
31
|
+
STATIC_IV = b"\x00" * 16
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def generate_rsa_key_pair() -> tuple[str, str]:
|
|
35
|
+
"""
|
|
36
|
+
Generates a new 1024-bit RSA key pair in PEM format.
|
|
37
|
+
Returns: (private_key_pem, public_key_pem)
|
|
38
|
+
"""
|
|
39
|
+
private_key = rsa.generate_private_key(
|
|
40
|
+
public_exponent=65537,
|
|
41
|
+
key_size=1024,
|
|
42
|
+
)
|
|
43
|
+
private_pem = private_key.private_bytes(
|
|
44
|
+
encoding=Encoding.PEM,
|
|
45
|
+
format=PrivateFormat.PKCS8,
|
|
46
|
+
encryption_algorithm=NoEncryption(),
|
|
47
|
+
).decode("utf-8")
|
|
48
|
+
|
|
49
|
+
public_pem = private_key.public_key().public_bytes(
|
|
50
|
+
encoding=Encoding.PEM,
|
|
51
|
+
format=PublicFormat.SubjectPublicKeyInfo,
|
|
52
|
+
).decode("utf-8")
|
|
53
|
+
|
|
54
|
+
return private_pem, public_pem
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def generate_tmp_session(length: int = 32) -> str:
|
|
58
|
+
"""Generates a random lowercase ASCII string for unauthenticated sessions."""
|
|
59
|
+
return "".join(random.choices(string.ascii_lowercase, k=length))
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def create_secret_passphrase(auth: str) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Derives the 32-character AES secret key from an auth token or tmp_session.
|
|
65
|
+
"""
|
|
66
|
+
if len(auth) < 32:
|
|
67
|
+
raise CryptoError("Auth key or tmp_session must have at least 32 characters.")
|
|
68
|
+
|
|
69
|
+
t = auth[0:8]
|
|
70
|
+
i = auth[8:16]
|
|
71
|
+
p1 = auth[16:24]
|
|
72
|
+
p2 = auth[24:32]
|
|
73
|
+
|
|
74
|
+
chars = list(p1 + t + p2 + i)
|
|
75
|
+
for idx, char in enumerate(chars):
|
|
76
|
+
if "0" <= char <= "9":
|
|
77
|
+
chars[idx] = chr((ord(char) - ord("0") + 5) % 10 + ord("0"))
|
|
78
|
+
elif "a" <= char <= "z":
|
|
79
|
+
chars[idx] = chr((ord(char) - ord("a") + 9) % 26 + ord("a"))
|
|
80
|
+
|
|
81
|
+
return "".join(chars)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def disguise_auth(auth: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Applies the Rubika V6 character substitution cipher to the auth string.
|
|
87
|
+
"""
|
|
88
|
+
result = []
|
|
89
|
+
for char in auth:
|
|
90
|
+
if "0" <= char <= "9":
|
|
91
|
+
result.append(chr((13 + (ord(char) - ord("0"))) % 10 + ord("0")))
|
|
92
|
+
elif "A" <= char <= "Z":
|
|
93
|
+
result.append(chr((29 - (ord(char) - ord("A"))) % 26 + ord("A")))
|
|
94
|
+
elif "a" <= char <= "z":
|
|
95
|
+
result.append(chr((32 - (ord(char) - ord("a"))) % 26 + ord("a")))
|
|
96
|
+
else:
|
|
97
|
+
result.append(char)
|
|
98
|
+
return "".join(result)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def encrypt_aes(cls, data: Union[Dict[str, Any], str], auth: str) -> str:
|
|
102
|
+
"""
|
|
103
|
+
Encrypts a dictionary or string payload using AES-256-CBC and returns Base64.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
passphrase = cls.create_secret_passphrase(auth).encode("utf-8")
|
|
107
|
+
if isinstance(data, dict):
|
|
108
|
+
raw_bytes = json.dumps(
|
|
109
|
+
data, separators=(",", ":"), ensure_ascii=False
|
|
110
|
+
).encode("utf-8")
|
|
111
|
+
else:
|
|
112
|
+
raw_bytes = data.encode("utf-8")
|
|
113
|
+
|
|
114
|
+
padder = padding.PKCS7(128).padder()
|
|
115
|
+
padded = padder.update(raw_bytes) + padder.finalize()
|
|
116
|
+
|
|
117
|
+
cipher = Cipher(algorithms.AES(passphrase), modes.CBC(cls.STATIC_IV))
|
|
118
|
+
encryptor = cipher.encryptor()
|
|
119
|
+
ciphertext = encryptor.update(padded) + encryptor.finalize()
|
|
120
|
+
|
|
121
|
+
return base64.b64encode(ciphertext).decode("utf-8")
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
raise CryptoError(f"AES encryption failed: {exc}") from exc
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def decrypt_aes(cls, encrypted_b64: str, auth: str) -> Union[Dict[str, Any], str]:
|
|
127
|
+
"""
|
|
128
|
+
Decrypts a Base64 AES-256-CBC ciphertext and parses it as JSON if possible.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
passphrase = cls.create_secret_passphrase(auth).encode("utf-8")
|
|
132
|
+
raw_ciphertext = base64.b64decode(encrypted_b64)
|
|
133
|
+
|
|
134
|
+
cipher = Cipher(algorithms.AES(passphrase), modes.CBC(cls.STATIC_IV))
|
|
135
|
+
decryptor = cipher.decryptor()
|
|
136
|
+
padded_plain = decryptor.update(raw_ciphertext) + decryptor.finalize()
|
|
137
|
+
|
|
138
|
+
unpadder = padding.PKCS7(128).unpadder()
|
|
139
|
+
plain_bytes = unpadder.update(padded_plain) + unpadder.finalize()
|
|
140
|
+
plain_text = plain_bytes.decode("utf-8")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
return json.loads(plain_text)
|
|
144
|
+
except json.JSONDecodeError:
|
|
145
|
+
return plain_text
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
raise CryptoError(f"AES decryption failed: {exc}") from exc
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def sign_rsa_sha256(data_enc_b64: str, private_key_pem: str) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Signs the data_enc string using RSA-SHA256 with the client's private key.
|
|
153
|
+
Returns the Base64-encoded signature.
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
pem_bytes = (
|
|
157
|
+
private_key_pem.encode("utf-8")
|
|
158
|
+
if isinstance(private_key_pem, str)
|
|
159
|
+
else private_key_pem
|
|
160
|
+
)
|
|
161
|
+
private_key = load_pem_private_key(pem_bytes, password=None)
|
|
162
|
+
signature = private_key.sign(
|
|
163
|
+
data_enc_b64.encode("utf-8"),
|
|
164
|
+
asym_padding.PKCS1v15(),
|
|
165
|
+
hashes.SHA256(),
|
|
166
|
+
)
|
|
167
|
+
return base64.b64encode(signature).decode("utf-8")
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
raise CryptoError(f"RSA signature failed: {exc}") from exc
|