x-nava 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.
x_nava-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: x-nava
3
+ Version: 1.0.0
4
+ Summary: Python-native messaging client with an embedded runtime
5
+ Author: x_nava contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # x_nava for Python — Termux
11
+
12
+ مكتبة Python للتعامل مع WhatsApp عبر Baileys. هذه النسخة مناسبة لـ **Termux على Android**: لا تستخدم Node.js المضمّن الخاص بسطح المكتب، بل تعتمد على Node.js الذي يثبّته Termux.
13
+
14
+ > غير تابعة لـ WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، ولا تستخدمها للسبام.
15
+
16
+ ## التثبيت على Termux
17
+
18
+ ```bash
19
+ pkg update -y
20
+ pkg install -y python nodejs-lts unzip
21
+ unzip x_nava-1.0.0-termux.zip
22
+ cd x_nava-1.0.0-termux
23
+ python -m pip install --upgrade pip
24
+ python -m pip install .
25
+ ```
26
+
27
+ في أول تشغيل ستثبت المكتبة تلقائياً اعتماد Baileys داخل `~/.x_nava_runtime`. للتثبيت اليدوي:
28
+
29
+ ```bash
30
+ cd ~/.x_nava_runtime
31
+ npm install --no-audit --no-fund --save-exact
32
+ ```
33
+
34
+ ## مثال سريع
35
+
36
+ ```python
37
+ import asyncio
38
+ from x_nava import MoXstarClient
39
+
40
+ async def main():
41
+ client = MoXstarClient(auth_dir='./auth')
42
+ client.on('qr', lambda code: print('QR:', code))
43
+ client.on('ready', lambda _: print('x_nava جاهزة'))
44
+ await client.start()
45
+ await client.wait_until_ready()
46
+ await client.wait_closed()
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ يمكن تحديد مسار Node يدوياً عبر متغير البيئة `NAVAPRO_NODE`، لكن الوضع الافتراضي يستخدم `node` الموجود في Termux.
52
+
53
+ ## ملاحظات Android/Termux
54
+
55
+ - استخدم `nodejs-lts` وليس نسخة Node مكتبية مضمّنة.
56
+ - يفضّل تعطيل تحسين البطارية لتطبيق Termux حتى لا ينقطع الاتصال في الخلفية.
57
+ - الاتصال الحي يحتاج QR أو Pairing Code ورقم WhatsApp حقيقي.
58
+ - اختبارات Python المحلية لا تتصل بواتساب.
x_nava-1.0.0/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # x_nava for Python — Termux
2
+
3
+ مكتبة Python للتعامل مع WhatsApp عبر Baileys. هذه النسخة مناسبة لـ **Termux على Android**: لا تستخدم Node.js المضمّن الخاص بسطح المكتب، بل تعتمد على Node.js الذي يثبّته Termux.
4
+
5
+ > غير تابعة لـ WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، ولا تستخدمها للسبام.
6
+
7
+ ## التثبيت على Termux
8
+
9
+ ```bash
10
+ pkg update -y
11
+ pkg install -y python nodejs-lts unzip
12
+ unzip x_nava-1.0.0-termux.zip
13
+ cd x_nava-1.0.0-termux
14
+ python -m pip install --upgrade pip
15
+ python -m pip install .
16
+ ```
17
+
18
+ في أول تشغيل ستثبت المكتبة تلقائياً اعتماد Baileys داخل `~/.x_nava_runtime`. للتثبيت اليدوي:
19
+
20
+ ```bash
21
+ cd ~/.x_nava_runtime
22
+ npm install --no-audit --no-fund --save-exact
23
+ ```
24
+
25
+ ## مثال سريع
26
+
27
+ ```python
28
+ import asyncio
29
+ from x_nava import MoXstarClient
30
+
31
+ async def main():
32
+ client = MoXstarClient(auth_dir='./auth')
33
+ client.on('qr', lambda code: print('QR:', code))
34
+ client.on('ready', lambda _: print('x_nava جاهزة'))
35
+ await client.start()
36
+ await client.wait_until_ready()
37
+ await client.wait_closed()
38
+
39
+ asyncio.run(main())
40
+ ```
41
+
42
+ يمكن تحديد مسار Node يدوياً عبر متغير البيئة `NAVAPRO_NODE`، لكن الوضع الافتراضي يستخدم `node` الموجود في Termux.
43
+
44
+ ## ملاحظات Android/Termux
45
+
46
+ - استخدم `nodejs-lts` وليس نسخة Node مكتبية مضمّنة.
47
+ - يفضّل تعطيل تحسين البطارية لتطبيق Termux حتى لا ينقطع الاتصال في الخلفية.
48
+ - الاتصال الحي يحتاج QR أو Pairing Code ورقم WhatsApp حقيقي.
49
+ - اختبارات Python المحلية لا تتصل بواتساب.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "x-nava"
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 = "x_nava contributors"}]
13
+ dependencies = []
14
+
15
+ [tool.setuptools]
16
+ packages = ["x_nava"]
17
+
18
+ [tool.setuptools.package-data]
19
+ x_nava = ["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"]
x_nava-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ import asyncio
2
+ from x_nava.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())
@@ -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,333 @@
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 x_nava 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() / ".x_nava_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("x_nava could not locate its bundled or system Node runtime.")
161
+ env = os.environ.copy()
162
+ env["x_nava_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("x_nava 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 logout(self) -> None:
323
+ await self._request("logout")
324
+ self._ready = False
325
+
326
+ async def close(self) -> None:
327
+ if self.alive:
328
+ try: await self._request("exit", timeout=5)
329
+ except BridgeError: pass
330
+ if self.process and self.process.returncode is None:
331
+ try: await asyncio.wait_for(self.process.wait(), 5)
332
+ except asyncio.TimeoutError: self.process.kill()
333
+ self._ready = False
@@ -0,0 +1,80 @@
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.x_nava_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 === 'baileys_call') {
55
+ if (!sock || !ready) throw new Error('WhatsApp is not connected');
56
+ const allowed = new Set([
57
+ 'relayMessage', 'sendPresenceUpdate', 'readMessages', 'onWhatsApp',
58
+ 'fetchStatus', 'profilePictureUrl', 'fetchBlocklist',
59
+ 'updateProfileName', 'updateProfileStatus', 'updateProfilePicture',
60
+ 'updateBlockStatus', 'groupCreate', 'groupMetadata',
61
+ 'groupUpdateSubject', 'groupUpdateDescription',
62
+ 'groupParticipantsUpdate', 'groupInviteCode', 'groupRevokeInvite',
63
+ 'groupSettingUpdate', 'groupAcceptInvite', 'chatModify', 'presenceSubscribe'
64
+ ]);
65
+ if (!allowed.has(String(c.method)) || typeof sock[c.method] !== 'function') throw new Error('Unsupported Baileys method: ' + c.method);
66
+ const args = Array.isArray(c.args) ? c.args : [];
67
+ const value = await sock[c.method](...args);
68
+ return out({ request_id: c.request_id, ok: true, result: { value } });
69
+ }
70
+ if (c.action === 'logout') { stopping = true; await sock?.logout(); return out({ request_id: c.request_id, ok: true, result: {} }); }
71
+ if (c.action === 'exit') { stopping = true; sock?.end(undefined); out({ request_id: c.request_id, ok: true, result: {} }); process.exit(0); }
72
+ throw new Error('Unknown action: ' + c.action);
73
+ }
74
+
75
+ readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on('line', line => {
76
+ if (!line.trim()) return;
77
+ try { const c = JSON.parse(line); command(c).catch(e => out({ request_id: c.request_id, ok: false, error: { message: errorMessage(e) } })); }
78
+ catch (e) { out({ ok: false, error: { message: errorMessage(e) } }); }
79
+ });
80
+ 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.x_nava_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,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: x-nava
3
+ Version: 1.0.0
4
+ Summary: Python-native messaging client with an embedded runtime
5
+ Author: x_nava contributors
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # x_nava for Python — Termux
11
+
12
+ مكتبة Python للتعامل مع WhatsApp عبر Baileys. هذه النسخة مناسبة لـ **Termux على Android**: لا تستخدم Node.js المضمّن الخاص بسطح المكتب، بل تعتمد على Node.js الذي يثبّته Termux.
13
+
14
+ > غير تابعة لـ WhatsApp أو Meta. استخدمها وفق القوانين وشروط الخدمة، ولا تستخدمها للسبام.
15
+
16
+ ## التثبيت على Termux
17
+
18
+ ```bash
19
+ pkg update -y
20
+ pkg install -y python nodejs-lts unzip
21
+ unzip x_nava-1.0.0-termux.zip
22
+ cd x_nava-1.0.0-termux
23
+ python -m pip install --upgrade pip
24
+ python -m pip install .
25
+ ```
26
+
27
+ في أول تشغيل ستثبت المكتبة تلقائياً اعتماد Baileys داخل `~/.x_nava_runtime`. للتثبيت اليدوي:
28
+
29
+ ```bash
30
+ cd ~/.x_nava_runtime
31
+ npm install --no-audit --no-fund --save-exact
32
+ ```
33
+
34
+ ## مثال سريع
35
+
36
+ ```python
37
+ import asyncio
38
+ from x_nava import MoXstarClient
39
+
40
+ async def main():
41
+ client = MoXstarClient(auth_dir='./auth')
42
+ client.on('qr', lambda code: print('QR:', code))
43
+ client.on('ready', lambda _: print('x_nava جاهزة'))
44
+ await client.start()
45
+ await client.wait_until_ready()
46
+ await client.wait_closed()
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ يمكن تحديد مسار Node يدوياً عبر متغير البيئة `NAVAPRO_NODE`، لكن الوضع الافتراضي يستخدم `node` الموجود في Termux.
52
+
53
+ ## ملاحظات Android/Termux
54
+
55
+ - استخدم `nodejs-lts` وليس نسخة Node مكتبية مضمّنة.
56
+ - يفضّل تعطيل تحسين البطارية لتطبيق Termux حتى لا ينقطع الاتصال في الخلفية.
57
+ - الاتصال الحي يحتاج QR أو Pairing Code ورقم WhatsApp حقيقي.
58
+ - اختبارات Python المحلية لا تتصل بواتساب.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_client.py
4
+ x_nava/__init__.py
5
+ x_nava/client.py
6
+ x_nava/engine.js
7
+ x_nava/package.json
8
+ x_nava.egg-info/PKG-INFO
9
+ x_nava.egg-info/SOURCES.txt
10
+ x_nava.egg-info/dependency_links.txt
11
+ x_nava.egg-info/top_level.txt
12
+ x_nava/runtime/engine.js
13
+ x_nava/runtime/package.json
@@ -0,0 +1 @@
1
+ x_nava