nava-link 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.
- nava_link-1.0.0/PKG-INFO +213 -0
- nava_link-1.0.0/README.md +204 -0
- nava_link-1.0.0/nava_link/__init__.py +4 -0
- nava_link-1.0.0/nava_link/client.py +368 -0
- nava_link-1.0.0/nava_link/engine.js +107 -0
- nava_link-1.0.0/nava_link/package.json +1 -0
- nava_link-1.0.0/nava_link/runtime/engine.js +64 -0
- nava_link-1.0.0/nava_link/runtime/package.json +1 -0
- nava_link-1.0.0/nava_link.egg-info/PKG-INFO +213 -0
- nava_link-1.0.0/nava_link.egg-info/SOURCES.txt +13 -0
- nava_link-1.0.0/nava_link.egg-info/dependency_links.txt +1 -0
- nava_link-1.0.0/nava_link.egg-info/top_level.txt +1 -0
- nava_link-1.0.0/pyproject.toml +22 -0
- nava_link-1.0.0/setup.cfg +4 -0
- nava_link-1.0.0/tests/test_client.py +19 -0
nava_link-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nava-link
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python-native messaging client with an embedded runtime
|
|
5
|
+
Author: nava_link contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# nava-link
|
|
11
|
+
|
|
12
|
+
مكتبة Python لإنشاء تطبيقات وبوتات واتساب بواجهة بسيطة. تدعم الاتصال بكود الاقتران، استقبال الرسائل، إرسال النصوص والوسائط، إدارة المجموعات، الحساب، الجلسة، المحادثات، جهات الاتصال والأجهزة.
|
|
13
|
+
|
|
14
|
+
> المكتبة غير رسمية وغير تابعة لشركة WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، وتجنب الرسائل المزعجة أو الإرسال العشوائي.
|
|
15
|
+
|
|
16
|
+
## التثبيت
|
|
17
|
+
|
|
18
|
+
على Termux:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pkg update -y
|
|
22
|
+
pkg install -y python nodejs-lts
|
|
23
|
+
pip install nava-link==1.0.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
الاستيراد:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from nava_link import MoXstarClient
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## الاتصال بكود الاقتران
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from nava_link import MoXstarClient
|
|
37
|
+
|
|
38
|
+
PHONE = "971501234567"
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
42
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
43
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
44
|
+
|
|
45
|
+
await client.start()
|
|
46
|
+
await asyncio.sleep(5)
|
|
47
|
+
code = await client.request_pairing_code(PHONE)
|
|
48
|
+
print("كود الاقتران:", code)
|
|
49
|
+
print("واتساب > الإعدادات > الأجهزة المرتبطة > ربط جهاز")
|
|
50
|
+
print("ثم اختر الربط باستخدام رقم الهاتف وأدخل الكود.")
|
|
51
|
+
|
|
52
|
+
await client.wait_until_ready()
|
|
53
|
+
await client.wait_closed()
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
استخدم الرقم الدولي بدون `+` أو مسافات. إذا كانت جلسة `auth` محفوظة، شغّل البرنامج وانتظر الاتصال ولا تطلب كودًا جديدًا.
|
|
59
|
+
|
|
60
|
+
## الرسائل والوسائط
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
await client.send_text("201099887766", "مرحبًا من nava-link")
|
|
64
|
+
await client.send_message("201099887766", {"text": "رسالة مخصصة"})
|
|
65
|
+
await client.send_image("201099887766", "/storage/emulated/0/photo.jpg", "صورة")
|
|
66
|
+
await client.send_video("201099887766", "/storage/emulated/0/video.mp4", "فيديو")
|
|
67
|
+
await client.send_document("201099887766", "/storage/emulated/0/file.pdf", "file.pdf", "application/pdf")
|
|
68
|
+
await client.send_audio("201099887766", "/storage/emulated/0/audio.mp3", "audio/mpeg")
|
|
69
|
+
await client.send_location("201099887766", 25.2048, 55.2708, "دبي")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## استقبال الرسائل
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
async def on_messages(event):
|
|
76
|
+
for message in event["messages"]:
|
|
77
|
+
print(message.chat_id, message.sender_id, message.text)
|
|
78
|
+
if message.text == "!ping":
|
|
79
|
+
await client.send_text(message.chat_id, "pong")
|
|
80
|
+
|
|
81
|
+
client.on("messages.upsert", on_messages)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
الأحداث: `qr`, `ready`, `messages.upsert`, `messages.update`, `groups.update`, `presence.update`, `contacts.upsert`, `contacts.update`, `close`, `error`, `log`.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.on("ready", handler)
|
|
88
|
+
client.off("ready", handler)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## الحساب والحالة
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
await client.send_presence("composing", "201099887766@s.whatsapp.net")
|
|
95
|
+
await client.check_number(["201099887766", "971501234567"])
|
|
96
|
+
await client.get_status("201099887766@s.whatsapp.net")
|
|
97
|
+
await client.get_profile_picture("201099887766@s.whatsapp.net")
|
|
98
|
+
await client.get_blocklist()
|
|
99
|
+
await client.update_profile_name("اسم تجريبي")
|
|
100
|
+
await client.update_profile_status("متاح الآن")
|
|
101
|
+
await client.update_profile_picture("201099887766@s.whatsapp.net", "/storage/emulated/0/profile.jpg")
|
|
102
|
+
await client.block("201099887766@s.whatsapp.net", "block")
|
|
103
|
+
await client.block("201099887766@s.whatsapp.net", "unblock")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## الأجهزة والمحادثات وجهات الاتصال
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
await client.get_devices()
|
|
110
|
+
await client.get_chats()
|
|
111
|
+
await client.get_contacts()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
تعيد المحادثات وجهات الاتصال التي رصدتها الجلسة أثناء التشغيل الحالي.
|
|
115
|
+
|
|
116
|
+
## الوسائط والرسائل
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
await client.download_media(message.raw, "/storage/emulated/0/downloaded_file")
|
|
120
|
+
await client.forward_message("201099887766", message.raw)
|
|
121
|
+
await client.edit_message("201099887766", {"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"}, "النص الجديد")
|
|
122
|
+
await client.delete_message({"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"})
|
|
123
|
+
await client.mark_as_read([{"remoteJid": "201099887766@s.whatsapp.net", "fromMe": False, "id": "MESSAGE_ID"}])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## المجموعات
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
await client.create_group("مجموعة تجريبية", ["201099887766", "971501234567"])
|
|
130
|
+
await client.get_group_info("120363000000000000@g.us")
|
|
131
|
+
await client.update_group_subject("120363000000000000@g.us", "اسم جديد")
|
|
132
|
+
await client.update_group_description("120363000000000000@g.us", "وصف جديد")
|
|
133
|
+
await client.update_group_participants("120363000000000000@g.us", ["201099887766"], "add")
|
|
134
|
+
await client.get_group_invite_code("120363000000000000@g.us")
|
|
135
|
+
await client.revoke_group_invite("120363000000000000@g.us")
|
|
136
|
+
await client.update_group_settings("120363000000000000@g.us", "announcement")
|
|
137
|
+
await client.accept_group_invite("INVITE_CODE")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
إجراءات الأعضاء: `add`, `remove`, `promote`, `demote`.
|
|
141
|
+
|
|
142
|
+
## إدارة الاتصال
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
await client.start()
|
|
146
|
+
await client.wait_until_ready()
|
|
147
|
+
await client.wait_closed()
|
|
148
|
+
await client.close()
|
|
149
|
+
await client.logout()
|
|
150
|
+
print(client.connected, client.alive)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## الطلبات المتقدمة
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
await client.raw_request("fetchStatus", "201099887766@s.whatsapp.net")
|
|
157
|
+
await client.relay_message("201099887766@s.whatsapp.net", {"conversation": "رسالة"}, {})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
استخدم البيانات الصحيحة وبأحجام معقولة فقط. لا ترسل بيانات مصطنعة أو ضخمة، ولا تستخدم المكتبة للإزعاج أو الإغراق.
|
|
161
|
+
|
|
162
|
+
## قائمة الدوال الكاملة
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
start, wait_until_ready, wait_closed, request_pairing_code
|
|
166
|
+
send_message, send_text, send_image, send_video, send_document, send_audio, send_location
|
|
167
|
+
raw_request, relay_message, send_presence, read_messages, mark_as_read
|
|
168
|
+
check_number, get_status, get_profile_picture, get_blocklist
|
|
169
|
+
update_profile_name, update_profile_status, update_profile_picture, block
|
|
170
|
+
get_devices, get_chats, get_contacts, download_media
|
|
171
|
+
forward_message, delete_message, edit_message
|
|
172
|
+
create_group, get_group_info, update_group_subject, update_group_description
|
|
173
|
+
update_group_participants, get_group_invite_code, revoke_group_invite
|
|
174
|
+
update_group_settings, accept_group_invite, logout, close, on, off
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## مثال بوت كامل
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import asyncio
|
|
181
|
+
from nava_link import MoXstarClient
|
|
182
|
+
|
|
183
|
+
async def main():
|
|
184
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
185
|
+
|
|
186
|
+
async def on_message(event):
|
|
187
|
+
for message in event["messages"]:
|
|
188
|
+
if message.text == "!ping":
|
|
189
|
+
await client.send_text(message.chat_id, "pong")
|
|
190
|
+
|
|
191
|
+
client.on("messages.upsert", on_message)
|
|
192
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
193
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
194
|
+
await client.start()
|
|
195
|
+
await client.wait_until_ready()
|
|
196
|
+
print("البوت يعمل الآن")
|
|
197
|
+
try:
|
|
198
|
+
await client.wait_closed()
|
|
199
|
+
finally:
|
|
200
|
+
await client.close()
|
|
201
|
+
|
|
202
|
+
asyncio.run(main())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## ملاحظات أمنية
|
|
206
|
+
|
|
207
|
+
- لا تشارك مجلد `auth` أو رمز الاقتران أو QR.
|
|
208
|
+
- لا تشغّل نسختين باستخدام مجلد الجلسة نفسه.
|
|
209
|
+
- لا تحذف `auth` إذا كنت تريد الاحتفاظ بالجلسة.
|
|
210
|
+
- استخدم أرقامًا دولية بدون `+` أو مسافات.
|
|
211
|
+
- تجنب الرسائل الجماعية أو المتكررة.
|
|
212
|
+
|
|
213
|
+
PyPI: https://pypi.org/project/nava-link/
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# nava-link
|
|
2
|
+
|
|
3
|
+
مكتبة Python لإنشاء تطبيقات وبوتات واتساب بواجهة بسيطة. تدعم الاتصال بكود الاقتران، استقبال الرسائل، إرسال النصوص والوسائط، إدارة المجموعات، الحساب، الجلسة، المحادثات، جهات الاتصال والأجهزة.
|
|
4
|
+
|
|
5
|
+
> المكتبة غير رسمية وغير تابعة لشركة WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، وتجنب الرسائل المزعجة أو الإرسال العشوائي.
|
|
6
|
+
|
|
7
|
+
## التثبيت
|
|
8
|
+
|
|
9
|
+
على Termux:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pkg update -y
|
|
13
|
+
pkg install -y python nodejs-lts
|
|
14
|
+
pip install nava-link==1.0.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
الاستيراد:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from nava_link import MoXstarClient
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## الاتصال بكود الاقتران
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import asyncio
|
|
27
|
+
from nava_link import MoXstarClient
|
|
28
|
+
|
|
29
|
+
PHONE = "971501234567"
|
|
30
|
+
|
|
31
|
+
async def main():
|
|
32
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
33
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
34
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
35
|
+
|
|
36
|
+
await client.start()
|
|
37
|
+
await asyncio.sleep(5)
|
|
38
|
+
code = await client.request_pairing_code(PHONE)
|
|
39
|
+
print("كود الاقتران:", code)
|
|
40
|
+
print("واتساب > الإعدادات > الأجهزة المرتبطة > ربط جهاز")
|
|
41
|
+
print("ثم اختر الربط باستخدام رقم الهاتف وأدخل الكود.")
|
|
42
|
+
|
|
43
|
+
await client.wait_until_ready()
|
|
44
|
+
await client.wait_closed()
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
استخدم الرقم الدولي بدون `+` أو مسافات. إذا كانت جلسة `auth` محفوظة، شغّل البرنامج وانتظر الاتصال ولا تطلب كودًا جديدًا.
|
|
50
|
+
|
|
51
|
+
## الرسائل والوسائط
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
await client.send_text("201099887766", "مرحبًا من nava-link")
|
|
55
|
+
await client.send_message("201099887766", {"text": "رسالة مخصصة"})
|
|
56
|
+
await client.send_image("201099887766", "/storage/emulated/0/photo.jpg", "صورة")
|
|
57
|
+
await client.send_video("201099887766", "/storage/emulated/0/video.mp4", "فيديو")
|
|
58
|
+
await client.send_document("201099887766", "/storage/emulated/0/file.pdf", "file.pdf", "application/pdf")
|
|
59
|
+
await client.send_audio("201099887766", "/storage/emulated/0/audio.mp3", "audio/mpeg")
|
|
60
|
+
await client.send_location("201099887766", 25.2048, 55.2708, "دبي")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## استقبال الرسائل
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
async def on_messages(event):
|
|
67
|
+
for message in event["messages"]:
|
|
68
|
+
print(message.chat_id, message.sender_id, message.text)
|
|
69
|
+
if message.text == "!ping":
|
|
70
|
+
await client.send_text(message.chat_id, "pong")
|
|
71
|
+
|
|
72
|
+
client.on("messages.upsert", on_messages)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
الأحداث: `qr`, `ready`, `messages.upsert`, `messages.update`, `groups.update`, `presence.update`, `contacts.upsert`, `contacts.update`, `close`, `error`, `log`.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
client.on("ready", handler)
|
|
79
|
+
client.off("ready", handler)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## الحساب والحالة
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
await client.send_presence("composing", "201099887766@s.whatsapp.net")
|
|
86
|
+
await client.check_number(["201099887766", "971501234567"])
|
|
87
|
+
await client.get_status("201099887766@s.whatsapp.net")
|
|
88
|
+
await client.get_profile_picture("201099887766@s.whatsapp.net")
|
|
89
|
+
await client.get_blocklist()
|
|
90
|
+
await client.update_profile_name("اسم تجريبي")
|
|
91
|
+
await client.update_profile_status("متاح الآن")
|
|
92
|
+
await client.update_profile_picture("201099887766@s.whatsapp.net", "/storage/emulated/0/profile.jpg")
|
|
93
|
+
await client.block("201099887766@s.whatsapp.net", "block")
|
|
94
|
+
await client.block("201099887766@s.whatsapp.net", "unblock")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## الأجهزة والمحادثات وجهات الاتصال
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
await client.get_devices()
|
|
101
|
+
await client.get_chats()
|
|
102
|
+
await client.get_contacts()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
تعيد المحادثات وجهات الاتصال التي رصدتها الجلسة أثناء التشغيل الحالي.
|
|
106
|
+
|
|
107
|
+
## الوسائط والرسائل
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
await client.download_media(message.raw, "/storage/emulated/0/downloaded_file")
|
|
111
|
+
await client.forward_message("201099887766", message.raw)
|
|
112
|
+
await client.edit_message("201099887766", {"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"}, "النص الجديد")
|
|
113
|
+
await client.delete_message({"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"})
|
|
114
|
+
await client.mark_as_read([{"remoteJid": "201099887766@s.whatsapp.net", "fromMe": False, "id": "MESSAGE_ID"}])
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## المجموعات
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
await client.create_group("مجموعة تجريبية", ["201099887766", "971501234567"])
|
|
121
|
+
await client.get_group_info("120363000000000000@g.us")
|
|
122
|
+
await client.update_group_subject("120363000000000000@g.us", "اسم جديد")
|
|
123
|
+
await client.update_group_description("120363000000000000@g.us", "وصف جديد")
|
|
124
|
+
await client.update_group_participants("120363000000000000@g.us", ["201099887766"], "add")
|
|
125
|
+
await client.get_group_invite_code("120363000000000000@g.us")
|
|
126
|
+
await client.revoke_group_invite("120363000000000000@g.us")
|
|
127
|
+
await client.update_group_settings("120363000000000000@g.us", "announcement")
|
|
128
|
+
await client.accept_group_invite("INVITE_CODE")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
إجراءات الأعضاء: `add`, `remove`, `promote`, `demote`.
|
|
132
|
+
|
|
133
|
+
## إدارة الاتصال
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
await client.start()
|
|
137
|
+
await client.wait_until_ready()
|
|
138
|
+
await client.wait_closed()
|
|
139
|
+
await client.close()
|
|
140
|
+
await client.logout()
|
|
141
|
+
print(client.connected, client.alive)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## الطلبات المتقدمة
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
await client.raw_request("fetchStatus", "201099887766@s.whatsapp.net")
|
|
148
|
+
await client.relay_message("201099887766@s.whatsapp.net", {"conversation": "رسالة"}, {})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
استخدم البيانات الصحيحة وبأحجام معقولة فقط. لا ترسل بيانات مصطنعة أو ضخمة، ولا تستخدم المكتبة للإزعاج أو الإغراق.
|
|
152
|
+
|
|
153
|
+
## قائمة الدوال الكاملة
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
start, wait_until_ready, wait_closed, request_pairing_code
|
|
157
|
+
send_message, send_text, send_image, send_video, send_document, send_audio, send_location
|
|
158
|
+
raw_request, relay_message, send_presence, read_messages, mark_as_read
|
|
159
|
+
check_number, get_status, get_profile_picture, get_blocklist
|
|
160
|
+
update_profile_name, update_profile_status, update_profile_picture, block
|
|
161
|
+
get_devices, get_chats, get_contacts, download_media
|
|
162
|
+
forward_message, delete_message, edit_message
|
|
163
|
+
create_group, get_group_info, update_group_subject, update_group_description
|
|
164
|
+
update_group_participants, get_group_invite_code, revoke_group_invite
|
|
165
|
+
update_group_settings, accept_group_invite, logout, close, on, off
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## مثال بوت كامل
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import asyncio
|
|
172
|
+
from nava_link import MoXstarClient
|
|
173
|
+
|
|
174
|
+
async def main():
|
|
175
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
176
|
+
|
|
177
|
+
async def on_message(event):
|
|
178
|
+
for message in event["messages"]:
|
|
179
|
+
if message.text == "!ping":
|
|
180
|
+
await client.send_text(message.chat_id, "pong")
|
|
181
|
+
|
|
182
|
+
client.on("messages.upsert", on_message)
|
|
183
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
184
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
185
|
+
await client.start()
|
|
186
|
+
await client.wait_until_ready()
|
|
187
|
+
print("البوت يعمل الآن")
|
|
188
|
+
try:
|
|
189
|
+
await client.wait_closed()
|
|
190
|
+
finally:
|
|
191
|
+
await client.close()
|
|
192
|
+
|
|
193
|
+
asyncio.run(main())
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## ملاحظات أمنية
|
|
197
|
+
|
|
198
|
+
- لا تشارك مجلد `auth` أو رمز الاقتران أو QR.
|
|
199
|
+
- لا تشغّل نسختين باستخدام مجلد الجلسة نفسه.
|
|
200
|
+
- لا تحذف `auth` إذا كنت تريد الاحتفاظ بالجلسة.
|
|
201
|
+
- استخدم أرقامًا دولية بدون `+` أو مسافات.
|
|
202
|
+
- تجنب الرسائل الجماعية أو المتكررة.
|
|
203
|
+
|
|
204
|
+
PyPI: https://pypi.org/project/nava-link/
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import platform
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Awaitable, Callable
|
|
15
|
+
|
|
16
|
+
Handler = Callable[[Any], Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BridgeError(RuntimeError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BridgeOffline(BridgeError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BridgeTimeout(BridgeError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConnectionState(str, Enum):
|
|
32
|
+
DISCONNECTED = "disconnected"
|
|
33
|
+
STARTING = "starting"
|
|
34
|
+
CONNECTED = "connected"
|
|
35
|
+
CLOSED = "closed"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class Message:
|
|
40
|
+
id: str
|
|
41
|
+
chat_id: str
|
|
42
|
+
sender_id: str | None = None
|
|
43
|
+
from_me: bool = False
|
|
44
|
+
is_group: bool = False
|
|
45
|
+
text: str | None = None
|
|
46
|
+
type: str = "unknown"
|
|
47
|
+
timestamp: int | None = None
|
|
48
|
+
push_name: str | None = None
|
|
49
|
+
raw: dict[str, Any] | None = None
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def from_dict(cls, value: dict[str, Any]) -> "Message":
|
|
53
|
+
return cls(id=value.get("id", ""), chat_id=value.get("chat_id", ""), sender_id=value.get("sender_id"), from_me=bool(value.get("from_me")), is_group=bool(value.get("is_group")), text=value.get("text"), type=value.get("type", "unknown"), timestamp=value.get("timestamp"), push_name=value.get("push_name"), raw=value.get("raw"))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EventBus:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._handlers: dict[str, list[Handler]] = {}
|
|
59
|
+
|
|
60
|
+
def on(self, event: str, handler: Handler) -> Handler:
|
|
61
|
+
self._handlers.setdefault(event, []).append(handler)
|
|
62
|
+
return handler
|
|
63
|
+
|
|
64
|
+
def off(self, event: str, handler: Handler) -> None:
|
|
65
|
+
if event in self._handlers and handler in self._handlers[event]:
|
|
66
|
+
self._handlers[event].remove(handler)
|
|
67
|
+
|
|
68
|
+
async def emit(self, event: str, value: Any) -> None:
|
|
69
|
+
for handler in tuple(self._handlers.get(event, ())):
|
|
70
|
+
result = handler(value)
|
|
71
|
+
if inspect_awaitable(result):
|
|
72
|
+
await result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def inspect_awaitable(value: Any) -> bool:
|
|
76
|
+
return hasattr(value, "__await__")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class MoXstarClient:
|
|
80
|
+
"""Python-native nava_link client backed by an embedded messaging process."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, auth_dir: str = "./auth", runtime_dir: str | None = None, auto_install: bool = True, node_path: str | None = None) -> None:
|
|
83
|
+
self.auth_dir = Path(auth_dir).expanduser().resolve()
|
|
84
|
+
self.runtime_dir = Path(runtime_dir or (Path.home() / ".nava_link_runtime")).expanduser().resolve()
|
|
85
|
+
self.auto_install = auto_install
|
|
86
|
+
self.node_path = node_path
|
|
87
|
+
self.process: asyncio.subprocess.Process | None = None
|
|
88
|
+
self.events = EventBus()
|
|
89
|
+
self._pending: dict[str, asyncio.Future[dict[str, Any]]] = {}
|
|
90
|
+
self._counter = 0
|
|
91
|
+
self._reader_task: asyncio.Task[None] | None = None
|
|
92
|
+
self._stderr_task: asyncio.Task[None] | None = None
|
|
93
|
+
self._waiter: asyncio.Future[None] | None = None
|
|
94
|
+
self._ready = False
|
|
95
|
+
self.auth_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def _is_termux() -> bool:
|
|
99
|
+
# Termux reports Linux but exposes these environment markers.
|
|
100
|
+
return bool(
|
|
101
|
+
os.environ.get("TERMUX_VERSION")
|
|
102
|
+
or os.environ.get("PREFIX", "").startswith("/data/data/com.termux")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def on(self, event: str, handler: Handler) -> Handler:
|
|
106
|
+
return self.events.on(event, handler)
|
|
107
|
+
|
|
108
|
+
def off(self, event: str, handler: Handler) -> None:
|
|
109
|
+
self.events.off(event, handler)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def connected(self) -> bool:
|
|
113
|
+
return self._ready and self.process is not None and self.process.returncode is None
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def alive(self) -> bool:
|
|
117
|
+
return self.process is not None and self.process.returncode is None
|
|
118
|
+
|
|
119
|
+
def _ensure_runtime(self) -> Path:
|
|
120
|
+
source_dir = Path(__file__).resolve().parent
|
|
121
|
+
bundled_dir = source_dir / "runtime"
|
|
122
|
+
bundled_node = bundled_dir / ("node.exe" if sys.platform == "win32" else "node")
|
|
123
|
+
bundled_engine = bundled_dir / "engine.js"
|
|
124
|
+
bundled_baileys = bundled_dir / "node_modules" / "@whiskeysockets" / "baileys" / "package.json"
|
|
125
|
+
# Android/Termux cannot execute the desktop Node binary shipped in
|
|
126
|
+
# desktop wheels. Always use Termux's nodejs-lts there.
|
|
127
|
+
if not self._is_termux() and not self.node_path and bundled_node.exists() and bundled_engine.exists() and bundled_baileys.exists():
|
|
128
|
+
return bundled_engine
|
|
129
|
+
node = self.node_path or os.environ.get("NAVAPRO_NODE") or shutil.which("node")
|
|
130
|
+
npm = os.environ.get("NAVAPRO_NPM") or shutil.which("npm")
|
|
131
|
+
if not node:
|
|
132
|
+
raise BridgeError("No Node.js runtime found. In Termux run: pkg update && pkg install nodejs-lts")
|
|
133
|
+
if not npm:
|
|
134
|
+
raise BridgeError("No npm found. In Termux install nodejs-lts (it provides npm).")
|
|
135
|
+
self.runtime_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
package = self.runtime_dir / "package.json"
|
|
137
|
+
engine = self.runtime_dir / "engine.js"
|
|
138
|
+
package.write_text((source_dir / "package.json").read_text(encoding="utf-8"), encoding="utf-8")
|
|
139
|
+
engine.write_text((source_dir / "engine.js").read_text(encoding="utf-8"), encoding="utf-8")
|
|
140
|
+
installed = self.runtime_dir / "node_modules" / "@whiskeysockets" / "baileys" / "package.json"
|
|
141
|
+
if not installed.exists():
|
|
142
|
+
if not self.auto_install:
|
|
143
|
+
raise BridgeError("Baileys is not installed. Use auto_install=True or run npm install in the runtime directory.")
|
|
144
|
+
subprocess.run([npm, "install", "--no-audit", "--no-fund", "--save-exact"], cwd=self.runtime_dir, check=True)
|
|
145
|
+
return engine
|
|
146
|
+
|
|
147
|
+
def _node_executable(self) -> str | None:
|
|
148
|
+
"""Return the bundled node binary when present, otherwise a system Node."""
|
|
149
|
+
bundled = Path(__file__).resolve().parent / "runtime" / ("node.exe" if sys.platform == "win32" else "node")
|
|
150
|
+
if not self._is_termux() and bundled.exists():
|
|
151
|
+
return str(bundled)
|
|
152
|
+
return self.node_path or os.environ.get("NAVAPRO_NODE") or shutil.which("node")
|
|
153
|
+
|
|
154
|
+
async def start(self) -> None:
|
|
155
|
+
if self.alive:
|
|
156
|
+
return
|
|
157
|
+
engine = self._ensure_runtime()
|
|
158
|
+
node = self._node_executable()
|
|
159
|
+
if not node:
|
|
160
|
+
raise BridgeError("nava_link could not locate its bundled or system Node runtime.")
|
|
161
|
+
env = os.environ.copy()
|
|
162
|
+
env["nava_link_AUTH_DIR"] = str(self.auth_dir)
|
|
163
|
+
self.process = await asyncio.create_subprocess_exec(node, str(engine), cwd=str(engine.parent), env=env, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
164
|
+
self._waiter = asyncio.get_running_loop().create_future()
|
|
165
|
+
self._reader_task = asyncio.create_task(self._read_stdout())
|
|
166
|
+
self._stderr_task = asyncio.create_task(self._read_stderr())
|
|
167
|
+
await self._request("start")
|
|
168
|
+
|
|
169
|
+
async def _read_stdout(self) -> None:
|
|
170
|
+
assert self.process and self.process.stdout
|
|
171
|
+
async for raw in self.process.stdout:
|
|
172
|
+
try:
|
|
173
|
+
packet = json.loads(raw.decode("utf-8"))
|
|
174
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
175
|
+
continue
|
|
176
|
+
request_id = packet.get("request_id")
|
|
177
|
+
if request_id and request_id in self._pending:
|
|
178
|
+
future = self._pending.pop(request_id)
|
|
179
|
+
if not future.done():
|
|
180
|
+
future.set_result(packet)
|
|
181
|
+
continue
|
|
182
|
+
event = packet.get("event")
|
|
183
|
+
data = packet.get("data")
|
|
184
|
+
if event == "ready":
|
|
185
|
+
self._ready = True
|
|
186
|
+
if self._waiter and not self._waiter.done(): self._waiter.set_result(None)
|
|
187
|
+
elif event == "close":
|
|
188
|
+
self._ready = False
|
|
189
|
+
elif event == "error":
|
|
190
|
+
await self.events.emit("error", BridgeError(data.get("message", "Engine error")))
|
|
191
|
+
if event:
|
|
192
|
+
if event == "messages.upsert" and isinstance(data, dict):
|
|
193
|
+
data = {**data, "messages": [Message.from_dict(x) for x in data.get("messages", [])]}
|
|
194
|
+
await self.events.emit(event, data)
|
|
195
|
+
|
|
196
|
+
async def _read_stderr(self) -> None:
|
|
197
|
+
if not self.process or not self.process.stderr:
|
|
198
|
+
return
|
|
199
|
+
async for raw in self.process.stderr:
|
|
200
|
+
await self.events.emit("log", raw.decode("utf-8", errors="replace").rstrip())
|
|
201
|
+
|
|
202
|
+
async def _request(self, action: str, timeout: float = 30.0, **payload: Any) -> dict[str, Any]:
|
|
203
|
+
if not self.process or not self.process.stdin or self.process.returncode is not None:
|
|
204
|
+
raise BridgeOffline("nava_link engine is not running")
|
|
205
|
+
self._counter += 1
|
|
206
|
+
request_id = str(self._counter)
|
|
207
|
+
future: asyncio.Future[dict[str, Any]] = asyncio.get_running_loop().create_future()
|
|
208
|
+
self._pending[request_id] = future
|
|
209
|
+
command = {"action": action, "request_id": request_id, **payload}
|
|
210
|
+
try:
|
|
211
|
+
self.process.stdin.write((json.dumps(command, ensure_ascii=False) + "\n").encode())
|
|
212
|
+
await self.process.stdin.drain()
|
|
213
|
+
packet = await asyncio.wait_for(future, timeout)
|
|
214
|
+
except asyncio.TimeoutError as exc:
|
|
215
|
+
self._pending.pop(request_id, None)
|
|
216
|
+
raise BridgeTimeout(f"Timed out waiting for {action}") from exc
|
|
217
|
+
if not packet.get("ok"):
|
|
218
|
+
raise BridgeError(packet.get("error", {}).get("message", "Engine request failed"))
|
|
219
|
+
return packet.get("result", {})
|
|
220
|
+
|
|
221
|
+
async def wait_until_ready(self, timeout: float | None = None) -> None:
|
|
222
|
+
if self.connected:
|
|
223
|
+
return
|
|
224
|
+
if self._waiter is None:
|
|
225
|
+
raise BridgeOffline("Call start() first")
|
|
226
|
+
await asyncio.wait_for(asyncio.shield(self._waiter), timeout) if timeout else await self._waiter
|
|
227
|
+
|
|
228
|
+
async def wait_closed(self) -> None:
|
|
229
|
+
if self.process:
|
|
230
|
+
await self.process.wait()
|
|
231
|
+
|
|
232
|
+
async def request_pairing_code(self, phone: str) -> str:
|
|
233
|
+
return str((await self._request("pairing_code", phone=phone))["code"])
|
|
234
|
+
|
|
235
|
+
async def send_message(self, to: str, content: dict[str, Any]) -> dict[str, Any]:
|
|
236
|
+
return await self._request("send_message", to=to, content=content)
|
|
237
|
+
|
|
238
|
+
async def send_text(self, to: str, text: str) -> dict[str, Any]:
|
|
239
|
+
if not isinstance(text, str) or not 1 <= len(text) <= 4096:
|
|
240
|
+
raise ValueError("text must be between 1 and 4096 characters")
|
|
241
|
+
return await self.send_message(to, {"text": text})
|
|
242
|
+
|
|
243
|
+
async def send_image(self, to: str, image: str | bytes, caption: str | None = None) -> dict[str, Any]:
|
|
244
|
+
return await self.send_message(to, {"image": image.decode() if isinstance(image, bytes) else image, "caption": caption})
|
|
245
|
+
|
|
246
|
+
async def send_video(self, to: str, video: str | bytes, caption: str | None = None) -> dict[str, Any]:
|
|
247
|
+
return await self.send_message(to, {"video": video.decode() if isinstance(video, bytes) else video, "caption": caption})
|
|
248
|
+
|
|
249
|
+
async def send_document(self, to: str, document: str | bytes, file_name: str, mimetype: str = "application/octet-stream") -> dict[str, Any]:
|
|
250
|
+
return await self.send_message(to, {"document": document.decode() if isinstance(document, bytes) else document, "fileName": file_name, "mimetype": mimetype})
|
|
251
|
+
|
|
252
|
+
async def send_audio(self, to: str, audio: str | bytes, mimetype: str = "audio/mp4") -> dict[str, Any]:
|
|
253
|
+
return await self.send_message(to, {"audio": audio.decode() if isinstance(audio, bytes) else audio, "mimetype": mimetype})
|
|
254
|
+
|
|
255
|
+
async def send_location(self, to: str, latitude: float, longitude: float, name: str | None = None) -> dict[str, Any]:
|
|
256
|
+
return await self.send_message(to, {"location": {"degreesLatitude": latitude, "degreesLongitude": longitude, "name": name}})
|
|
257
|
+
|
|
258
|
+
async def raw_request(self, method: str, *args: Any) -> Any:
|
|
259
|
+
"""Call a supported Baileys socket method for advanced use cases."""
|
|
260
|
+
return (await self._request("baileys_call", method=method, args=list(args))).get("value")
|
|
261
|
+
|
|
262
|
+
async def relay_message(self, jid: str, message: dict[str, Any], options: dict[str, Any] | None = None) -> Any:
|
|
263
|
+
return await self.raw_request("relayMessage", jid, message, options or {})
|
|
264
|
+
|
|
265
|
+
async def send_presence(self, presence: str, to: str | None = None) -> Any:
|
|
266
|
+
return await self.raw_request("sendPresenceUpdate", presence, to)
|
|
267
|
+
|
|
268
|
+
async def read_messages(self, keys: list[dict[str, Any]]) -> Any:
|
|
269
|
+
return await self.raw_request("readMessages", keys)
|
|
270
|
+
|
|
271
|
+
async def check_number(self, numbers: list[str]) -> Any:
|
|
272
|
+
return await self.raw_request("onWhatsApp", numbers)
|
|
273
|
+
|
|
274
|
+
async def get_status(self, jid: str) -> Any:
|
|
275
|
+
return await self.raw_request("fetchStatus", jid)
|
|
276
|
+
|
|
277
|
+
async def get_profile_picture(self, jid: str, picture_type: str = "image") -> Any:
|
|
278
|
+
return await self.raw_request("profilePictureUrl", jid, picture_type)
|
|
279
|
+
|
|
280
|
+
async def get_blocklist(self) -> Any:
|
|
281
|
+
return await self.raw_request("fetchBlocklist")
|
|
282
|
+
|
|
283
|
+
async def update_profile_name(self, name: str) -> Any:
|
|
284
|
+
return await self.raw_request("updateProfileName", name)
|
|
285
|
+
|
|
286
|
+
async def update_profile_status(self, status: str) -> Any:
|
|
287
|
+
return await self.raw_request("updateProfileStatus", status)
|
|
288
|
+
|
|
289
|
+
async def update_profile_picture(self, jid: str, image: str | bytes) -> Any:
|
|
290
|
+
return await self.raw_request("updateProfilePicture", jid, image.decode() if isinstance(image, bytes) else image)
|
|
291
|
+
|
|
292
|
+
async def block(self, jid: str, action: str = "block") -> Any:
|
|
293
|
+
return await self.raw_request("updateBlockStatus", jid, action)
|
|
294
|
+
|
|
295
|
+
async def create_group(self, subject: str, participants: list[str]) -> Any:
|
|
296
|
+
return await self.raw_request("groupCreate", subject, participants)
|
|
297
|
+
|
|
298
|
+
async def get_group_info(self, jid: str) -> Any:
|
|
299
|
+
return await self.raw_request("groupMetadata", jid)
|
|
300
|
+
|
|
301
|
+
async def update_group_subject(self, jid: str, subject: str) -> Any:
|
|
302
|
+
return await self.raw_request("groupUpdateSubject", jid, subject)
|
|
303
|
+
|
|
304
|
+
async def update_group_description(self, jid: str, description: str) -> Any:
|
|
305
|
+
return await self.raw_request("groupUpdateDescription", jid, description)
|
|
306
|
+
|
|
307
|
+
async def update_group_participants(self, jid: str, participants: list[str], action: str) -> Any:
|
|
308
|
+
return await self.raw_request("groupParticipantsUpdate", jid, participants, action)
|
|
309
|
+
|
|
310
|
+
async def get_group_invite_code(self, jid: str) -> Any:
|
|
311
|
+
return await self.raw_request("groupInviteCode", jid)
|
|
312
|
+
|
|
313
|
+
async def revoke_group_invite(self, jid: str) -> Any:
|
|
314
|
+
return await self.raw_request("groupRevokeInvite", jid)
|
|
315
|
+
|
|
316
|
+
async def update_group_settings(self, jid: str, setting: str) -> Any:
|
|
317
|
+
return await self.raw_request("groupSettingUpdate", jid, setting)
|
|
318
|
+
|
|
319
|
+
async def accept_group_invite(self, code: str) -> Any:
|
|
320
|
+
return await self.raw_request("groupAcceptInvite", code)
|
|
321
|
+
|
|
322
|
+
async def get_devices(self, jids: list[str] | None = None) -> Any:
|
|
323
|
+
"""Return linked-device records for the supplied JIDs or the account."""
|
|
324
|
+
return await self.raw_request("getUSyncDevices", jids or [], False, False)
|
|
325
|
+
|
|
326
|
+
async def get_chats(self) -> list[dict[str, Any]]:
|
|
327
|
+
"""Return chats observed by the running client session."""
|
|
328
|
+
return list((await self._request("get_chats")).get("chats", []))
|
|
329
|
+
|
|
330
|
+
async def get_contacts(self) -> list[dict[str, Any]]:
|
|
331
|
+
"""Return contacts observed by the running client session."""
|
|
332
|
+
return list((await self._request("get_contacts")).get("contacts", []))
|
|
333
|
+
|
|
334
|
+
async def download_media(self, message: dict[str, Any], output_path: str | None = None) -> bytes | str:
|
|
335
|
+
"""Download media from a raw message; optionally save it to output_path."""
|
|
336
|
+
import base64
|
|
337
|
+
result = await self._request("download_media", message=message)
|
|
338
|
+
data = base64.b64decode(result["data"])
|
|
339
|
+
if output_path is not None:
|
|
340
|
+
from pathlib import Path
|
|
341
|
+
Path(output_path).write_bytes(data)
|
|
342
|
+
return output_path
|
|
343
|
+
return data
|
|
344
|
+
|
|
345
|
+
async def forward_message(self, to: str, message: dict[str, Any]) -> dict[str, Any]:
|
|
346
|
+
return await self._request("forward_message", to=to, message=message)
|
|
347
|
+
|
|
348
|
+
async def delete_message(self, key: dict[str, Any], jid: str | None = None) -> Any:
|
|
349
|
+
return await self.raw_request("chatModify", {"delete": key}, {"jid": jid} if jid else {})
|
|
350
|
+
|
|
351
|
+
async def edit_message(self, jid: str, key: dict[str, Any], text: str) -> dict[str, Any]:
|
|
352
|
+
return await self._request("edit_message", to=jid, key=key, text=text)
|
|
353
|
+
|
|
354
|
+
async def mark_as_read(self, keys: list[dict[str, Any]]) -> Any:
|
|
355
|
+
return await self.read_messages(keys)
|
|
356
|
+
|
|
357
|
+
async def logout(self) -> None:
|
|
358
|
+
await self._request("logout")
|
|
359
|
+
self._ready = False
|
|
360
|
+
|
|
361
|
+
async def close(self) -> None:
|
|
362
|
+
if self.alive:
|
|
363
|
+
try: await self._request("exit", timeout=5)
|
|
364
|
+
except BridgeError: pass
|
|
365
|
+
if self.process and self.process.returncode is None:
|
|
366
|
+
try: await asyncio.wait_for(self.process.wait(), 5)
|
|
367
|
+
except asyncio.TimeoutError: self.process.kill()
|
|
368
|
+
self._ready = False
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const makeWASocket = require('@whiskeysockets/baileys').default;
|
|
3
|
+
const { useMultiFileAuthState, DisconnectReason, Browsers, downloadMediaMessage } = require('@whiskeysockets/baileys');
|
|
4
|
+
|
|
5
|
+
const authDir = process.env.nava_link_AUTH_DIR || './auth';
|
|
6
|
+
let sock = null;
|
|
7
|
+
let ready = false;
|
|
8
|
+
let stopping = false;
|
|
9
|
+
const chats = new Map();
|
|
10
|
+
const contacts = new Map();
|
|
11
|
+
|
|
12
|
+
function out(value) { process.stdout.write(JSON.stringify(value) + '\n'); }
|
|
13
|
+
function errorMessage(e) { return String(e?.message || e); }
|
|
14
|
+
function textOf(m) {
|
|
15
|
+
const c = m?.message || {};
|
|
16
|
+
return c.conversation || c.extendedTextMessage?.text || c.imageMessage?.caption || c.videoMessage?.caption || c.documentMessage?.caption;
|
|
17
|
+
}
|
|
18
|
+
function normalize(m) {
|
|
19
|
+
const jid = m?.key?.remoteJid || '';
|
|
20
|
+
return { id: m?.key?.id || '', chat_id: jid, sender_id: m?.key?.participant || (m?.key?.fromMe ? null : jid), from_me: !!m?.key?.fromMe, is_group: jid.endsWith('@g.us'), text: textOf(m), type: Object.keys(m?.message || {})[0] || 'unknown', timestamp: Number(m?.messageTimestamp || 0) || null, push_name: m?.pushName || null, raw: m };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function start() {
|
|
24
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
25
|
+
sock = makeWASocket({ auth: state, printQRInTerminal: false, browser: Browsers.ubuntu('Chrome'), markOnlineOnConnect: false, syncFullHistory: false });
|
|
26
|
+
sock.ev.on('creds.update', saveCreds);
|
|
27
|
+
sock.ev.on('connection.update', ({ connection, lastDisconnect, qr }) => {
|
|
28
|
+
if (qr) out({ event: 'qr', data: qr });
|
|
29
|
+
if (connection === 'open') { ready = true; out({ event: 'ready', data: {} }); }
|
|
30
|
+
if (connection === 'close') {
|
|
31
|
+
ready = false;
|
|
32
|
+
const code = lastDisconnect?.error?.output?.statusCode || null;
|
|
33
|
+
out({ event: 'close', data: { code } });
|
|
34
|
+
if (!stopping && code !== DisconnectReason.loggedOut) setTimeout(() => start().catch(e => out({ event: 'error', data: { message: errorMessage(e) } })), 2000);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
sock.ev.on('messages.upsert', ({ messages, type }) => {
|
|
38
|
+
for (const m of messages) { const jid = m?.key?.remoteJid; if (jid) chats.set(jid, { id: jid, unread: (chats.get(jid)?.unread || 0) + (m?.key?.fromMe ? 0 : 1), lastMessage: normalize(m) }); }
|
|
39
|
+
out({ event: 'messages.upsert', data: { type, messages: messages.map(normalize) } });
|
|
40
|
+
});
|
|
41
|
+
sock.ev.on('contacts.upsert', items => { for (const item of items) if (item?.id) contacts.set(item.id, item); out({ event: 'contacts.upsert', data: items }); });
|
|
42
|
+
sock.ev.on('contacts.update', items => { for (const item of items) if (item?.id) contacts.set(item.id, { ...(contacts.get(item.id) || {}), ...item }); out({ event: 'contacts.update', data: items }); });
|
|
43
|
+
sock.ev.on('messages.update', updates => out({ event: 'messages.update', data: updates }));
|
|
44
|
+
sock.ev.on('groups.update', updates => out({ event: 'groups.update', data: updates }));
|
|
45
|
+
sock.ev.on('presence.update', update => out({ event: 'presence.update', data: update }));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function command(c) {
|
|
49
|
+
if (c.action === 'status') return out({ request_id: c.request_id, ok: true, result: { ready, alive: true } });
|
|
50
|
+
if (c.action === 'start') return out({ request_id: c.request_id, ok: true, result: { starting: true } });
|
|
51
|
+
if (c.action === 'send_message') {
|
|
52
|
+
if (!sock || !ready) throw new Error('WhatsApp is not connected');
|
|
53
|
+
const jid = String(c.to).replace(/\D/g, '') + '@s.whatsapp.net';
|
|
54
|
+
const result = await sock.sendMessage(jid, c.content || { text: c.text });
|
|
55
|
+
return out({ request_id: c.request_id, ok: true, result: { id: result?.key?.id || null, to: jid } });
|
|
56
|
+
}
|
|
57
|
+
if (c.action === 'pairing_code') {
|
|
58
|
+
if (!sock) throw new Error('WhatsApp socket is not initialized');
|
|
59
|
+
return out({ request_id: c.request_id, ok: true, result: { code: await sock.requestPairingCode(String(c.phone).replace(/\D/g, '')) } });
|
|
60
|
+
}
|
|
61
|
+
if (c.action === 'get_chats') return out({ request_id: c.request_id, ok: true, result: { chats: [...chats.values()] } });
|
|
62
|
+
if (c.action === 'get_contacts') return out({ request_id: c.request_id, ok: true, result: { contacts: [...contacts.values()] } });
|
|
63
|
+
if (c.action === 'download_media') {
|
|
64
|
+
const message = c.message;
|
|
65
|
+
if (!message || typeof message !== 'object') throw new Error('message object is required');
|
|
66
|
+
const buffer = await downloadMediaMessage(message, 'buffer', {});
|
|
67
|
+
return out({ request_id: c.request_id, ok: true, result: { data: Buffer.from(buffer).toString('base64') } });
|
|
68
|
+
}
|
|
69
|
+
if (c.action === 'forward_message') {
|
|
70
|
+
if (!sock || !ready) throw new Error('WhatsApp is not connected');
|
|
71
|
+
const jid = String(c.to).replace(/\D/g, '') + '@s.whatsapp.net';
|
|
72
|
+
const result = await sock.sendMessage(jid, { forward: c.message });
|
|
73
|
+
return out({ request_id: c.request_id, ok: true, result: { id: result?.key?.id || null, to: jid } });
|
|
74
|
+
}
|
|
75
|
+
if (c.action === 'edit_message') {
|
|
76
|
+
if (!sock || !ready) throw new Error('WhatsApp is not connected');
|
|
77
|
+
const jid = String(c.to).replace(/\D/g, '') + '@s.whatsapp.net';
|
|
78
|
+
const result = await sock.sendMessage(jid, { text: String(c.text) }, { edit: c.key });
|
|
79
|
+
return out({ request_id: c.request_id, ok: true, result: { id: result?.key?.id || null, to: jid } });
|
|
80
|
+
}
|
|
81
|
+
if (c.action === 'baileys_call') {
|
|
82
|
+
if (!sock || !ready) throw new Error('WhatsApp is not connected');
|
|
83
|
+
const allowed = new Set([
|
|
84
|
+
'relayMessage', 'sendPresenceUpdate', 'readMessages', 'onWhatsApp',
|
|
85
|
+
'fetchStatus', 'profilePictureUrl', 'fetchBlocklist',
|
|
86
|
+
'updateProfileName', 'updateProfileStatus', 'updateProfilePicture',
|
|
87
|
+
'updateBlockStatus', 'groupCreate', 'groupMetadata',
|
|
88
|
+
'groupUpdateSubject', 'groupUpdateDescription',
|
|
89
|
+
'groupParticipantsUpdate', 'groupInviteCode', 'groupRevokeInvite',
|
|
90
|
+
'groupSettingUpdate', 'groupAcceptInvite', 'chatModify', 'presenceSubscribe', 'getUSyncDevices'
|
|
91
|
+
]);
|
|
92
|
+
if (!allowed.has(String(c.method)) || typeof sock[c.method] !== 'function') throw new Error('Unsupported Baileys method: ' + c.method);
|
|
93
|
+
const args = Array.isArray(c.args) ? c.args : [];
|
|
94
|
+
const value = await sock[c.method](...args);
|
|
95
|
+
return out({ request_id: c.request_id, ok: true, result: { value } });
|
|
96
|
+
}
|
|
97
|
+
if (c.action === 'logout') { stopping = true; await sock?.logout(); return out({ request_id: c.request_id, ok: true, result: {} }); }
|
|
98
|
+
if (c.action === 'exit') { stopping = true; sock?.end(undefined); out({ request_id: c.request_id, ok: true, result: {} }); process.exit(0); }
|
|
99
|
+
throw new Error('Unknown action: ' + c.action);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', line => {
|
|
103
|
+
if (!line.trim()) return;
|
|
104
|
+
try { const c = JSON.parse(line); command(c).catch(e => out({ request_id: c.request_id, ok: false, error: { message: errorMessage(e) } })); }
|
|
105
|
+
catch (e) { out({ ok: false, error: { message: errorMessage(e) } }); }
|
|
106
|
+
});
|
|
107
|
+
start().catch(e => out({ event: 'error', data: { message: errorMessage(e) } }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"private":true,"type":"commonjs","dependencies":{"@whiskeysockets/baileys":"6.7.24"}}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const makeWASocket = require('@whiskeysockets/baileys').default;
|
|
3
|
+
const { useMultiFileAuthState, DisconnectReason, Browsers } = require('@whiskeysockets/baileys');
|
|
4
|
+
|
|
5
|
+
const authDir = process.env.nava_link_AUTH_DIR || './auth';
|
|
6
|
+
let sock = null;
|
|
7
|
+
let ready = false;
|
|
8
|
+
let stopping = false;
|
|
9
|
+
|
|
10
|
+
function out(value) { process.stdout.write(JSON.stringify(value) + '\n'); }
|
|
11
|
+
function errorMessage(e) { return String(e?.message || e); }
|
|
12
|
+
function textOf(m) {
|
|
13
|
+
const c = m?.message || {};
|
|
14
|
+
return c.conversation || c.extendedTextMessage?.text || c.imageMessage?.caption || c.videoMessage?.caption || c.documentMessage?.caption;
|
|
15
|
+
}
|
|
16
|
+
function normalize(m) {
|
|
17
|
+
const jid = m?.key?.remoteJid || '';
|
|
18
|
+
return { id: m?.key?.id || '', chat_id: jid, sender_id: m?.key?.participant || (m?.key?.fromMe ? null : jid), from_me: !!m?.key?.fromMe, is_group: jid.endsWith('@g.us'), text: textOf(m), type: Object.keys(m?.message || {})[0] || 'unknown', timestamp: Number(m?.messageTimestamp || 0) || null, push_name: m?.pushName || null, raw: m };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function start() {
|
|
22
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
23
|
+
sock = makeWASocket({ auth: state, printQRInTerminal: false, browser: Browsers.ubuntu('Chrome'), markOnlineOnConnect: false, syncFullHistory: false });
|
|
24
|
+
sock.ev.on('creds.update', saveCreds);
|
|
25
|
+
sock.ev.on('connection.update', ({ connection, lastDisconnect, qr }) => {
|
|
26
|
+
if (qr) out({ event: 'qr', data: qr });
|
|
27
|
+
if (connection === 'open') { ready = true; out({ event: 'ready', data: {} }); }
|
|
28
|
+
if (connection === 'close') {
|
|
29
|
+
ready = false;
|
|
30
|
+
const code = lastDisconnect?.error?.output?.statusCode || null;
|
|
31
|
+
out({ event: 'close', data: { code } });
|
|
32
|
+
if (!stopping && code !== DisconnectReason.loggedOut) setTimeout(() => start().catch(e => out({ event: 'error', data: { message: errorMessage(e) } })), 2000);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
sock.ev.on('messages.upsert', ({ messages, type }) => out({ event: 'messages.upsert', data: { type, messages: messages.map(normalize) } }));
|
|
36
|
+
sock.ev.on('messages.update', updates => out({ event: 'messages.update', data: updates }));
|
|
37
|
+
sock.ev.on('groups.update', updates => out({ event: 'groups.update', data: updates }));
|
|
38
|
+
sock.ev.on('presence.update', update => out({ event: 'presence.update', data: update }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function command(c) {
|
|
42
|
+
if (c.action === 'status') return out({ request_id: c.request_id, ok: true, result: { ready, alive: true } });
|
|
43
|
+
if (c.action === 'start') return out({ request_id: c.request_id, ok: true, result: { starting: true } });
|
|
44
|
+
if (c.action === 'send_message') {
|
|
45
|
+
if (!sock || !ready) throw new Error('WhatsApp is not connected');
|
|
46
|
+
const jid = String(c.to).replace(/\D/g, '') + '@s.whatsapp.net';
|
|
47
|
+
const result = await sock.sendMessage(jid, c.content || { text: c.text });
|
|
48
|
+
return out({ request_id: c.request_id, ok: true, result: { id: result?.key?.id || null, to: jid } });
|
|
49
|
+
}
|
|
50
|
+
if (c.action === 'pairing_code') {
|
|
51
|
+
if (!sock) throw new Error('WhatsApp socket is not initialized');
|
|
52
|
+
return out({ request_id: c.request_id, ok: true, result: { code: await sock.requestPairingCode(String(c.phone).replace(/\D/g, '')) } });
|
|
53
|
+
}
|
|
54
|
+
if (c.action === 'logout') { stopping = true; await sock?.logout(); return out({ request_id: c.request_id, ok: true, result: {} }); }
|
|
55
|
+
if (c.action === 'exit') { stopping = true; sock?.end(undefined); out({ request_id: c.request_id, ok: true, result: {} }); process.exit(0); }
|
|
56
|
+
throw new Error('Unknown action: ' + c.action);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', line => {
|
|
60
|
+
if (!line.trim()) return;
|
|
61
|
+
try { const c = JSON.parse(line); command(c).catch(e => out({ request_id: c.request_id, ok: false, error: { message: errorMessage(e) } })); }
|
|
62
|
+
catch (e) { out({ ok: false, error: { message: errorMessage(e) } }); }
|
|
63
|
+
});
|
|
64
|
+
start().catch(e => out({ event: 'error', data: { message: errorMessage(e) } }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"private":true,"type":"commonjs","dependencies":{"@whiskeysockets/baileys":"6.7.24"}}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nava-link
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python-native messaging client with an embedded runtime
|
|
5
|
+
Author: nava_link contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# nava-link
|
|
11
|
+
|
|
12
|
+
مكتبة Python لإنشاء تطبيقات وبوتات واتساب بواجهة بسيطة. تدعم الاتصال بكود الاقتران، استقبال الرسائل، إرسال النصوص والوسائط، إدارة المجموعات، الحساب، الجلسة، المحادثات، جهات الاتصال والأجهزة.
|
|
13
|
+
|
|
14
|
+
> المكتبة غير رسمية وغير تابعة لشركة WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، وتجنب الرسائل المزعجة أو الإرسال العشوائي.
|
|
15
|
+
|
|
16
|
+
## التثبيت
|
|
17
|
+
|
|
18
|
+
على Termux:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pkg update -y
|
|
22
|
+
pkg install -y python nodejs-lts
|
|
23
|
+
pip install nava-link==1.0.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
الاستيراد:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from nava_link import MoXstarClient
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## الاتصال بكود الاقتران
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import asyncio
|
|
36
|
+
from nava_link import MoXstarClient
|
|
37
|
+
|
|
38
|
+
PHONE = "971501234567"
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
42
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
43
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
44
|
+
|
|
45
|
+
await client.start()
|
|
46
|
+
await asyncio.sleep(5)
|
|
47
|
+
code = await client.request_pairing_code(PHONE)
|
|
48
|
+
print("كود الاقتران:", code)
|
|
49
|
+
print("واتساب > الإعدادات > الأجهزة المرتبطة > ربط جهاز")
|
|
50
|
+
print("ثم اختر الربط باستخدام رقم الهاتف وأدخل الكود.")
|
|
51
|
+
|
|
52
|
+
await client.wait_until_ready()
|
|
53
|
+
await client.wait_closed()
|
|
54
|
+
|
|
55
|
+
asyncio.run(main())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
استخدم الرقم الدولي بدون `+` أو مسافات. إذا كانت جلسة `auth` محفوظة، شغّل البرنامج وانتظر الاتصال ولا تطلب كودًا جديدًا.
|
|
59
|
+
|
|
60
|
+
## الرسائل والوسائط
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
await client.send_text("201099887766", "مرحبًا من nava-link")
|
|
64
|
+
await client.send_message("201099887766", {"text": "رسالة مخصصة"})
|
|
65
|
+
await client.send_image("201099887766", "/storage/emulated/0/photo.jpg", "صورة")
|
|
66
|
+
await client.send_video("201099887766", "/storage/emulated/0/video.mp4", "فيديو")
|
|
67
|
+
await client.send_document("201099887766", "/storage/emulated/0/file.pdf", "file.pdf", "application/pdf")
|
|
68
|
+
await client.send_audio("201099887766", "/storage/emulated/0/audio.mp3", "audio/mpeg")
|
|
69
|
+
await client.send_location("201099887766", 25.2048, 55.2708, "دبي")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## استقبال الرسائل
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
async def on_messages(event):
|
|
76
|
+
for message in event["messages"]:
|
|
77
|
+
print(message.chat_id, message.sender_id, message.text)
|
|
78
|
+
if message.text == "!ping":
|
|
79
|
+
await client.send_text(message.chat_id, "pong")
|
|
80
|
+
|
|
81
|
+
client.on("messages.upsert", on_messages)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
الأحداث: `qr`, `ready`, `messages.upsert`, `messages.update`, `groups.update`, `presence.update`, `contacts.upsert`, `contacts.update`, `close`, `error`, `log`.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.on("ready", handler)
|
|
88
|
+
client.off("ready", handler)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## الحساب والحالة
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
await client.send_presence("composing", "201099887766@s.whatsapp.net")
|
|
95
|
+
await client.check_number(["201099887766", "971501234567"])
|
|
96
|
+
await client.get_status("201099887766@s.whatsapp.net")
|
|
97
|
+
await client.get_profile_picture("201099887766@s.whatsapp.net")
|
|
98
|
+
await client.get_blocklist()
|
|
99
|
+
await client.update_profile_name("اسم تجريبي")
|
|
100
|
+
await client.update_profile_status("متاح الآن")
|
|
101
|
+
await client.update_profile_picture("201099887766@s.whatsapp.net", "/storage/emulated/0/profile.jpg")
|
|
102
|
+
await client.block("201099887766@s.whatsapp.net", "block")
|
|
103
|
+
await client.block("201099887766@s.whatsapp.net", "unblock")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## الأجهزة والمحادثات وجهات الاتصال
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
await client.get_devices()
|
|
110
|
+
await client.get_chats()
|
|
111
|
+
await client.get_contacts()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
تعيد المحادثات وجهات الاتصال التي رصدتها الجلسة أثناء التشغيل الحالي.
|
|
115
|
+
|
|
116
|
+
## الوسائط والرسائل
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
await client.download_media(message.raw, "/storage/emulated/0/downloaded_file")
|
|
120
|
+
await client.forward_message("201099887766", message.raw)
|
|
121
|
+
await client.edit_message("201099887766", {"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"}, "النص الجديد")
|
|
122
|
+
await client.delete_message({"remoteJid": "201099887766@s.whatsapp.net", "fromMe": True, "id": "MESSAGE_ID"})
|
|
123
|
+
await client.mark_as_read([{"remoteJid": "201099887766@s.whatsapp.net", "fromMe": False, "id": "MESSAGE_ID"}])
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## المجموعات
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
await client.create_group("مجموعة تجريبية", ["201099887766", "971501234567"])
|
|
130
|
+
await client.get_group_info("120363000000000000@g.us")
|
|
131
|
+
await client.update_group_subject("120363000000000000@g.us", "اسم جديد")
|
|
132
|
+
await client.update_group_description("120363000000000000@g.us", "وصف جديد")
|
|
133
|
+
await client.update_group_participants("120363000000000000@g.us", ["201099887766"], "add")
|
|
134
|
+
await client.get_group_invite_code("120363000000000000@g.us")
|
|
135
|
+
await client.revoke_group_invite("120363000000000000@g.us")
|
|
136
|
+
await client.update_group_settings("120363000000000000@g.us", "announcement")
|
|
137
|
+
await client.accept_group_invite("INVITE_CODE")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
إجراءات الأعضاء: `add`, `remove`, `promote`, `demote`.
|
|
141
|
+
|
|
142
|
+
## إدارة الاتصال
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
await client.start()
|
|
146
|
+
await client.wait_until_ready()
|
|
147
|
+
await client.wait_closed()
|
|
148
|
+
await client.close()
|
|
149
|
+
await client.logout()
|
|
150
|
+
print(client.connected, client.alive)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## الطلبات المتقدمة
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
await client.raw_request("fetchStatus", "201099887766@s.whatsapp.net")
|
|
157
|
+
await client.relay_message("201099887766@s.whatsapp.net", {"conversation": "رسالة"}, {})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
استخدم البيانات الصحيحة وبأحجام معقولة فقط. لا ترسل بيانات مصطنعة أو ضخمة، ولا تستخدم المكتبة للإزعاج أو الإغراق.
|
|
161
|
+
|
|
162
|
+
## قائمة الدوال الكاملة
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
start, wait_until_ready, wait_closed, request_pairing_code
|
|
166
|
+
send_message, send_text, send_image, send_video, send_document, send_audio, send_location
|
|
167
|
+
raw_request, relay_message, send_presence, read_messages, mark_as_read
|
|
168
|
+
check_number, get_status, get_profile_picture, get_blocklist
|
|
169
|
+
update_profile_name, update_profile_status, update_profile_picture, block
|
|
170
|
+
get_devices, get_chats, get_contacts, download_media
|
|
171
|
+
forward_message, delete_message, edit_message
|
|
172
|
+
create_group, get_group_info, update_group_subject, update_group_description
|
|
173
|
+
update_group_participants, get_group_invite_code, revoke_group_invite
|
|
174
|
+
update_group_settings, accept_group_invite, logout, close, on, off
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## مثال بوت كامل
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import asyncio
|
|
181
|
+
from nava_link import MoXstarClient
|
|
182
|
+
|
|
183
|
+
async def main():
|
|
184
|
+
client = MoXstarClient(auth_dir="./auth")
|
|
185
|
+
|
|
186
|
+
async def on_message(event):
|
|
187
|
+
for message in event["messages"]:
|
|
188
|
+
if message.text == "!ping":
|
|
189
|
+
await client.send_text(message.chat_id, "pong")
|
|
190
|
+
|
|
191
|
+
client.on("messages.upsert", on_message)
|
|
192
|
+
client.on("ready", lambda _: print("تم الاتصال"))
|
|
193
|
+
client.on("error", lambda error: print("خطأ:", error))
|
|
194
|
+
await client.start()
|
|
195
|
+
await client.wait_until_ready()
|
|
196
|
+
print("البوت يعمل الآن")
|
|
197
|
+
try:
|
|
198
|
+
await client.wait_closed()
|
|
199
|
+
finally:
|
|
200
|
+
await client.close()
|
|
201
|
+
|
|
202
|
+
asyncio.run(main())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## ملاحظات أمنية
|
|
206
|
+
|
|
207
|
+
- لا تشارك مجلد `auth` أو رمز الاقتران أو QR.
|
|
208
|
+
- لا تشغّل نسختين باستخدام مجلد الجلسة نفسه.
|
|
209
|
+
- لا تحذف `auth` إذا كنت تريد الاحتفاظ بالجلسة.
|
|
210
|
+
- استخدم أرقامًا دولية بدون `+` أو مسافات.
|
|
211
|
+
- تجنب الرسائل الجماعية أو المتكررة.
|
|
212
|
+
|
|
213
|
+
PyPI: https://pypi.org/project/nava-link/
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
nava_link/__init__.py
|
|
4
|
+
nava_link/client.py
|
|
5
|
+
nava_link/engine.js
|
|
6
|
+
nava_link/package.json
|
|
7
|
+
nava_link.egg-info/PKG-INFO
|
|
8
|
+
nava_link.egg-info/SOURCES.txt
|
|
9
|
+
nava_link.egg-info/dependency_links.txt
|
|
10
|
+
nava_link.egg-info/top_level.txt
|
|
11
|
+
nava_link/runtime/engine.js
|
|
12
|
+
nava_link/runtime/package.json
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nava_link
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nava-link"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python-native messaging client with an embedded runtime"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "nava_link contributors"}]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
packages = ["nava_link"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.package-data]
|
|
19
|
+
nava_link = ["engine.js", "package.json", "runtime/node", "runtime/node.exe", "runtime/engine.js", "runtime/package.json", "runtime/node_modules/**/*"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from nava_link.client import Message, EventBus
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_message_model():
|
|
6
|
+
m = Message.from_dict({'id': 'x', 'chat_id': '1@s.whatsapp.net', 'text': 'hello'})
|
|
7
|
+
assert m.id == 'x'
|
|
8
|
+
assert m.text == 'hello'
|
|
9
|
+
assert m.chat_id == '1@s.whatsapp.net'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_event_bus_supports_async_handlers():
|
|
13
|
+
async def run():
|
|
14
|
+
bus = EventBus()
|
|
15
|
+
seen = []
|
|
16
|
+
bus.on('message', lambda value: seen.append(value))
|
|
17
|
+
await bus.emit('message', 'ok')
|
|
18
|
+
assert seen == ['ok']
|
|
19
|
+
asyncio.run(run())
|