feat(reticulum): implement bootstrap-only interface handling

This commit is contained in:
Ivan
2026-04-27 11:14:59 -05:00
parent 6ea4b37cb9
commit 6f186b3986
+452 -85
View File
@@ -124,6 +124,7 @@ from meshchatx.src.backend.nomadnet_utils import (
from meshchatx.src.backend.page_node_manager import PageNodeManager
from meshchatx.src.backend.persistent_log_handler import PersistentLogHandler
from meshchatx.src.backend.recovery import CrashRecovery, HealthMonitor
from meshchatx.src.backend import reticulum_pathfinding
import meshchatx.src.backend.rngit_tool as rngit_tool
from meshchatx.src.backend.rnprobe_handler import RNProbeHandler
from meshchatx.src.backend.sideband_commands import SidebandCommands
@@ -638,6 +639,7 @@ class ReticulumMeshChat:
instance_name = default
discover_interfaces = True
autoconnect_discovered_interfaces = 3
default_bootstrap_only = True
required_discovery_value = 16
# Some platforms don't support domain sockets, and if that
@@ -1946,6 +1948,50 @@ class ReticulumMeshChat:
break
return sanitized
@staticmethod
def _reticulum_yes_no_preference(value, *, default):
if value is None or value == "":
return default
if isinstance(value, bool):
return value
s = str(value).strip().lower()
if s in ("false", "no", "0", "n", "off"):
return False
if s in ("true", "yes", "1", "y", "on"):
return True
return default
@staticmethod
def _bootstrap_only_request_yes_no(value):
if value is None or value == "":
return None
if isinstance(value, bool):
return "yes" if value else "no"
s = str(value).strip().lower()
if s in ("true", "yes", "1", "y", "on"):
return "yes"
if s in ("false", "no", "0", "n", "off"):
return "no"
return None
@staticmethod
def apply_bootstrap_only_to_interface(interface_details, data, default_enabled):
if "bootstrap_only" in data:
yn = ReticulumMeshChat._bootstrap_only_request_yes_no(
data.get("bootstrap_only")
)
if yn == "yes":
interface_details["bootstrap_only"] = "yes"
elif yn == "no":
interface_details["bootstrap_only"] = "no"
else:
interface_details.pop("bootstrap_only", None)
return
if default_enabled:
interface_details["bootstrap_only"] = "yes"
else:
interface_details.pop("bootstrap_only", None)
@staticmethod
def discovery_filter_candidates(interface):
if not isinstance(interface, dict):
@@ -3790,25 +3836,30 @@ class ReticulumMeshChat:
@routes.get("/api/v1/tools/rnode/latest_release")
async def tools_rnode_latest_release(request):
"""Proxy the Gitea releases-latest endpoint for the configured RNode firmware repo.
"""Proxy GitHub's latest-release JSON for RNode firmware (official repo).
The frontend cannot call git.quad4.io directly because the Gitea instance
does not return CORS headers; doing the lookup server-side avoids that
restriction and works identically in browser, Electron and Android WebView.
Browsers cannot reliably call third-party APIs from static pages (CORS); the
MeshChat server fetches api.github.com instead. Default repo is
markqvist/RNode_Firmware; optional ?repo=owner/name must match a strict slug.
"""
repo = request.query.get("repo", "Reticulum/RNode_Firmware")
repo = request.query.get("repo", "markqvist/RNode_Firmware")
if "/" not in repo or any(c in repo for c in (" ", "?", "#", "..", "\\")):
return web.json_response({"error": "Invalid repo"}, status=400)
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repo):
return web.json_response({"error": "Invalid repo"}, status=400)
gitea_url = "https://git.quad4.io"
if self.current_context and self.current_context.config:
gitea_url = self.current_context.config.gitea_base_url.get()
url = f"{gitea_url.rstrip('/')}/api/v1/repos/{repo}/releases/latest"
url = f"https://api.github.com/repos/{repo}/releases/latest"
gh_headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "MeshChatX-RNodeFlasher",
}
try:
timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, allow_redirects=True) as response:
async with session.get(
url, headers=gh_headers, allow_redirects=True
) as response:
if response.status != 200:
return web.json_response(
{
@@ -3837,6 +3888,7 @@ class ReticulumMeshChat:
and not url.startswith("https://git.quad4.io/")
and not url.startswith("https://github.com/")
and not url.startswith("https://objects.githubusercontent.com/")
and not url.startswith("https://release-assets.githubusercontent.com/")
):
return web.json_response({"error": "Invalid download URL"}, status=403)
@@ -3906,6 +3958,51 @@ class ReticulumMeshChat:
interfaces = await self.community_interfaces_manager.get_interfaces()
return web.json_response({"interfaces": interfaces})
@routes.post("/api/v1/community-interfaces/refresh")
async def community_interfaces_refresh(request):
body: dict = {}
try:
data = await request.json()
if isinstance(data, dict):
body = data
except asyncio.CancelledError:
raise
except Exception:
pass
url = body.get("url")
if url is not None and url != "":
if not isinstance(url, str):
return web.json_response(
{"ok": False, "message": "url must be a string"},
status=422,
)
if len(url) > 512:
return web.json_response(
{"ok": False, "message": "url too long"},
status=422,
)
def do_refresh():
return self.community_interfaces_manager.refresh_from_directory(
url=url.strip() if isinstance(url, str) and url.strip() else None,
)
try:
result = await asyncio.to_thread(do_refresh)
except ValueError as e:
return web.json_response({"ok": False, "message": str(e)}, status=400)
except OSError as e:
return web.json_response({"ok": False, "message": str(e)}, status=502)
except json.JSONDecodeError as e:
return web.json_response(
{"ok": False, "message": f"Invalid directory response: {e}"},
status=502,
)
except Exception as e:
return web.json_response({"ok": False, "message": str(e)}, status=500)
return web.json_response({"ok": True, **result})
# enable reticulum interface
@routes.post("/api/v1/reticulum/interfaces/enable")
async def reticulum_interfaces_enable(request):
@@ -4723,6 +4820,20 @@ class ReticulumMeshChat:
):
InterfaceEditor.update_value(interface_details, data, discovery_key)
if interface_type == "TCPClientInterface" or (
interface_type == "BackboneInterface"
and str(interface_details.get("remote") or "").strip() != ""
):
default_boot = ReticulumMeshChat._reticulum_yes_no_preference(
self._get_reticulum_section().get("default_bootstrap_only"),
default=True,
)
ReticulumMeshChat.apply_bootstrap_only_to_interface(
interface_details,
data,
default_boot,
)
# set common interface options
InterfaceEditor.update_value(interface_details, data, "bitrate")
InterfaceEditor.update_value(interface_details, data, "mode")
@@ -6165,6 +6276,10 @@ class ReticulumMeshChat:
"autoconnect_discovered_interfaces": reticulum_config.get(
"autoconnect_discovered_interfaces",
),
"default_bootstrap_only": ReticulumMeshChat._reticulum_yes_no_preference(
reticulum_config.get("default_bootstrap_only"),
default=True,
),
"network_identity": reticulum_config.get("network_identity"),
}
@@ -6208,6 +6323,7 @@ class ReticulumMeshChat:
"interface_discovery_blacklist",
"required_discovery_value",
"autoconnect_discovered_interfaces",
"default_bootstrap_only",
"network_identity",
):
update_config_value(key)
@@ -6235,6 +6351,10 @@ class ReticulumMeshChat:
"autoconnect_discovered_interfaces": reticulum_config.get(
"autoconnect_discovered_interfaces",
),
"default_bootstrap_only": ReticulumMeshChat._reticulum_yes_no_preference(
reticulum_config.get("default_bootstrap_only"),
default=True,
),
"network_identity": reticulum_config.get("network_identity"),
}
@@ -8308,6 +8428,8 @@ class ReticulumMeshChat:
"next_hop": destination_hash_hex,
"next_hop_interface": "Local",
},
"path_stale": False,
"path_unresponsive": False,
},
)
@@ -8319,9 +8441,11 @@ class ReticulumMeshChat:
timeout_seconds = int(request.query.get("timeout", 15))
timeout_after_seconds = time.time() + timeout_seconds
# request path if we don't have it
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
reticulum = self.reticulum if hasattr(self, "reticulum") else None
reticulum_pathfinding.prepare_fresh_path_request(
reticulum,
destination_hash,
)
# wait until we have a path, or give up after the configured timeout
while (
@@ -8332,27 +8456,27 @@ class ReticulumMeshChat:
# ensure path is known
if not RNS.Transport.has_path(destination_hash):
pm = reticulum_pathfinding.path_metadata_for_api(destination_hash)
return web.json_response(
{
"path": None,
**pm,
},
)
# determine next hop and hop count
hops = RNS.Transport.hops_to(destination_hash)
next_hop_bytes = None
next_hop_interface = None
if hasattr(self, "reticulum") and self.reticulum:
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
next_hop_interface = self.reticulum.get_next_hop_if_name(
destination_hash,
)
# ensure next hop provided
if next_hop_bytes is None:
pm = reticulum_pathfinding.path_metadata_for_api(destination_hash)
return web.json_response(
{
"path": None,
**pm,
},
)
@@ -8363,6 +8487,7 @@ class ReticulumMeshChat:
else None
)
pm = reticulum_pathfinding.path_metadata_for_api(destination_hash)
return web.json_response(
{
"path": {
@@ -8370,6 +8495,7 @@ class ReticulumMeshChat:
"next_hop": next_hop,
"next_hop_interface": next_hop_interface,
},
**pm,
},
)
@@ -8405,7 +8531,11 @@ class ReticulumMeshChat:
},
status=400,
)
RNS.Transport.request_path(destination_hash_bytes)
reticulum = self.reticulum if hasattr(self, "reticulum") else None
reticulum_pathfinding.prepare_fresh_path_request(
reticulum,
destination_hash_bytes,
)
return web.json_response(
{
"message": "ok",
@@ -9180,15 +9310,18 @@ class ReticulumMeshChat:
async def translator_languages(request):
try:
libretranslate_url = request.query.get("libretranslate_url")
languages = self.translator_handler.get_supported_languages(
th = self.translator_handler
out = th.get_translator_languages_response(
libretranslate_url=libretranslate_url,
)
return web.json_response(
{
"languages": languages,
"has_argos": self.translator_handler.has_argos,
"has_argos_lib": self.translator_handler.has_argos_lib,
"has_argos_cli": self.translator_handler.has_argos_cli,
"languages": out["languages"],
"has_argos": th.has_argos,
"has_argos_lib": th.has_argos_lib,
"has_argos_cli": th.has_argos_cli,
"libre_client_available": th.has_requests,
"libretranslate_reachable": out["libretranslate_reachable"],
},
)
except ValueError as e:
@@ -11711,9 +11844,20 @@ class ReticulumMeshChat:
]
path = request.path
if path.startswith("/reticulum-docs/") or path.startswith(
"/rnode-flasher/"
):
if path.startswith("/rnode-flasher/"):
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Cross-Origin-Resource-Policy"] = "cross-origin"
if path.startswith("/rnode-flasher/"):
# Standalone RNode Flasher uses Vue in-DOM templates; compileToFunction
# relies on new Function(), which requires unsafe-eval.
script_sources = [
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"'wasm-unsafe-eval'",
"blob:",
]
elif path.startswith("/reticulum-docs/"):
# blob: AudioWorklet addModule(blob:...) and similar dynamic scripts
script_sources = [
"'self'",
@@ -12087,6 +12231,51 @@ class ReticulumMeshChat:
# Sleep for 12 hours
await asyncio.sleep(12 * 3600)
async def local_message_retention_loop(self, session_id, context=None):
from meshchatx.src.backend import local_message_retention as lmr
ctx = context or self.current_context
if not ctx:
return
await asyncio.sleep(lmr.LOCAL_RETENTION_STARTUP_GRACE_SECONDS)
while self.running and ctx.running and ctx.session_id == session_id:
try:
if not ctx.config.local_message_auto_delete_enabled.get():
await asyncio.sleep(300)
continue
now = time.time()
if not interval_action_due(
True,
ctx.config.local_message_auto_delete_last_run_at.get(),
lmr.RETENTION_CHECK_INTERVAL_SECONDS,
now,
):
await asyncio.sleep(60)
continue
v = ctx.config.local_message_auto_delete_value.get() or 30
u = ctx.config.local_message_auto_delete_unit.get() or "days"
if ctx.message_router is not None:
def _cancel(h):
try:
ctx.message_router.cancel_outbound(h)
except Exception:
pass
else:
_cancel = None
lmr.apply_local_message_retention(
ctx.database.messages,
_cancel,
value=int(v),
unit=str(u),
now=now,
)
ctx.config.local_message_auto_delete_last_run_at.set(int(now))
except Exception as e:
print(f"local_message_retention_loop failed: {e}")
await asyncio.sleep(60)
async def telemetry_tracking_loop(self, session_id, context=None):
ctx = context or self.current_context
if not ctx:
@@ -12525,6 +12714,40 @@ class ReticulumMeshChat:
):
self.config.nomad_default_page_path.set(s)
if "local_message_auto_delete_enabled" in data:
self.config.local_message_auto_delete_enabled.set(
self._parse_bool(data["local_message_auto_delete_enabled"]),
)
if (
"local_message_auto_delete_value" in data
or "local_message_auto_delete_unit" in data
):
from meshchatx.src.backend.local_message_retention import (
MAX_VALUE_DAYS,
MAX_VALUE_MONTHS,
normalize_unit,
)
u_str = str(
data.get("local_message_auto_delete_unit")
or self.config.local_message_auto_delete_unit.get()
or "days",
)
u_norm = normalize_unit(u_str)
if "local_message_auto_delete_unit" in data:
self.config.local_message_auto_delete_unit.set(u_norm)
v_raw = data.get(
"local_message_auto_delete_value",
self.config.local_message_auto_delete_value.get(),
)
try:
v = int(v_raw)
except (TypeError, ValueError):
v = 30
v = max(1, v)
v = min(v, MAX_VALUE_MONTHS if u_norm == "months" else MAX_VALUE_DAYS)
self.config.local_message_auto_delete_value.set(v)
if "block_attachments_from_strangers" in data:
self.config.block_attachments_from_strangers.set(
self._parse_bool(data["block_attachments_from_strangers"]),
@@ -12639,6 +12862,16 @@ class ReticulumMeshChat:
value = self._parse_bool(data["blackhole_integration_enabled"])
self.config.blackhole_integration_enabled.set(value)
for _k in (
"announce_store_lxmf_delivery",
"announce_store_lxst_telephony",
"announce_store_nomadnetwork_node",
"announce_store_lxmf_propagation",
"announce_store_git_repositories",
):
if _k in data:
getattr(self.config, _k).set(self._parse_bool(data[_k]))
# update csp extra sources
if "csp_extra_connect_src" in data:
self.config.csp_extra_connect_src.set(data["csp_extra_connect_src"])
@@ -12745,11 +12978,26 @@ class ReticulumMeshChat:
self._parse_bool(data["telephone_web_audio_allow_fallback"]),
)
if "translator_enabled" in data:
value = self._parse_bool(data["translator_enabled"])
self.config.translator_enabled.set(value)
if "translator_argos_enabled" in data:
v = self._parse_bool(data["translator_argos_enabled"])
self.config.translator_argos_enabled.set(v)
if hasattr(self, "translator_handler"):
self.translator_handler.enabled = value
self.translator_handler.translator_argos_enabled = v
if "translator_libretranslate_enabled" in data:
v = self._parse_bool(data["translator_libretranslate_enabled"])
self.config.translator_libretranslate_enabled.set(v)
if hasattr(self, "translator_handler"):
self.translator_handler.translator_libretranslate_enabled = v
if "translator_enabled" in data:
v = self._parse_bool(data["translator_enabled"])
self.config.translator_argos_enabled.set(v)
self.config.translator_libretranslate_enabled.set(v)
if hasattr(self, "translator_handler"):
th = self.translator_handler
th.translator_argos_enabled = v
th.translator_libretranslate_enabled = v
if "libretranslate_url" in data:
value = data["libretranslate_url"]
@@ -13846,11 +14094,17 @@ class ReticulumMeshChat:
"message_inbound_bubble_color": ctx.config.message_inbound_bubble_color.get(),
"message_failed_bubble_color": ctx.config.message_failed_bubble_color.get(),
"message_waiting_bubble_color": ctx.config.message_waiting_bubble_color.get(),
"translator_enabled": ctx.config.translator_enabled.get(),
"translator_argos_enabled": ctx.config.translator_argos_enabled.get(),
"translator_libretranslate_enabled": ctx.config.translator_libretranslate_enabled.get(),
"libretranslate_url": ctx.config.libretranslate_url.get(),
"desktop_open_calls_in_separate_window": ctx.config.desktop_open_calls_in_separate_window.get(),
"desktop_hardware_acceleration_enabled": ctx.config.desktop_hardware_acceleration_enabled.get(),
"blackhole_integration_enabled": ctx.config.blackhole_integration_enabled.get(),
"announce_store_lxmf_delivery": ctx.config.announce_store_lxmf_delivery.get(),
"announce_store_lxst_telephony": ctx.config.announce_store_lxst_telephony.get(),
"announce_store_nomadnetwork_node": ctx.config.announce_store_nomadnetwork_node.get(),
"announce_store_lxmf_propagation": ctx.config.announce_store_lxmf_propagation.get(),
"announce_store_git_repositories": ctx.config.announce_store_git_repositories.get(),
"announce_max_stored_lxmf_delivery": ctx.config.announce_max_stored_lxmf_delivery.get(),
"announce_max_stored_nomadnetwork_node": ctx.config.announce_max_stored_nomadnetwork_node.get(),
"announce_max_stored_lxmf_propagation": ctx.config.announce_max_stored_lxmf_propagation.get(),
@@ -13875,6 +14129,10 @@ class ReticulumMeshChat:
"nomad_render_html_enabled": ctx.config.nomad_render_html_enabled.get(),
"nomad_render_plaintext_enabled": ctx.config.nomad_render_plaintext_enabled.get(),
"nomad_default_page_path": ctx.config.nomad_default_page_path.get(),
"local_message_auto_delete_enabled": ctx.config.local_message_auto_delete_enabled.get(),
"local_message_auto_delete_value": ctx.config.local_message_auto_delete_value.get(),
"local_message_auto_delete_unit": ctx.config.local_message_auto_delete_unit.get()
or "days",
}
# try and get a name for the provided identity hash
@@ -14675,6 +14933,7 @@ class ReticulumMeshChat:
attachments_stripped=attachments_stripped,
context=ctx,
)
self._maybe_store_path_at_send_for_lxmf(ctx, lxmf_message)
# handle forwarding
self.handle_forwarding(lxmf_message, context=ctx)
@@ -14801,6 +15060,9 @@ class ReticulumMeshChat:
include_attachments=False,
reticulum=self.reticulum,
)
self._merge_stored_path_fields_from_db(
ctx, lxmf_message.hash.hex(), msg_dict
)
suppress_notifications = self._lxmf_sieve_suppresses_notifications(
source_hash,
@@ -14937,6 +15199,104 @@ class ReticulumMeshChat:
traceback.print_exc()
def _merge_stored_path_fields_from_db(self, ctx, msg_hash_hex, msg_dict):
try:
row = ctx.database.messages.get_lxmf_message_by_hash(msg_hash_hex)
if not row:
return
def _scalar_row_value(key):
try:
if hasattr(row, "get"):
v = row.get(key)
elif hasattr(row, "keys") and key in row.keys():
v = row[key]
else:
return None
except Exception:
return None
if isinstance(v, (bool, int, float, str)):
return v
return None
hops = _scalar_row_value("path_hops_at_send")
if hops is not None:
msg_dict["path_hops_at_send"] = hops
iface = _scalar_row_value("path_interface_at_send")
if iface is not None:
msg_dict["path_interface_at_send"] = iface
pfm = _scalar_row_value("path_finding_measure")
if pfm is not None:
msg_dict["path_finding_measure"] = pfm
prh = _scalar_row_value("path_row_hash_hex")
if prh is not None:
msg_dict["path_row_hash_hex"] = prh
except Exception:
pass
def _reticulum_path_hops_and_interface_to_identity(self, ctx, identity_hash_bytes):
if not identity_hash_bytes:
return None, None
try:
destination_hash = (
identity_hash_bytes
if isinstance(identity_hash_bytes, (bytes, bytearray))
else bytes.fromhex(str(identity_hash_bytes))
)
except Exception:
return None, None
destination_hash_hex = destination_hash.hex()
local_hashes: set[str] = set()
with contextlib.suppress(Exception):
if ctx and ctx.identity:
local_hashes.add(ctx.identity.hash.hex())
with contextlib.suppress(Exception):
if self.local_lxmf_destination is not None:
local_hashes.add(self.local_lxmf_destination.hash.hex())
with contextlib.suppress(Exception):
if ctx and ctx.message_router:
pdest = ctx.message_router.propagation_destination
if pdest is not None and getattr(pdest, "hash", None):
local_hashes.add(pdest.hash.hex())
if destination_hash_hex in local_hashes:
return 0, "Local"
if not RNS.Transport.has_path(destination_hash):
return None, None
hops = RNS.Transport.hops_to(destination_hash)
next_hop_bytes = None
if hasattr(self, "reticulum") and self.reticulum:
next_hop_bytes = self.reticulum.get_next_hop(destination_hash)
if next_hop_bytes is None:
return None, None
iface = (
self.reticulum.get_next_hop_if_name(destination_hash)
if hasattr(self, "reticulum") and self.reticulum
else None
)
return hops, iface
def _maybe_store_path_at_send_for_lxmf(self, ctx, lxmf_message):
try:
msg_hash = lxmf_message.hash.hex()
row = ctx.database.messages.get_lxmf_message_by_hash(msg_hash)
if not row or row.get("path_hops_at_send") is not None:
return
if getattr(lxmf_message, "incoming", False):
dest_bytes = lxmf_message.source_hash
else:
dest_bytes = lxmf_message.destination_hash
hops, iface = self._reticulum_path_hops_and_interface_to_identity(
ctx, dest_bytes
)
if hops is None:
return
ctx.database.messages.set_lxmf_message_path_at_send_if_unset(
msg_hash, hops, iface
)
except Exception:
pass
def on_lxmf_sending_state_updated(self, lxmf_message, context=None):
ctx = context or self.current_context
if not ctx or not ctx.database:
@@ -14969,17 +15329,21 @@ class ReticulumMeshChat:
quality=quality,
method=convert_lxmf_method_to_string(lxmf_message),
)
self._maybe_store_path_at_send_for_lxmf(ctx, lxmf_message)
msg_dict = convert_lxmf_message_to_dict(
lxmf_message,
include_attachments=False,
reticulum=self.reticulum,
)
self._merge_stored_path_fields_from_db(ctx, lxmf_message.hash.hex(), msg_dict)
AsyncUtils.run_async(
self.websocket_broadcast(
json.dumps(
{
"type": "lxmf_message_state_updated",
"lxmf_message": convert_lxmf_message_to_dict(
lxmf_message,
include_attachments=False,
reticulum=self.reticulum,
),
"lxmf_message": msg_dict,
},
),
),
@@ -15036,6 +15400,8 @@ class ReticulumMeshChat:
is_spam: bool = False,
attachments_stripped: bool = False,
context=None,
path_finding_measure: str | None = None,
path_row_hash_hex: str | None = None,
):
ctx = context or self.current_context
if not ctx:
@@ -15048,6 +15414,10 @@ class ReticulumMeshChat:
)
lxmf_message_dict["is_spam"] = 1 if is_spam else 0
lxmf_message_dict["attachments_stripped"] = 1 if attachments_stripped else 0
if path_finding_measure is not None:
lxmf_message_dict["path_finding_measure"] = path_finding_measure
if path_row_hash_hex is not None:
lxmf_message_dict["path_row_hash_hex"] = path_row_hash_hex
# calculate peer hash
local_hash = ctx.local_lxmf_destination.hexhash
@@ -15059,31 +15429,14 @@ class ReticulumMeshChat:
ctx.database.messages.upsert_lxmf_message(lxmf_message_dict)
def _lxmf_path_wait_seconds(self):
try:
base = float(RNS.Transport.PATH_REQUEST_TIMEOUT)
except Exception:
base = 30.0
return max(30.0, min(base, 120.0))
return reticulum_pathfinding.lxmf_path_wait_cap_seconds()
async def _await_transport_path(self, destination_hash_bytes: bytes) -> bool:
deadline = time.time() + self._lxmf_path_wait_seconds()
if not RNS.Transport.has_path(destination_hash_bytes):
RNS.Transport.request_path(destination_hash_bytes)
while (
not RNS.Transport.has_path(destination_hash_bytes)
and time.time() < deadline
):
await asyncio.sleep(0.1)
if RNS.Transport.has_path(destination_hash_bytes):
return True
RNS.Transport.request_path(destination_hash_bytes)
deadline = time.time() + max(15.0, self._lxmf_path_wait_seconds() * 0.5)
while (
not RNS.Transport.has_path(destination_hash_bytes)
and time.time() < deadline
):
await asyncio.sleep(0.1)
return RNS.Transport.has_path(destination_hash_bytes)
async def _await_transport_path(self, destination_hash_bytes: bytes):
r = self.reticulum if hasattr(self, "reticulum") else None
return await reticulum_pathfinding.await_transport_path_for_outbound_lxmf(
r,
destination_hash_bytes,
)
# upserts the provided announce to the database
# handle sending an lxmf message to reticulum
@@ -15133,7 +15486,7 @@ class ReticulumMeshChat:
# Reticulum keeps a live path table; entries expire when peers move or links drop.
# We cannot replay "old" paths from the app layer — Transport.request_path refreshes discovery.
await self._await_transport_path(destination_hash_bytes)
path_outcome = await self._await_transport_path(destination_hash_bytes)
# find destination identity from hash
destination_identity = RNS.Identity.recall(destination_hash_bytes)
@@ -15309,7 +15662,16 @@ class ReticulumMeshChat:
# upsert lxmf message to database
if not no_display:
self.db_upsert_lxmf_message(lxmf_message, context=ctx)
self.db_upsert_lxmf_message(
lxmf_message,
context=ctx,
path_finding_measure=reticulum_pathfinding.format_outbound_path_finding_measure(
path_outcome,
),
path_row_hash_hex=destination_hash.lower()
if path_outcome.path_available
else None,
)
# tell all websocket clients that old failed message was deleted so it can remove from ui
if not no_display:
@@ -15495,16 +15857,22 @@ class ReticulumMeshChat:
),
method=convert_lxmf_method_to_string(lxmf_message),
)
self._maybe_store_path_at_send_for_lxmf(ctx, lxmf_message)
msg_dict = convert_lxmf_message_to_dict(
lxmf_message,
include_attachments=False,
reticulum=self.reticulum,
)
self._merge_stored_path_fields_from_db(
ctx, lxmf_message.hash.hex(), msg_dict
)
await self.websocket_broadcast(
json.dumps(
{
"type": "lxmf_message_state_updated",
"lxmf_message": convert_lxmf_message_to_dict(
lxmf_message,
include_attachments=False,
reticulum=self.reticulum,
),
"lxmf_message": msg_dict,
},
),
)
@@ -15545,6 +15913,9 @@ class ReticulumMeshChat:
self.reticulum.drop_path(destination_hash)
return
if not ctx.announce_manager.is_storing_announce_for_aspect(aspect):
return
# log received announce
print(
"Received an announce from "
@@ -15611,6 +15982,9 @@ class ReticulumMeshChat:
self.reticulum.drop_path(destination_hash)
return
if not ctx.announce_manager.is_storing_announce_for_aspect(aspect):
return
# log received announce
print(
"Received an announce from "
@@ -15676,6 +16050,9 @@ class ReticulumMeshChat:
if not ctx or not ctx.running or not ctx.announce_manager or not ctx.database:
return
if not ctx.announce_manager.is_storing_announce_for_aspect(aspect):
return
# log received announce
print(
"Received an announce from "
@@ -15901,6 +16278,9 @@ class ReticulumMeshChat:
self.reticulum.drop_path(destination_hash)
return
if not ctx.announce_manager.is_storing_announce_for_aspect(aspect):
return
print(
"Received an announce from "
+ RNS.prettyhexrep(destination_hash)
@@ -15955,6 +16335,9 @@ class ReticulumMeshChat:
self.reticulum.drop_path(destination_hash)
return
if not ctx.announce_manager.is_storing_announce_for_aspect(aspect):
return
# log received announce
print(
"Received an announce from "
@@ -15992,23 +16375,6 @@ class ReticulumMeshChat:
),
)
# find announce from database
announce = self.database.announces.get_announce_by_hash(destination_hash.hex())
if announce is None:
return
# send database announce to all websocket clients
AsyncUtils.run_async(
self.websocket_broadcast(
json.dumps(
{
"type": "announce",
"announce": self.convert_db_announce_to_dict(announce),
},
),
),
)
self.queue_crawler_task(
destination_hash.hex(),
self.config.nomad_default_page_path.get() or "/page/index.mu",
@@ -16075,6 +16441,7 @@ class ReticulumMeshChat:
aspect,
app_data,
None,
force_store=True,
)
announce = ctx.database.announces.get_announce_by_hash(destination_hash.hex())
if announce: