mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-04-27 15:05:56 +00:00
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
# SPDX-License-Identifier: 0BSD
|
|
|
|
"""Android (Chaquopy): mirror selected websocket payloads to OS notifications."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any
|
|
|
|
logger = logging.getLogger("meshchatx.android_push_bridge")
|
|
|
|
_ws_hook_installed = False
|
|
|
|
|
|
def _is_chaquopy_android() -> bool:
|
|
try:
|
|
import java # noqa: F401
|
|
except ImportError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def lxmf_delivery_notification_text(payload: dict[str, Any]) -> tuple[str, str] | None:
|
|
"""Return (title, body) for a system notification, or None to skip."""
|
|
if payload.get("type") != "lxmf.delivery":
|
|
return None
|
|
if payload.get("sieve_suppress_notifications"):
|
|
return None
|
|
msg = payload.get("lxmf_message")
|
|
if not isinstance(msg, dict):
|
|
return None
|
|
if not msg.get("is_incoming"):
|
|
return None
|
|
sender = str(payload.get("remote_identity_name") or "").strip() or "Mesh"
|
|
if msg.get("is_reaction"):
|
|
emoji = str(msg.get("reaction_emoji") or "").strip()
|
|
body = f"Reaction {emoji}".strip() if emoji else "Reaction"
|
|
return (sender, body)
|
|
fields = msg.get("fields")
|
|
if isinstance(fields, dict) and not msg.get("title") and not msg.get("content"):
|
|
keys = set(fields.keys())
|
|
if keys <= {"telemetry"}:
|
|
return None
|
|
title = str(msg.get("title") or "").strip()
|
|
content = str(msg.get("content") or "").strip()
|
|
if len(content) > 200:
|
|
content = content[:197] + "..."
|
|
if title and content:
|
|
return (sender, f"{title}\n{content}")
|
|
if title:
|
|
return (sender, title)
|
|
if content:
|
|
return (sender, content)
|
|
if isinstance(fields, dict) and fields.get("image"):
|
|
return (sender, "Image message")
|
|
if isinstance(fields, dict) and fields.get("audio"):
|
|
return (sender, "Audio message")
|
|
if isinstance(fields, dict) and fields.get("file_attachments"):
|
|
return (sender, "Attachment")
|
|
return (sender, "New message")
|
|
|
|
|
|
def _notify_java(title: str, body: str, dedupe_hex: str | None) -> None:
|
|
try:
|
|
from com.meshchatx import AndroidNotificationBridge # type: ignore[import-not-found,import-untyped]
|
|
except Exception as exc:
|
|
logger.debug("Android notification bridge unavailable: %s", exc)
|
|
return
|
|
try:
|
|
AndroidNotificationBridge.showInboundMessage(title, body, dedupe_hex)
|
|
except Exception as exc:
|
|
logger.debug("showInboundMessage failed: %s", exc)
|
|
|
|
|
|
def _after_websocket_broadcast(data: object) -> None:
|
|
if not isinstance(data, str):
|
|
return
|
|
try:
|
|
payload = json.loads(data)
|
|
except json.JSONDecodeError:
|
|
return
|
|
if not isinstance(payload, dict):
|
|
return
|
|
pair = lxmf_delivery_notification_text(payload)
|
|
if not pair:
|
|
return
|
|
title, body = pair
|
|
msg = payload.get("lxmf_message")
|
|
dedupe = None
|
|
if isinstance(msg, dict):
|
|
h = msg.get("hash")
|
|
if isinstance(h, str) and len(h) >= 8:
|
|
dedupe = h
|
|
_notify_java(title, body, dedupe)
|
|
|
|
|
|
def install_websocket_hook(reticulum_mesh_chat_cls: type) -> None:
|
|
global _ws_hook_installed
|
|
if not _is_chaquopy_android():
|
|
return
|
|
if _ws_hook_installed:
|
|
return
|
|
orig = reticulum_mesh_chat_cls.websocket_broadcast
|
|
|
|
async def _wrapped(self, data):
|
|
result = await orig(self, data)
|
|
try:
|
|
_after_websocket_broadcast(data)
|
|
except Exception:
|
|
logger.debug("android ws hook post-broadcast failed", exc_info=True)
|
|
return result
|
|
|
|
reticulum_mesh_chat_cls.websocket_broadcast = _wrapped
|
|
_ws_hook_installed = True
|