Implement WhatsApp linking

This commit is contained in:
2026-02-15 23:21:18 +00:00
parent 88224d972c
commit 10af1e4d6b
3 changed files with 63 additions and 36 deletions

View File

@@ -1,4 +1,5 @@
import asyncio
import os
import re
import time
from urllib.parse import quote_plus
@@ -49,6 +50,8 @@ class WhatsAppClient(ClientBase):
self.database_url = str(
getattr(settings, "WHATSAPP_DATABASE_URL", "")
).strip()
safe_name = re.sub(r"[^a-zA-Z0-9_.-]+", "_", self.client_name) or "gia_whatsapp"
self.session_db = self.database_url or f"/tmp/{safe_name}.db"
transport.register_runtime_client(self.service, self)
self._publish_state(
@@ -60,6 +63,7 @@ class WhatsAppClient(ClientBase):
),
accounts=[],
last_event="init",
session_db=self.session_db,
)
def _publish_state(self, **updates):
@@ -78,6 +82,7 @@ class WhatsAppClient(ClientBase):
async def _run(self):
try:
import neonize.aioze.client as wa_client_mod
from neonize.aioze.client import NewAClient
from neonize.aioze import events as wa_events
try:
@@ -100,9 +105,41 @@ class WhatsAppClient(ClientBase):
self.log.warning("whatsapp neonize import failed: %s", exc)
return
# Neonize async module ships with its own global event loop object.
# In this runtime we already have a live asyncio loop; bind Neonize's
# globals to it so QR/pair callbacks and connect tasks actually execute.
try:
wa_events.event_global_loop = self.loop
wa_client_mod.event_global_loop = self.loop
self._publish_state(
neonize_loop_bound=True,
neonize_loop_type=str(type(self.loop).__name__),
last_event="neonize_loop_bound",
)
except Exception as exc:
self._publish_state(
neonize_loop_bound=False,
last_event="neonize_loop_bind_failed",
last_error=str(exc),
)
self.log.warning("failed binding neonize loop: %s", exc)
self._build_jid = wa_build_jid
self._chat_presence = ChatPresence
self._chat_presence_media = ChatPresenceMedia
try:
db_dir = os.path.dirname(self.session_db)
if db_dir:
os.makedirs(db_dir, exist_ok=True)
except Exception as exc:
self._publish_state(
connected=False,
warning=f"WhatsApp DB path setup failed: {exc}",
last_event="db_path_setup_failed",
last_error=str(exc),
)
self.log.warning("whatsapp db path setup failed: %s", exc)
return
self._client = self._build_client(NewAClient)
if self._client is None:
self._publish_state(
@@ -216,8 +253,13 @@ class WhatsAppClient(ClientBase):
return
now_ts = int(time.time())
try:
if hasattr(self._client, "is_connected"):
connected_value = await self._maybe_await(self._client.is_connected())
check_connected = getattr(self._client, "is_connected", None)
if check_connected is not None:
connected_value = (
await self._maybe_await(check_connected())
if callable(check_connected)
else await self._maybe_await(check_connected)
)
if connected_value:
self._connected = True
self._publish_state(
@@ -322,6 +364,7 @@ class WhatsAppClient(ClientBase):
last_event="qr_handler",
pair_status="qr_ready",
qr_received_at=int(time.time()),
qr_probe_result="event",
last_error="",
)
@@ -359,23 +402,16 @@ class WhatsAppClient(ClientBase):
return str(raw_payload).strip()
def _build_client(self, cls):
candidates = []
if self.database_url:
candidates.append((self.client_name, self.database_url))
candidates.append((self.client_name,))
for args in candidates:
try:
return cls(*args)
except TypeError:
continue
except Exception as exc:
self.log.warning("whatsapp client init failed for args %s: %s", args, exc)
# NewAClient first arg is the SQLite filename / DB string.
try:
if self.database_url:
return cls(name=self.client_name, database=self.database_url)
return cls(name=self.client_name)
return cls(self.session_db)
except Exception as exc:
self.log.warning("whatsapp client init failed: %s", exc)
self.log.warning("whatsapp client init failed (%s): %s", self.session_db, exc)
self._publish_state(
last_event="client_init_exception",
last_error=str(exc),
session_db=self.session_db,
)
return None
def _register_event_handlers(self, wa_events):
@@ -459,6 +495,7 @@ class WhatsAppClient(ClientBase):
last_event="pair_status_qr",
pair_status="qr_ready",
qr_received_at=int(time.time()),
qr_probe_result="event",
last_error="",
)
status_raw = self._pluck(event, "Status")
@@ -501,6 +538,7 @@ class WhatsAppClient(ClientBase):
last_event="qr_event",
pair_status="qr_ready",
qr_received_at=int(time.time()),
qr_probe_result="event",
last_error="",
)

View File

@@ -4,13 +4,6 @@
{% if object.warning %}
<p class="is-size-7" style="margin-top: 0.6rem;">{{ object.warning }}</p>
{% endif %}
{% if object.debug_lines %}
<article class="notification is-light" style="margin-top: 0.6rem; margin-bottom: 0;">
<p><strong>Runtime Debug</strong></p>
<pre class="is-size-7" style="white-space: pre-wrap; margin: 0.4rem 0 0;">{% for line in object.debug_lines %}{{ line }}
{% endfor %}</pre>
</article>
{% endif %}
{% else %}
<article class="notification is-warning is-light" style="margin-bottom: 0;">
<p><strong>WhatsApp QR Not Ready.</strong></p>
@@ -34,11 +27,13 @@
</form>
{% endif %}
{% if object.debug_lines %}
<article class="notification is-light" style="margin-top: 0.6rem; margin-bottom: 0;">
<p><strong>Runtime Debug</strong></p>
<pre class="is-size-7" style="white-space: pre-wrap; margin: 0.4rem 0 0;">{% for line in object.debug_lines %}{{ line }}
<details style="margin-top: 0.6rem;">
<summary><strong>Runtime Debug</strong></summary>
<article class="notification is-light" style="margin-top: 0.5rem; margin-bottom: 0;">
<pre class="is-size-7" style="white-space: pre-wrap; margin: 0;">{% for line in object.debug_lines %}{{ line }}
{% endfor %}</pre>
</article>
</article>
</details>
{% endif %}
</article>
{% endif %}

View File

@@ -105,23 +105,17 @@ class WhatsAppAccountAdd(SuperUserRequiredMixin, ObjectRead):
qr_value = str(state.get("pair_qr") or "")
return [
f"connected={bool(state.get('connected'))}",
f"runtime_updated={_age('updated_at')}",
f"runtime_seen={_age('runtime_seen_at')}",
f"pair_requested={_age('pair_requested_at')}",
f"qr_received={_age('qr_received_at')}",
f"last_qr_probe={_age('last_qr_probe_at')}",
f"pair_status={state.get('pair_status') or '-'}",
f"pair_request_source={state.get('pair_request_source') or '-'}",
f"qr_probe_result={state.get('qr_probe_result') or '-'}",
f"qr_handler_supported={state.get('qr_handler_supported')}",
f"qr_handler_registered={state.get('qr_handler_registered')}",
f"event_hook_callable={state.get('event_hook_callable')}",
f"event_support={state.get('event_support') or {}}",
f"last_event={state.get('last_event') or '-'}",
f"last_error={state.get('last_error') or '-'}",
f"pair_qr_present={bool(qr_value)} len={len(qr_value)}",
f"accounts={state.get('accounts') or []}",
f"warning={state.get('warning') or '-'}",
f"pair_qr_present={bool(qr_value)}",
f"session_db={state.get('session_db') or '-'}",
]
def post(self, request, *args, **kwargs):