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.
@@ -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,4 @@
1
+ from .client import MoXstarClient, Message, ConnectionState, BridgeError, BridgeOffline, BridgeTimeout
2
+
3
+ __all__ = ["MoXstarClient", "Message", "ConnectionState", "BridgeError", "BridgeOffline", "BridgeTimeout"]
4
+ __version__ = "1.0.0"
@@ -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
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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())