""" MeshBay Node configuration. Config file: ~/.config/meshbay/node.toml All values have sensible defaults and can be overridden by env vars prefixed with MESHBAY_ (e.g. MESHBAY_HUB_URL). """ import os from dataclasses import dataclass, field from pathlib import Path try: import tomllib # Python 3.11+ except ImportError: import tomli as tomllib # type: ignore[no-redef] DEFAULT_CONFIG_PATH = Path.home() / ".config" / "meshbay" / "node.toml" EXAMPLE_CONFIG = """\ # MeshBay Node configuration — multi-group example # See: https://meshbay.org/docs/node-config [hub] url = "https://meshbay.org" username = "myusername" [node] port = 19000 # TCP+TLS (MNP v1) quic_port = 19010 # QUIC (MNP v2) http_port = 19001 # HTTP file API (public content) ui_port = 18000 # local web UI # Multiple groups — each with its own directory and ports [[groups]] id = "" # set after joining name = "My Media" shared_dir = "/home/user/Media" port = 19000 quic_port = 19010 http_port = 19001 [[groups]] id = "" name = "Public Archive" shared_dir = "/home/user/Archive" port = 19002 quic_port = 19012 http_port = 19003 visibility = "public" [keystore] # unlock_file = "~/.config/meshbay/unlock.key" # or set MESHBAY_UNLOCK_KEY env var """ @dataclass class HubConfig: url: str = "https://meshbay.org" username: str = "" password: str = "" # loaded from keystore or env; never written to TOML @dataclass class NodeConfig: port: int = 19000 quic_port: int = 19010 http_port: int = 19001 ui_port: int = 18000 @dataclass class GroupConfig: id: str = "" name: str = "" shared_dir: str = "" visibility: str = "private" # public|private port: int = 19000 # TCP+TLS MNP port for this group quic_port: int = 19010 # QUIC MNP port http_port: int = 19001 # HTTP file API port @dataclass class KeystoreConfig: path: Path = field(default_factory=lambda: DEFAULT_CONFIG_PATH.parent / "keystore.enc") unlock_file: Path | None = None @dataclass class Config: hub: HubConfig = field(default_factory=HubConfig) node: NodeConfig = field(default_factory=NodeConfig) groups: list[GroupConfig] = field(default_factory=list) keystore: KeystoreConfig = field(default_factory=KeystoreConfig) data_dir: Path = field(default_factory=lambda: Path.home() / ".local" / "share" / "meshbay") # Back-compat: single-group access @property def group(self) -> GroupConfig: return self.groups[0] if self.groups else GroupConfig() def load_config(path: Path = DEFAULT_CONFIG_PATH) -> Config: """ Load config from TOML file. Supports both single [group] and multiple [[groups]] sections. Env vars override file values. """ cfg = Config() if path.exists(): raw = tomllib.loads(path.read_text()) hub = raw.get("hub", {}) cfg.hub.url = hub.get("url", cfg.hub.url) cfg.hub.username = hub.get("username", cfg.hub.username) nd = raw.get("node", {}) cfg.node.port = nd.get("port", cfg.node.port) cfg.node.quic_port = nd.get("quic_port", cfg.node.quic_port) cfg.node.http_port = nd.get("http_port", cfg.node.http_port) cfg.node.ui_port = nd.get("ui_port", cfg.node.ui_port) # Multi-group: [[groups]] array if "groups" in raw: for g in raw["groups"]: cfg.groups.append(GroupConfig( id=g.get("id", ""), name=g.get("name", ""), shared_dir=g.get("shared_dir", ""), visibility=g.get("visibility", "private"), port=g.get("port", cfg.node.port), quic_port=g.get("quic_port", cfg.node.quic_port), http_port=g.get("http_port", cfg.node.http_port), )) # Back-compat: single [group] section elif "group" in raw: grp = raw["group"] cfg.groups.append(GroupConfig( id=grp.get("id", ""), name=grp.get("name", ""), )) if "data_dir" in raw: cfg.data_dir = Path(raw["data_dir"]).expanduser().resolve() ks = raw.get("keystore", {}) if "path" in ks: cfg.keystore.path = Path(ks["path"]).expanduser() if "unlock_file" in ks: cfg.keystore.unlock_file = Path(ks["unlock_file"]).expanduser() # Env var overrides if url := os.environ.get("MESHBAY_HUB_URL"): cfg.hub.url = url if user := os.environ.get("MESHBAY_USERNAME"): cfg.hub.username = user if pwd := os.environ.get("MESHBAY_PASSWORD"): cfg.hub.password = pwd if port := os.environ.get("MESHBAY_PORT"): cfg.node.port = int(port) return cfg def write_example_config(path: Path = DEFAULT_CONFIG_PATH) -> None: """Write an example config file if none exists.""" if not path.exists(): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(EXAMPLE_CONFIG)