import os import sys import time import threading import argparse import RNS from http.server import HTTPServer from db import init_db, get_setting, set_setting from handlers import dispatch_request from gateway import GatewayState, GatewayHandler, GATEWAY_PORT APP_NAME = "tinyweb" ASPECTS = ["server"] IDENTITY_FILE = "tinyweb_identity" DEFAULT_TRANSPORT_HOST = "reticulum.derickphan.com" DEFAULT_TRANSPORT_PORT = 4242 DATA_DIR = os.path.expanduser("~/.tinyweb") def get_transport_config(): host = get_setting("transport_host", DEFAULT_TRANSPORT_HOST) port = get_setting("transport_port", str(DEFAULT_TRANSPORT_PORT)) return host, int(port) def find_available_port(start=8080, max_attempts=20): """Find an available port starting from start.""" import socket for port in range(start, start + max_attempts): try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("0.0.0.0", port)) return port except OSError: continue return start def get_version(): """Get version from git tag or VERSION file.""" try: import subprocess tag = subprocess.check_output( ["git", "describe", "--tags", "--abbrev=0"], stderr=subprocess.DEVNULL, text=True ).strip() if tag.startswith("v"): return tag[1:] return tag except Exception: version_file = os.path.join(os.path.dirname(__file__), "VERSION") if os.path.exists(version_file): with open(version_file) as f: return f.read().strip() return "0.0.0" def load_or_create_identity(): os.makedirs(DATA_DIR, exist_ok=True) identity_path = os.path.join(DATA_DIR, IDENTITY_FILE) if os.path.isfile(identity_path): current = os.stat(identity_path).st_mode & 0o777 if current != 0o600: os.chmod(identity_path, 0o600) return RNS.Identity.from_file(identity_path) identity = RNS.Identity() identity.to_file(identity_path) os.chmod(identity_path, 0o600) return identity def rns_request_handler(path, data, request_id, link_id, remote_identity, requested_at): if data is None: data = {"method": "GET", "path": "/", "query": {}, "body": {}, "gateway_host": ""} return dispatch_request(data) def start_gateway(reticulum): GatewayState.reticulum = reticulum GatewayState.local_dispatch = dispatch_request server = HTTPServer(("0.0.0.0", GATEWAY_PORT), GatewayHandler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() def _transport_settings_match(config_file, desired_host, desired_port): """Check if existing config transport settings match desired values.""" import configparser try: config = configparser.ConfigParser() config.read(config_file) if config.has_section("TCP Transport"): existing_host = config.get("TCP Transport", "target_host") existing_port = config.get("TCP Transport", "target_port") return existing_host == desired_host and existing_port == str(desired_port) except Exception: pass return False def ensure_rns_config(config_dir, transport_host=None, transport_port=None): """Generate a default Reticulum config with internet transport if none exists.""" if config_dir is None: config_dir = os.path.expanduser("~/.reticulum") config_file = os.path.join(config_dir, "config") if transport_host is None: transport_host = get_setting("transport_host", DEFAULT_TRANSPORT_HOST) if transport_port is None: transport_port = int(get_setting("transport_port", str(DEFAULT_TRANSPORT_PORT))) if os.path.exists(config_file): if _transport_settings_match(config_file, transport_host, transport_port): return os.makedirs(config_dir, exist_ok=True) with open(config_file, "w") as f: f.write(f"""[reticulum] enable_transport = False share_instance = No [logging] loglevel = 4 [interfaces] [[Default Interface]] type = AutoInterface enabled = Yes [[TCP Transport]] type = TCPClientInterface enabled = yes target_host = {transport_host} target_port = {transport_port} """) print(f"Created Reticulum config at {config_file}") def _preload_embeddings(): """Pre-load the embedding model and build the HNSW index in background.""" if get_setting("semantic_search", "1") != "1": print("Semantic search disabled.") return try: from embeddings import _get_session, _get_reranker, build_index _get_session() build_index() if get_setting("use_reranker", "1") == "1": _get_reranker() print("Semantic search ready (with reranker).") else: print("Semantic search ready.") except Exception as e: print(f"Semantic search unavailable: {e}") def main(): parser = argparse.ArgumentParser(prog="tinyweb", description="Personal decentralized search engine") parser.add_argument("--version", "-v", action="store_true", help="Show version") parser.add_argument("--port", "-p", type=int, default=None, help="HTTP gateway port (default: 8080)") args = parser.parse_args() if args.version: print(f"TinyWeb {get_version()}") return port = args.port or 8080 import gateway gateway.GATEWAY_PORT = find_available_port(port) init_db() transport_host = get_setting("transport_host", DEFAULT_TRANSPORT_HOST) transport_port = int(get_setting("transport_port", str(DEFAULT_TRANSPORT_PORT))) threading.Thread(target=_preload_embeddings, daemon=True).start() config_dir = os.environ.get("RNS_CONFIG_DIR") ensure_rns_config(config_dir, transport_host, transport_port) reticulum = RNS.Reticulum(configdir=config_dir) identity = load_or_create_identity() destination = RNS.Destination( identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, *ASPECTS, ) destination.register_request_handler( "/tinyweb", response_generator=rns_request_handler, allow=RNS.Destination.ALLOW_ALL, ) # Brief delay to ensure all interfaces (especially TCP) are fully ready time.sleep(2) destination.announce() set_setting("dest_hash", destination.hash.hex()) start_gateway(reticulum) print(f"TinyWeb running!") print(f"Open http://localhost:{GATEWAY_PORT} in your browser") print(f"Destination hash: {RNS.prettyhexrep(destination.hash)} (share this so friends can subscribe)") while True: time.sleep(1) if __name__ == "__main__": main()