Skip to content
This repository was archived by the owner on May 3, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions reticulum_openapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,24 @@ def __init__(
timeout: float = 10.0,
shared_instance_rpc_key: Optional[str] = None,
):
self.reticulum = RNS.Reticulum(config_path)
config_directory = self._normalise_config_directory(config_path)
if config_directory is not None:
try:
Path(config_directory).expanduser().mkdir(parents=True, exist_ok=True)
except OSError:
pass

self.reticulum = RNS.Reticulum(config_directory)
self._shared_instance_rpc_key: Optional[bytes] = None
if shared_instance_rpc_key is not None:
key_bytes = self._decode_shared_instance_rpc_key(shared_instance_rpc_key)
self.reticulum.rpc_key = key_bytes
self._shared_instance_rpc_key = key_bytes
storage_path = storage_path or (RNS.Reticulum.storagepath + "/lxmf_client")
self.router = LXMF.LXMRouter(storagepath=storage_path)
resolved_storage_path = self._normalise_storage_path(storage_path)
self.router = LXMF.LXMRouter(storagepath=resolved_storage_path)
self.router.register_delivery_callback(self._callback)
if identity is None:
identity = load_or_create_identity(config_path)
identity = load_or_create_identity(config_directory or config_path)
self.identity = identity
self.source_identity = self.router.register_delivery_identity(
identity, display_name=display_name, stamp_cost=0
Expand All @@ -94,6 +101,36 @@ def __init__(
self._link_events: Dict[bytes, asyncio.Event] = {}
self._links: Dict[bytes, RNS.Link] = {}

@staticmethod
def _normalise_config_directory(
config_path: Optional[str],
) -> Optional[str]:
"""Return a configuration directory compatible with Reticulum."""

if not config_path:
return None

candidate = Path(config_path).expanduser()
if candidate.is_file():
return str(candidate.parent)
if candidate.name == "config":
return str(candidate.parent)
return str(candidate)

@staticmethod
def _normalise_storage_path(storage_path: Optional[str]) -> str:
"""Return a filesystem path for LXMF client storage."""

if storage_path:
resolved = Path(storage_path).expanduser()
else:
resolved = Path(RNS.Reticulum.storagepath).expanduser() / "lxmf_client"
try:
resolved.mkdir(parents=True, exist_ok=True)
except OSError:
pass
return str(resolved)

def _get_link_lock(self, dest_hash: bytes) -> asyncio.Lock:
"""Return a lock guarding link creation for ``dest_hash``."""

Expand Down
51 changes: 51 additions & 0 deletions tests/test_client_extra.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import contextlib
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock

Expand Down Expand Up @@ -70,6 +71,56 @@ def fake_register(handler):
assert register_calls["handler"].aspect_filter == "lxmf"


def test_client_normalises_config_directory(tmp_path, monkeypatch):
called = {}

storage_root = tmp_path / "storage_root"

class DummyReticulum:
storagepath = str(storage_root)

def __init__(self, config_path=None):
called["config_path"] = config_path

class DummyIdentity:
def __init__(self):
self.hash = b"h"

class DummyRouter:
def __init__(self, storagepath=None):
self.storagepath = storagepath

def register_delivery_callback(self, cb):
self.cb = cb

def register_delivery_identity(self, ident, display_name=None, stamp_cost=0):
return ident

def handle_outbound(self, msg):
pass

monkeypatch.setattr(client_module.RNS, "Reticulum", DummyReticulum)
monkeypatch.setattr(client_module.RNS, "Identity", DummyIdentity)
monkeypatch.setattr(client_module.LXMF, "LXMRouter", DummyRouter)
monkeypatch.setattr(client_module.LXMF, "LXMessage", object)
monkeypatch.setattr(
client_module,
"load_or_create_identity",
lambda *a, **k: DummyIdentity(),
)

config_dir = tmp_path / "config"
config_dir.mkdir()
config_file = config_dir / "config"
config_file.write_text("", encoding="utf-8")

client_module.LXMFClient(config_path=str(config_file))

assert called["config_path"] == str(config_dir)
expected_storage = Path(storage_root) / "lxmf_client"
assert expected_storage.is_dir()


@pytest.mark.asyncio
async def test_send_command_bytes_payload(monkeypatch):
loop = asyncio.get_running_loop()
Expand Down
Loading