97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Reticulum adapter — wraps RNS + LXMF (the standard host-side Reticulum
|
|
messaging stack; see docs/ARCHITECTURE.md for why archy-messh talks to a
|
|
real RNode-flashed radio via RNS rather than reimplementing Reticulum's
|
|
wire format from scratch).
|
|
|
|
Reticulum has no built-in broadcast-channel concept the way Meshtastic
|
|
("channel" PSK) or MeshCore ("public channel index") do — LXMF messaging is
|
|
addressed point-to-point between Destinations. To bridge broadcast-style
|
|
text, this adapter tracks a roster of peer LXMF delivery destinations
|
|
learned from their announces (any Reticulum/Sideband/NomadNet/MeshChat user
|
|
who has announced is added), and fans outbound bridge messages out to that
|
|
roster individually. This is the simplest correct v0; a dedicated shared
|
|
GROUP destination (symmetric-key, closer to a real "channel") is a known
|
|
follow-up once Phase 0 proves the roster approach works at all.
|
|
"""
|
|
|
|
import threading
|
|
from collections.abc import Callable
|
|
|
|
|
|
class ReticulumAdapter:
|
|
NETWORK = "reticulum"
|
|
ASPECT = "lxmf.delivery"
|
|
|
|
def __init__(
|
|
self,
|
|
on_message: Callable[[str, str, str], None],
|
|
storage_path: str,
|
|
display_name: str = "archy-messh-bridge",
|
|
):
|
|
self._on_message = on_message
|
|
self._storage_path = storage_path
|
|
self._display_name = display_name
|
|
self._router = None
|
|
self._identity = None
|
|
self._destination = None
|
|
self._peers: set[bytes] = set()
|
|
self._lock = threading.Lock()
|
|
|
|
# RNS.Transport.register_announce_handler() requires an object with
|
|
# an `aspect_filter` attribute — set here so `self` can be registered
|
|
# directly as the announce handler.
|
|
self.aspect_filter = self.ASPECT
|
|
|
|
def connect(self) -> None:
|
|
import RNS
|
|
import LXMF
|
|
|
|
RNS.Reticulum()
|
|
self._router = LXMF.LXMRouter(storagepath=self._storage_path)
|
|
self._identity = RNS.Identity()
|
|
self._destination = self._router.register_delivery_identity(
|
|
self._identity, display_name=self._display_name
|
|
)
|
|
self._router.register_delivery_callback(self._handle_delivery)
|
|
RNS.Transport.register_announce_handler(self)
|
|
self._router.announce(self._destination.hash)
|
|
|
|
def received_announce(self, destination_hash, announced_identity, app_data) -> None: # noqa: ANN001
|
|
if self._destination is not None and destination_hash == self._destination.hash:
|
|
return # ignore our own announce
|
|
with self._lock:
|
|
self._peers.add(destination_hash)
|
|
|
|
def _handle_delivery(self, message) -> None: # noqa: ANN001
|
|
import RNS
|
|
|
|
text = message.content_as_string()
|
|
sender = RNS.prettyhexrep(message.source_hash)
|
|
if text:
|
|
self._on_message(self.NETWORK, sender, text)
|
|
|
|
def send(self, text: str) -> None:
|
|
import RNS
|
|
import LXMF
|
|
|
|
if self._router is None or self._destination is None:
|
|
raise RuntimeError("ReticulumAdapter.send() called before connect()")
|
|
|
|
with self._lock:
|
|
peer_hashes = list(self._peers)
|
|
|
|
for peer_hash in peer_hashes:
|
|
identity = RNS.Identity.recall(peer_hash)
|
|
if identity is None:
|
|
continue # no path/identity known yet for this peer
|
|
dest = RNS.Destination(
|
|
identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery"
|
|
)
|
|
lxm = LXMF.LXMessage(
|
|
dest, self._destination, text, desired_method=LXMF.LXMessage.OPPORTUNISTIC
|
|
)
|
|
self._router.handle_outbound(lxm)
|
|
|
|
def close(self) -> None:
|
|
pass # RNS/LXMF manage their own threads; no explicit teardown needed for a v0 prototype
|