import json import time import RNS APP_NAME = "tinyweb" ASPECTS = ["server"] # Two-tier timeout profiles: fast first, then slow for LoRa/multi-hop links _TIMEOUT_TIERS = [ {"path": 15, "link": 15, "request": 30, "poll": 0.25}, {"path": 60, "link": 60, "request": 120, "poll": 1.0}, ] def fetch_remote_sites(dest_hash_hex, since=""): """ Connect to a remote TinyWeb instance over Reticulum and fetch its shared sites. Returns the response dict from /api/sites, or raises an exception on failure. Pass `since` as ISO timestamp for delta sync. Uses progressive timeouts: tries fast first, then retries with longer timeouts for slow links (LoRa, multi-hop). """ last_error = None for tier in _TIMEOUT_TIERS: try: return _fetch(dest_hash_hex, since, tier) except PermissionError: raise # Don't retry permission errors except Exception as e: last_error = e continue raise ConnectionError( f"Could not reach {dest_hash_hex} after {len(_TIMEOUT_TIERS)} attempts: {last_error}" ) def _fetch(dest_hash_hex, since, timeouts): """Single fetch attempt with the given timeout profile.""" dest_hash = bytes.fromhex(dest_hash_hex) poll = timeouts["poll"] # Resolve path if needed if not RNS.Transport.has_path(dest_hash): RNS.Transport.request_path(dest_hash) elapsed = 0 while not RNS.Transport.has_path(dest_hash) and elapsed < timeouts["path"]: time.sleep(poll) elapsed += poll if not RNS.Transport.has_path(dest_hash): raise ConnectionError( f"Could not find path to {dest_hash_hex} ({timeouts['path']}s timeout)" ) server_identity = RNS.Identity.recall(dest_hash) if server_identity is None: raise ConnectionError(f"Could not recall identity for {dest_hash_hex}") destination = RNS.Destination( server_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, *ASPECTS, ) # Establish link link = RNS.Link(destination) elapsed = 0 while link.status == RNS.Link.PENDING and elapsed < timeouts["link"]: time.sleep(poll) elapsed += poll if link.status != RNS.Link.ACTIVE: raise ConnectionError( f"Could not establish link to {dest_hash_hex} ({timeouts['link']}s timeout)" ) try: query = {"since": [since]} if since else {} request_data = { "method": "GET", "path": "/api/sites", "query": query, "body": {}, "gateway_host": "", } req_timeout = timeouts["request"] receipt = link.request("/tinyweb", data=request_data, timeout=req_timeout) elapsed = 0 done = (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED, RNS.RequestReceipt.FAILED) while receipt.get_status() not in done and elapsed < req_timeout: time.sleep(poll) elapsed += poll if receipt.get_status() in (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED): resp = receipt.get_response() if resp["status"] == 403: raise PermissionError("That instance has sharing disabled.") if resp["status"] != 200: raise ConnectionError(f"Remote returned status {resp['status']}") return json.loads(resp["body"]) else: raise ConnectionError( f"Request failed or timed out ({req_timeout}s timeout)" ) finally: link.teardown()