aioshad 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.
- aioshad-1.0.0/CHANGELOG.md +15 -0
- aioshad-1.0.0/LICENSE +21 -0
- aioshad-1.0.0/MANIFEST.in +7 -0
- aioshad-1.0.0/PKG-INFO +184 -0
- aioshad-1.0.0/README.md +161 -0
- aioshad-1.0.0/aioshad/__init__.py +25 -0
- aioshad-1.0.0/aioshad/client.py +270 -0
- aioshad-1.0.0/aioshad/config.py +46 -0
- aioshad-1.0.0/aioshad/crypto.py +228 -0
- aioshad-1.0.0/aioshad/dispatcher.py +145 -0
- aioshad-1.0.0/aioshad/errors.py +28 -0
- aioshad-1.0.0/aioshad/features.py +147 -0
- aioshad-1.0.0/aioshad/filters.py +287 -0
- aioshad-1.0.0/aioshad/methods.py +819 -0
- aioshad-1.0.0/aioshad/network.py +371 -0
- aioshad-1.0.0/aioshad/py.typed +0 -0
- aioshad-1.0.0/aioshad/rate_limit.py +23 -0
- aioshad-1.0.0/aioshad/session.py +136 -0
- aioshad-1.0.0/aioshad/types/__init__.py +5 -0
- aioshad-1.0.0/aioshad/types/chat.py +253 -0
- aioshad-1.0.0/aioshad/types/message.py +138 -0
- aioshad-1.0.0/aioshad/types/user.py +34 -0
- aioshad-1.0.0/aioshad.egg-info/PKG-INFO +184 -0
- aioshad-1.0.0/aioshad.egg-info/SOURCES.txt +32 -0
- aioshad-1.0.0/aioshad.egg-info/dependency_links.txt +1 -0
- aioshad-1.0.0/aioshad.egg-info/requires.txt +4 -0
- aioshad-1.0.0/aioshad.egg-info/top_level.txt +1 -0
- aioshad-1.0.0/examples/selfbot.py +22 -0
- aioshad-1.0.0/examples/time_name.py +18 -0
- aioshad-1.0.0/pyproject.toml +41 -0
- aioshad-1.0.0/requirements.txt +5 -0
- aioshad-1.0.0/setup.cfg +4 -0
- aioshad-1.0.0/setup.py +17 -0
- aioshad-1.0.0/tests/test_core.py +46 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- Async account/client core based on the provided Shad protocol implementation.
|
|
6
|
+
- HTTP/2 transport with host failover, retry/backoff and local rate limiting.
|
|
7
|
+
- Persistent sessions with optional AES-GCM encrypted storage and file permissions.
|
|
8
|
+
- Message dispatcher with duplicate-update suppression and sync/async handler support.
|
|
9
|
+
- Message, chat and user models with reply/edit/delete helpers.
|
|
10
|
+
- File/photo upload helpers and voice-chat methods from the base protocol.
|
|
11
|
+
- Profile manager for first name, last name and bio.
|
|
12
|
+
- Presence time-name scheduler using account profile updates.
|
|
13
|
+
- Auto-responder and managed background scheduler.
|
|
14
|
+
- Extended filters and generic authenticated RPC access through `Client.invoke()`.
|
|
15
|
+
- Examples and unit tests.
|
aioshad-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) aioshad contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aioshad-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aioshad
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Advanced asynchronous Python client/userbot library for a Shad account
|
|
5
|
+
Author: aioshad contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: shad,messenger,userbot,asyncio,python,account-client
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: httpx>=0.27.0
|
|
18
|
+
Requires-Dist: h2>=4.0.0
|
|
19
|
+
Requires-Dist: cryptography>=42.0.0
|
|
20
|
+
Requires-Dist: Pillow>=10.0.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
Dynamic: requires-python
|
|
23
|
+
|
|
24
|
+
# aioshad 1.0.0
|
|
25
|
+
|
|
26
|
+
کتابخانهٔ asynchronous برای ساخت **custom client / self account** روی شاد. این پروژه یک Bot API جداگانه نیست؛ session به حسابی تعلق دارد که با آن login شده است.
|
|
27
|
+
|
|
28
|
+
> نکتهٔ compatibility: بخش transport و payloadهای شاد بر پایهٔ ساختار موجود در پروژهٔ `shadism` ارائهشده و APIهای مشاهدهشدهٔ وب شاد ساخته شدهاند. چون endpointها و payloadهای رسمی شاد عمومی و ثابت نیستند، قابلیتهایی که payload دقیقشان مستند نیستند بهصورت `client.invoke()` در دسترساند و حدس زده نشدهاند.
|
|
29
|
+
|
|
30
|
+
## نصب
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## ورود اکانت
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
from aioshad import Client
|
|
41
|
+
|
|
42
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
43
|
+
|
|
44
|
+
async def main():
|
|
45
|
+
await app.start()
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
در اجرای اول کد OTP از شما درخواست میشود و session ذخیره میشود.
|
|
51
|
+
|
|
52
|
+
برای رمزنگاری session:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
export AIOSHAD_SESSION_KEY="یک-راز-32-بایتی-یا-عبارت-قوی"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## پیام و handler
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from aioshad import Client, filters
|
|
62
|
+
|
|
63
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
64
|
+
|
|
65
|
+
@app.on_message(filters.command("ping"))
|
|
66
|
+
async def ping(message):
|
|
67
|
+
await message.reply("pong")
|
|
68
|
+
|
|
69
|
+
@app.on_message(filters.private & filters.text)
|
|
70
|
+
async def private_text(message):
|
|
71
|
+
print(message.text)
|
|
72
|
+
|
|
73
|
+
asyncio.run(app.start())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
handlerهای sync و async هر دو پشتیبانی میشوند.
|
|
77
|
+
|
|
78
|
+
## امکانات حساب
|
|
79
|
+
|
|
80
|
+
### تایم کنار نام
|
|
81
|
+
|
|
82
|
+
این قابلیت واقعاً `updateProfile` را روی **همان حساب session** اجرا میکند:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from aioshad import Client, TimeName
|
|
86
|
+
|
|
87
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
88
|
+
|
|
89
|
+
async def main():
|
|
90
|
+
await app.connect()
|
|
91
|
+
await app.presence.start_time_name(
|
|
92
|
+
TimeName(
|
|
93
|
+
format="⏰ {time}",
|
|
94
|
+
timezone="Asia/Tehran",
|
|
95
|
+
interval=60,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
await app.start_in_background()
|
|
99
|
+
await app.run_until_disconnected()
|
|
100
|
+
|
|
101
|
+
asyncio.run(main())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Placeholders: `{time}`, `{time_12}`, `{date}`, `{day}`, `{timestamp}`.
|
|
105
|
+
|
|
106
|
+
### پروفایل
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
await app.profile.set_name("AioShad")
|
|
110
|
+
await app.profile.set_last_name("Client")
|
|
111
|
+
await app.profile.set_bio("Powered by aioshad")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### پاسخ خودکار حساب
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
app.autoreply.add("سلام", "سلام، در خدمتم.")
|
|
118
|
+
app.autoreply.set_default("پیامت دریافت شد.")
|
|
119
|
+
await app.autoreply.enable()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### زمانبندی داخلی
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
async def task():
|
|
126
|
+
await app.send_message("g0...", "پیام زمانبندیشده")
|
|
127
|
+
|
|
128
|
+
app.scheduler.every(60, task)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
حداقل فاصلهٔ scheduler پنج ثانیه است.
|
|
132
|
+
|
|
133
|
+
## APIهای اصلی
|
|
134
|
+
|
|
135
|
+
- `send_message`, `edit_message`, `delete_message(s)`
|
|
136
|
+
- `reply`, `reply_photo`, `reply_file`
|
|
137
|
+
- `upload_file`, `send_photo`, `send_file`
|
|
138
|
+
- `get_me`, `get_user_info`
|
|
139
|
+
- `get_chats`, `get_chat_info`, `get_chat_info_by_username`
|
|
140
|
+
- `get_messages`, `get_chat_history`
|
|
141
|
+
- `get_chats_updates`, `get_messages_updates`
|
|
142
|
+
- login/session/device management
|
|
143
|
+
- voice chat methods موجود در پروتکل پایه
|
|
144
|
+
- `block_user` / `unblock_user`
|
|
145
|
+
- فیلترهای command/regex/private/group/channel/reply/edited/author/chat/media/text
|
|
146
|
+
- `&`, `|`, `~` برای ترکیب فیلترها
|
|
147
|
+
- host failover
|
|
148
|
+
- HTTP/2
|
|
149
|
+
- retry/backoff
|
|
150
|
+
- local rate limiting
|
|
151
|
+
- duplicate-update suppression
|
|
152
|
+
- encrypted session storage
|
|
153
|
+
- graceful shutdown
|
|
154
|
+
|
|
155
|
+
## RPC خام
|
|
156
|
+
|
|
157
|
+
برای متدهای پروتکل که هنوز payload قطعی آنها مشخص نیست:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
result = await app.invoke(
|
|
161
|
+
"METHOD_NAME",
|
|
162
|
+
some_parameter="value",
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
این قسمت عمداً generic است تا بدون جعل payload جدید، آدرس هر API واقعی را بتوانید به کتابخانه اضافه کنید.
|
|
167
|
+
|
|
168
|
+
## معماری
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Client
|
|
172
|
+
├── Methods / RPC
|
|
173
|
+
├── Transport (HTTP/2 + failover + rate limit)
|
|
174
|
+
├── Session (optional AES-GCM storage)
|
|
175
|
+
├── Dispatcher / Filters
|
|
176
|
+
├── ProfileManager
|
|
177
|
+
├── PresenceManager / TimeName
|
|
178
|
+
├── AutoResponder
|
|
179
|
+
└── Scheduler
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## وضعیت تست
|
|
183
|
+
|
|
184
|
+
قبل از release این پروژه تستهای unit برای session encryption، filters، rate limiting و models دارد. تست زندهٔ شبکه بهصورت پیشفرض اجرا نمیشود، چون نیازمند یک حساب شاد و endpointهای فعال است.
|
aioshad-1.0.0/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# aioshad 1.0.0
|
|
2
|
+
|
|
3
|
+
کتابخانهٔ asynchronous برای ساخت **custom client / self account** روی شاد. این پروژه یک Bot API جداگانه نیست؛ session به حسابی تعلق دارد که با آن login شده است.
|
|
4
|
+
|
|
5
|
+
> نکتهٔ compatibility: بخش transport و payloadهای شاد بر پایهٔ ساختار موجود در پروژهٔ `shadism` ارائهشده و APIهای مشاهدهشدهٔ وب شاد ساخته شدهاند. چون endpointها و payloadهای رسمی شاد عمومی و ثابت نیستند، قابلیتهایی که payload دقیقشان مستند نیستند بهصورت `client.invoke()` در دسترساند و حدس زده نشدهاند.
|
|
6
|
+
|
|
7
|
+
## نصب
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## ورود اکانت
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
from aioshad import Client
|
|
18
|
+
|
|
19
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
await app.start()
|
|
23
|
+
|
|
24
|
+
asyncio.run(main())
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
در اجرای اول کد OTP از شما درخواست میشود و session ذخیره میشود.
|
|
28
|
+
|
|
29
|
+
برای رمزنگاری session:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export AIOSHAD_SESSION_KEY="یک-راز-32-بایتی-یا-عبارت-قوی"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## پیام و handler
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from aioshad import Client, filters
|
|
39
|
+
|
|
40
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
41
|
+
|
|
42
|
+
@app.on_message(filters.command("ping"))
|
|
43
|
+
async def ping(message):
|
|
44
|
+
await message.reply("pong")
|
|
45
|
+
|
|
46
|
+
@app.on_message(filters.private & filters.text)
|
|
47
|
+
async def private_text(message):
|
|
48
|
+
print(message.text)
|
|
49
|
+
|
|
50
|
+
asyncio.run(app.start())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
handlerهای sync و async هر دو پشتیبانی میشوند.
|
|
54
|
+
|
|
55
|
+
## امکانات حساب
|
|
56
|
+
|
|
57
|
+
### تایم کنار نام
|
|
58
|
+
|
|
59
|
+
این قابلیت واقعاً `updateProfile` را روی **همان حساب session** اجرا میکند:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from aioshad import Client, TimeName
|
|
63
|
+
|
|
64
|
+
app = Client("0937xxxxxxxx", session_directory="./sessions")
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
await app.connect()
|
|
68
|
+
await app.presence.start_time_name(
|
|
69
|
+
TimeName(
|
|
70
|
+
format="⏰ {time}",
|
|
71
|
+
timezone="Asia/Tehran",
|
|
72
|
+
interval=60,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
await app.start_in_background()
|
|
76
|
+
await app.run_until_disconnected()
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Placeholders: `{time}`, `{time_12}`, `{date}`, `{day}`, `{timestamp}`.
|
|
82
|
+
|
|
83
|
+
### پروفایل
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
await app.profile.set_name("AioShad")
|
|
87
|
+
await app.profile.set_last_name("Client")
|
|
88
|
+
await app.profile.set_bio("Powered by aioshad")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### پاسخ خودکار حساب
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
app.autoreply.add("سلام", "سلام، در خدمتم.")
|
|
95
|
+
app.autoreply.set_default("پیامت دریافت شد.")
|
|
96
|
+
await app.autoreply.enable()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### زمانبندی داخلی
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
async def task():
|
|
103
|
+
await app.send_message("g0...", "پیام زمانبندیشده")
|
|
104
|
+
|
|
105
|
+
app.scheduler.every(60, task)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
حداقل فاصلهٔ scheduler پنج ثانیه است.
|
|
109
|
+
|
|
110
|
+
## APIهای اصلی
|
|
111
|
+
|
|
112
|
+
- `send_message`, `edit_message`, `delete_message(s)`
|
|
113
|
+
- `reply`, `reply_photo`, `reply_file`
|
|
114
|
+
- `upload_file`, `send_photo`, `send_file`
|
|
115
|
+
- `get_me`, `get_user_info`
|
|
116
|
+
- `get_chats`, `get_chat_info`, `get_chat_info_by_username`
|
|
117
|
+
- `get_messages`, `get_chat_history`
|
|
118
|
+
- `get_chats_updates`, `get_messages_updates`
|
|
119
|
+
- login/session/device management
|
|
120
|
+
- voice chat methods موجود در پروتکل پایه
|
|
121
|
+
- `block_user` / `unblock_user`
|
|
122
|
+
- فیلترهای command/regex/private/group/channel/reply/edited/author/chat/media/text
|
|
123
|
+
- `&`, `|`, `~` برای ترکیب فیلترها
|
|
124
|
+
- host failover
|
|
125
|
+
- HTTP/2
|
|
126
|
+
- retry/backoff
|
|
127
|
+
- local rate limiting
|
|
128
|
+
- duplicate-update suppression
|
|
129
|
+
- encrypted session storage
|
|
130
|
+
- graceful shutdown
|
|
131
|
+
|
|
132
|
+
## RPC خام
|
|
133
|
+
|
|
134
|
+
برای متدهای پروتکل که هنوز payload قطعی آنها مشخص نیست:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
result = await app.invoke(
|
|
138
|
+
"METHOD_NAME",
|
|
139
|
+
some_parameter="value",
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
این قسمت عمداً generic است تا بدون جعل payload جدید، آدرس هر API واقعی را بتوانید به کتابخانه اضافه کنید.
|
|
144
|
+
|
|
145
|
+
## معماری
|
|
146
|
+
|
|
147
|
+
```text
|
|
148
|
+
Client
|
|
149
|
+
├── Methods / RPC
|
|
150
|
+
├── Transport (HTTP/2 + failover + rate limit)
|
|
151
|
+
├── Session (optional AES-GCM storage)
|
|
152
|
+
├── Dispatcher / Filters
|
|
153
|
+
├── ProfileManager
|
|
154
|
+
├── PresenceManager / TimeName
|
|
155
|
+
├── AutoResponder
|
|
156
|
+
└── Scheduler
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## وضعیت تست
|
|
160
|
+
|
|
161
|
+
قبل از release این پروژه تستهای unit برای session encryption، filters، rate limiting و models دارد. تست زندهٔ شبکه بهصورت پیشفرض اجرا نمیشود، چون نیازمند یک حساب شاد و endpointهای فعال است.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from aioshad.client import Client
|
|
2
|
+
from aioshad.config import ClientConfig
|
|
3
|
+
from aioshad.errors import (
|
|
4
|
+
AioShadError,
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
InvalidSessionError,
|
|
7
|
+
RPCError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
UnsupportedMethodError,
|
|
10
|
+
)
|
|
11
|
+
from aioshad.features import AutoResponder, PresenceManager, ProfileManager, Scheduler, TimeName
|
|
12
|
+
from aioshad.types.chat import Chat
|
|
13
|
+
from aioshad.types.message import Message
|
|
14
|
+
from aioshad.types.user import User
|
|
15
|
+
from aioshad import filters
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Client", "ClientConfig", "Message", "User", "Chat", "filters",
|
|
19
|
+
"TimeName", "ProfileManager", "PresenceManager", "AutoResponder", "Scheduler",
|
|
20
|
+
"AioShadError", "AuthenticationError", "InvalidSessionError", "RPCError",
|
|
21
|
+
"RateLimitError", "UnsupportedMethodError",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
__version__ = "1.0.0"
|
|
25
|
+
__author__ = "aioshad contributors"
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import signal
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from aioshad.config import ClientConfig
|
|
11
|
+
from aioshad.dispatcher import Dispatcher, MessageHandler
|
|
12
|
+
from aioshad.features import AutoResponder, PresenceManager, ProfileManager, Scheduler
|
|
13
|
+
from aioshad.filters import Filter, ensure_filter
|
|
14
|
+
from aioshad.methods import Methods
|
|
15
|
+
from aioshad.network import Transport
|
|
16
|
+
from aioshad.session import Session, SessionStorage
|
|
17
|
+
from aioshad.types.chat import Chat
|
|
18
|
+
from aioshad.types.message import Message
|
|
19
|
+
from aioshad.types.user import User
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("aioshad.client")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Client:
|
|
25
|
+
"""Async client for a Shad account (custom client/userbot).
|
|
26
|
+
|
|
27
|
+
The client operates with the authenticated account session; it is not a
|
|
28
|
+
separate bot identity. High-level account features live in ``profile``,
|
|
29
|
+
``presence`` and ``autoreply``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
phone_number: str,
|
|
35
|
+
session_directory: str = ".",
|
|
36
|
+
messenger_host: Optional[str] = None,
|
|
37
|
+
*,
|
|
38
|
+
config: Optional[ClientConfig] = None,
|
|
39
|
+
session_encryption_key: Optional[str] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self.phone_number = phone_number
|
|
42
|
+
self.config = config or ClientConfig(messenger_host=messenger_host)
|
|
43
|
+
self._storage = SessionStorage(
|
|
44
|
+
phone_number,
|
|
45
|
+
directory=session_directory,
|
|
46
|
+
encryption_key=session_encryption_key,
|
|
47
|
+
)
|
|
48
|
+
self.session: Session = self._storage.load(phone_number)
|
|
49
|
+
if messenger_host:
|
|
50
|
+
self.session.messenger_host = messenger_host
|
|
51
|
+
elif self.config.messenger_host:
|
|
52
|
+
self.session.messenger_host = self.config.messenger_host
|
|
53
|
+
|
|
54
|
+
self._pending_handlers: List[Tuple[MessageHandler, Optional[Filter]]] = []
|
|
55
|
+
self._transport: Optional[Transport] = None
|
|
56
|
+
self._methods: Optional[Methods] = None
|
|
57
|
+
self._dispatcher: Optional[Dispatcher] = None
|
|
58
|
+
self._started = False
|
|
59
|
+
|
|
60
|
+
self.profile = ProfileManager(self)
|
|
61
|
+
self.presence = PresenceManager(self)
|
|
62
|
+
self.autoreply = AutoResponder(self)
|
|
63
|
+
self.scheduler = Scheduler()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def transport(self) -> Transport:
|
|
67
|
+
if self._transport is None:
|
|
68
|
+
raise RuntimeError("Client is not connected. Call await client.connect() first.")
|
|
69
|
+
return self._transport
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def methods(self) -> Methods:
|
|
73
|
+
if self._methods is None:
|
|
74
|
+
raise RuntimeError("Client is not connected. Call await client.connect() first.")
|
|
75
|
+
return self._methods
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def is_connected(self) -> bool:
|
|
79
|
+
return self._transport is not None and not self._transport.is_closed
|
|
80
|
+
|
|
81
|
+
def _bootstrap(self) -> None:
|
|
82
|
+
self._transport = Transport(self.session, self.config)
|
|
83
|
+
self._methods = Methods(self.session, self._transport, self._storage, client=self)
|
|
84
|
+
self._dispatcher = Dispatcher(
|
|
85
|
+
self,
|
|
86
|
+
poll_interval=self.config.poll_interval,
|
|
87
|
+
max_errors=self.config.max_consecutive_errors,
|
|
88
|
+
)
|
|
89
|
+
for handler, filter_obj in self._pending_handlers:
|
|
90
|
+
self._dispatcher.register_handler(handler, filter_obj=filter_obj)
|
|
91
|
+
self._pending_handlers.clear()
|
|
92
|
+
|
|
93
|
+
async def connect(self) -> "Client":
|
|
94
|
+
if self._transport is None:
|
|
95
|
+
self._bootstrap()
|
|
96
|
+
methods = self.methods
|
|
97
|
+
if not self.session.has_auth():
|
|
98
|
+
logger.info("No active session found; starting account login flow.")
|
|
99
|
+
await methods.login_flow(self.phone_number)
|
|
100
|
+
else:
|
|
101
|
+
try:
|
|
102
|
+
await methods.register_device()
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
if "INVALID_AUTH" in str(exc):
|
|
105
|
+
logger.warning("Stored session is invalid; starting login flow.")
|
|
106
|
+
self._clear_auth()
|
|
107
|
+
await methods.login_flow(self.phone_number)
|
|
108
|
+
else:
|
|
109
|
+
logger.debug("Device registration check: %s", exc)
|
|
110
|
+
return self
|
|
111
|
+
|
|
112
|
+
def _clear_auth(self) -> None:
|
|
113
|
+
self.session.auth = ""
|
|
114
|
+
self.session.decode_auth = ""
|
|
115
|
+
self.session.private_key_pem = ""
|
|
116
|
+
self.session.key_hex = ""
|
|
117
|
+
self.session.iv_hex = ""
|
|
118
|
+
self.session.tmp_session = ""
|
|
119
|
+
self._storage.save(self.session)
|
|
120
|
+
|
|
121
|
+
async def start(self) -> None:
|
|
122
|
+
await self.connect()
|
|
123
|
+
if self._dispatcher is not None:
|
|
124
|
+
self._dispatcher.start()
|
|
125
|
+
self._started = True
|
|
126
|
+
await self._idle()
|
|
127
|
+
|
|
128
|
+
async def start_in_background(self) -> None:
|
|
129
|
+
await self.connect()
|
|
130
|
+
if self._dispatcher is not None:
|
|
131
|
+
self._dispatcher.start()
|
|
132
|
+
self._started = True
|
|
133
|
+
|
|
134
|
+
async def run_until_disconnected(self) -> None:
|
|
135
|
+
await self.start()
|
|
136
|
+
|
|
137
|
+
async def _idle(self) -> None:
|
|
138
|
+
while self._dispatcher is not None and self._dispatcher._running:
|
|
139
|
+
await asyncio.sleep(1.0)
|
|
140
|
+
|
|
141
|
+
async def stop(self) -> None:
|
|
142
|
+
await self.presence.close()
|
|
143
|
+
await self.scheduler.cancel_all()
|
|
144
|
+
if self._dispatcher:
|
|
145
|
+
await self._dispatcher.stop()
|
|
146
|
+
if self._transport:
|
|
147
|
+
await self._transport.close()
|
|
148
|
+
self._storage.save(self.session)
|
|
149
|
+
self._started = False
|
|
150
|
+
|
|
151
|
+
def on_message(
|
|
152
|
+
self,
|
|
153
|
+
filters_or_func: Optional[Union[Filter, Callable[..., Any], MessageHandler]] = None,
|
|
154
|
+
) -> Any:
|
|
155
|
+
if inspect.iscoroutinefunction(filters_or_func) or (
|
|
156
|
+
callable(filters_or_func) and not isinstance(filters_or_func, Filter)
|
|
157
|
+
and not isinstance(filters_or_func, (str, bytes))
|
|
158
|
+
and len(inspect.signature(filters_or_func).parameters) == 1
|
|
159
|
+
):
|
|
160
|
+
func = filters_or_func
|
|
161
|
+
self._register_message_handler(func, filter_obj=None)
|
|
162
|
+
return func
|
|
163
|
+
|
|
164
|
+
filter_obj: Optional[Filter] = None
|
|
165
|
+
if filters_or_func is not None:
|
|
166
|
+
filter_obj = ensure_filter(filters_or_func)
|
|
167
|
+
|
|
168
|
+
def decorator(func: MessageHandler) -> MessageHandler:
|
|
169
|
+
self._register_message_handler(func, filter_obj=filter_obj)
|
|
170
|
+
return func
|
|
171
|
+
|
|
172
|
+
return decorator
|
|
173
|
+
|
|
174
|
+
def _register_message_handler(self, func: MessageHandler, filter_obj: Optional[Filter] = None) -> None:
|
|
175
|
+
if self._dispatcher is not None:
|
|
176
|
+
self._dispatcher.register_handler(func, filter_obj=filter_obj)
|
|
177
|
+
else:
|
|
178
|
+
self._pending_handlers.append((func, filter_obj))
|
|
179
|
+
|
|
180
|
+
def remove_handler(self, handler: MessageHandler) -> int:
|
|
181
|
+
if self._dispatcher is None:
|
|
182
|
+
before = len(self._pending_handlers)
|
|
183
|
+
self._pending_handlers = [item for item in self._pending_handlers if item[0] is not handler]
|
|
184
|
+
return before - len(self._pending_handlers)
|
|
185
|
+
return self._dispatcher.unregister_handler(handler)
|
|
186
|
+
|
|
187
|
+
async def send_message(self, object_guid: str, text: str = "", reply_to_message_id: Optional[str] = None, file_inline: Optional[Dict[str, Any]] = None) -> Message:
|
|
188
|
+
return await self.methods.send_message(object_guid, text, reply_to_message_id, file_inline)
|
|
189
|
+
|
|
190
|
+
async def edit_message(self, object_guid: str, message_id: str, text: str) -> Message:
|
|
191
|
+
return await self.methods.edit_message(object_guid, message_id, text)
|
|
192
|
+
|
|
193
|
+
async def delete_messages(self, object_guid: str, message_ids: Union[str, int, List[Union[str, int]]], delete_type: str = "Global") -> Dict[str, Any]:
|
|
194
|
+
return await self.methods.delete_messages(object_guid, message_ids, delete_type)
|
|
195
|
+
|
|
196
|
+
async def delete_message(self, object_guid: str, message_id: Union[str, int], delete_type: str = "Global") -> Dict[str, Any]:
|
|
197
|
+
return await self.methods.delete_message(object_guid, message_id, delete_type)
|
|
198
|
+
|
|
199
|
+
async def upload_file(self, file: Union[str, bytes, Path], file_name: Optional[str] = None, mime: Optional[str] = None, chunk_size: Optional[int] = None) -> Dict[str, Any]:
|
|
200
|
+
return await self.methods.upload_file(file, file_name, mime, chunk_size or self.config.upload_chunk_size)
|
|
201
|
+
|
|
202
|
+
async def send_photo(self, object_guid: str, photo: Union[str, bytes, Path], caption: Optional[str] = None, reply_to_message_id: Optional[str] = None, file_name: Optional[str] = None) -> Message:
|
|
203
|
+
return await self.methods.send_photo(object_guid, photo, caption, reply_to_message_id, file_name)
|
|
204
|
+
|
|
205
|
+
async def send_file(self, object_guid: str, file: Union[str, bytes, Path], file_name: Optional[str] = None, mime: Optional[str] = None, caption: Optional[str] = None, reply_to_message_id: Optional[str] = None) -> Message:
|
|
206
|
+
return await self.methods.send_file(object_guid, file, file_name, mime, caption, reply_to_message_id)
|
|
207
|
+
|
|
208
|
+
async def get_user_info(self, user_guid: Optional[str] = None) -> User:
|
|
209
|
+
return await self.methods.get_user_info(user_guid)
|
|
210
|
+
|
|
211
|
+
async def get_me(self) -> User:
|
|
212
|
+
return await self.get_user_info()
|
|
213
|
+
|
|
214
|
+
async def update_profile(self, first_name: Optional[str] = None, last_name: Optional[str] = None, bio: Optional[str] = None) -> bool:
|
|
215
|
+
return await self.methods.update_profile(first_name, last_name, bio)
|
|
216
|
+
|
|
217
|
+
async def get_chats(self, start_id: Optional[str] = None) -> Dict[str, Any]:
|
|
218
|
+
return await self.methods.get_chats(start_id)
|
|
219
|
+
|
|
220
|
+
async def get_messages(self, object_guid: str, limit: int = 50, sort: str = "FromMax", max_id: Optional[str] = None, min_id: Optional[str] = None) -> Dict[str, Any]:
|
|
221
|
+
return await self.methods.get_messages(object_guid, limit, sort, max_id, min_id)
|
|
222
|
+
|
|
223
|
+
async def get_chats_updates(self, state: Optional[int] = None) -> Dict[str, Any]:
|
|
224
|
+
return await self.methods.get_chats_updates(state)
|
|
225
|
+
|
|
226
|
+
async def get_messages_updates(self, object_guid: str, state: Optional[int] = None) -> Dict[str, Any]:
|
|
227
|
+
return await self.methods.get_messages_updates(object_guid, state)
|
|
228
|
+
|
|
229
|
+
async def get_chat_history(self, object_guid: Optional[str] = None, limit: int = 50, max_id: Optional[str] = None, min_id: Optional[str] = None, sort: str = "FromMax", guid: Optional[str] = None, state: Optional[int] = None) -> List[Message]:
|
|
230
|
+
return await self.methods.get_chat_history(object_guid, limit, max_id, min_id, sort, guid, state)
|
|
231
|
+
|
|
232
|
+
async def register_device(self) -> Dict[str, Any]:
|
|
233
|
+
return await self.methods.register_device()
|
|
234
|
+
|
|
235
|
+
async def get_chat_info(self, object_guid: str) -> Chat:
|
|
236
|
+
return await self.methods.get_chat_info(object_guid)
|
|
237
|
+
|
|
238
|
+
async def get_chat_info_by_username(self, username: str) -> Chat:
|
|
239
|
+
return await self.methods.get_chat_info_by_username(username)
|
|
240
|
+
|
|
241
|
+
async def join_voice_chat(self, chat_guid: str, voice_chat_id: Optional[str] = None, sdp_offer_data: str = "") -> Dict[str, Any]:
|
|
242
|
+
return await self.methods.join_voice_chat(chat_guid, voice_chat_id, sdp_offer_data)
|
|
243
|
+
|
|
244
|
+
async def leave_voice_chat(self, chat_guid: str, voice_chat_id: Optional[str] = None) -> Dict[str, Any]:
|
|
245
|
+
return await self.methods.leave_voice_chat(chat_guid, voice_chat_id)
|
|
246
|
+
|
|
247
|
+
async def get_voice_chat_participants(self, chat_guid: str, voice_chat_id: Optional[str] = None) -> Dict[str, Any]:
|
|
248
|
+
return await self.methods.get_voice_chat_participants(chat_guid, voice_chat_id)
|
|
249
|
+
|
|
250
|
+
async def create_voice_chat(self, chat_guid: str) -> Dict[str, Any]:
|
|
251
|
+
return await self.methods.create_voice_chat(chat_guid)
|
|
252
|
+
|
|
253
|
+
async def discard_voice_chat(self, chat_guid: str, voice_chat_id: Optional[str] = None) -> Dict[str, Any]:
|
|
254
|
+
return await self.methods.discard_voice_chat(chat_guid, voice_chat_id)
|
|
255
|
+
|
|
256
|
+
async def set_voice_chat_state(self, chat_guid: str, voice_chat_id: str, activity: str = "Speaking", participant_object_guid: Optional[str] = None) -> Dict[str, Any]:
|
|
257
|
+
return await self.methods.set_voice_chat_state(chat_guid, voice_chat_id, activity, participant_object_guid)
|
|
258
|
+
|
|
259
|
+
async def invoke(self, method: str, **input_data: Any) -> Dict[str, Any]:
|
|
260
|
+
"""Call an authenticated Shad RPC method not yet wrapped by a helper."""
|
|
261
|
+
return await self.transport.send_authenticated(method, input_data)
|
|
262
|
+
|
|
263
|
+
async def block_user(self, user_guid: str) -> Dict[str, Any]:
|
|
264
|
+
return await self.invoke("setBlockUser", action="Block", user_guid=user_guid)
|
|
265
|
+
|
|
266
|
+
async def unblock_user(self, user_guid: str) -> Dict[str, Any]:
|
|
267
|
+
return await self.invoke("setBlockUser", action="Unblock", user_guid=user_guid)
|
|
268
|
+
|
|
269
|
+
def set_messenger_host(self, host: str) -> None:
|
|
270
|
+
self.session.messenger_host = host
|