summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/ops.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-29 18:55:42 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-29 18:55:42 +0200
commita2215bb071c4e1ce11c616d26df8fd4e7a8fdf57 (patch)
treede2969394d8673194d219b22c4af0dd7faef2d10 /packages/meshbay-node/src/meshbay_node/ops.py
parentc37fe3047367f401765351e5aca73f8f0ed6810c (diff)
parent1f8a52484412b48205e5ff6aac506428e2fb77ed (diff)
downloadmeshbay-a2215bb071c4e1ce11c616d26df8fd4e7a8fdf57.tar.gz
Merge remote-tracking branch 'origin/main'
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/ops.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py115
1 files changed, 114 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index c921b05..d1314f0 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -332,7 +332,19 @@ async def list_groups(state: dict) -> dict:
members = await roster.list_members()
has_operator = any(m["role"] == "operator" and m["status"] == "active"
for m in members)
- return {"groups": out, "operator_paired": has_operator}
+ nd = config.node if config else None
+ defaults = {
+ "invite_ttl_hours": nd.invite_ttl_hours if nd else 168,
+ "pair_ttl_hours": nd.pair_ttl_hours if nd else 24,
+ "device_request_ttl_minutes": nd.device_request_ttl_minutes if nd else 60,
+ "max_concurrent_streams": nd.max_concurrent_streams if nd else 8,
+ "transcode_incompatible_video": nd.transcode_incompatible_video if nd else True,
+ }
+ if roster:
+ settings = await roster.node_settings(defaults)
+ else:
+ settings = defaults
+ return {"groups": out, "operator_paired": has_operator, "settings": settings}
async def attach_group(state: dict, name: str, shared_dir: str,
@@ -472,6 +484,57 @@ def _find_group_range(lines: list[str], group_id: str) -> tuple[int, int] | None
return None
+def _update_node_toml(conf_path: Path, updates: dict) -> None:
+ """Write changed [node] settings back to node.toml without disturbing comments.
+
+ For each key, if the line exists (commented or not) it is replaced in place;
+ otherwise the key is appended to the end of the [node] section.
+ """
+ if not conf_path.exists():
+ return
+ text = conf_path.read_text()
+ lines = text.split("\n")
+
+ node_start = None
+ node_end = len(lines)
+ for i, line in enumerate(lines):
+ stripped = line.strip()
+ if stripped == "[node]":
+ node_start = i
+ elif node_start is not None and re.match(r'^\[', stripped):
+ node_end = i
+ break
+
+ if node_start is None:
+ lines.append("")
+ lines.append("[node]")
+ node_start = len(lines) - 1
+ node_end = len(lines)
+
+ remaining = dict(updates)
+ for i in range(node_start + 1, node_end):
+ for key in list(remaining):
+ pattern = re.compile(
+ r'^(\s*#?\s*)' + re.escape(key) + r'\s*=\s*.*$')
+ if pattern.match(lines[i]):
+ value = remaining.pop(key)
+ if isinstance(value, bool):
+ lines[i] = f"{key} = {'true' if value else 'false'}"
+ else:
+ lines[i] = f"{key} = {value}"
+ break
+
+ for key, value in remaining.items():
+ if isinstance(value, bool):
+ insert_line = f"{key} = {'true' if value else 'false'}"
+ else:
+ insert_line = f"{key} = {value}"
+ lines.insert(node_end, insert_line)
+ node_end += 1
+
+ conf_path.write_text("\n".join(lines))
+
+
def _insert_roots_block(conf_path: Path, group_id: str,
root_block: str) -> None:
"""Append a [[groups.roots]] block inside the matching [[groups]] section."""
@@ -718,6 +781,56 @@ async def set_member_upload(state: dict, group_id: str, allowed: bool) -> dict:
return {"allowed": allowed, "group_id": group_id}
+# ── Node settings ────────────────────────────────────────────────────────────
+
+async def set_node_settings(state: dict, settings: dict) -> dict:
+ """Update node-level daemon settings. Writes to both roster.db and node.toml."""
+ roster = _roster(state)
+ config = _config(state)
+ nd = config.node
+ conf_path = Path(state.get("config_path") or DEFAULT_CONFIG_PATH)
+
+ allowed_keys = {
+ "invite_ttl_hours": ("int", roster.SETTING_INVITE_TTL),
+ "pair_ttl_hours": ("int", roster.SETTING_PAIR_TTL),
+ "device_request_ttl_minutes": ("int", roster.SETTING_DEVICE_TTL),
+ "max_concurrent_streams": ("int", roster.SETTING_MAX_STREAMS),
+ "transcode_incompatible_video": ("bool", roster.SETTING_TRANSCODE),
+ }
+
+ set_by = state.get("node_user_id", "")
+ updated = {}
+ for key, value in settings.items():
+ if key not in allowed_keys:
+ continue
+ kind, setting_key = allowed_keys[key]
+ if kind == "int":
+ try:
+ v = int(value)
+ except (TypeError, ValueError):
+ raise OpError(f"{key} must be an integer")
+ if v < 1:
+ raise OpError(f"{key} must be positive")
+ setattr(nd, key, v)
+ await roster.set_node_setting(setting_key, str(v), set_by)
+ updated[key] = v
+ elif kind == "bool":
+ v = bool(value)
+ setattr(nd, key, v)
+ await roster.set_node_setting(setting_key, "1" if v else "0", set_by)
+ updated[key] = v
+
+ if updated:
+ _update_node_toml(conf_path, updated)
+ if "max_concurrent_streams" in updated:
+ webrtc = state.get("webrtc")
+ if webrtc and hasattr(webrtc, '_stream_sem'):
+ webrtc._stream_sem = asyncio.Semaphore(updated["max_concurrent_streams"])
+
+ log.info("Node settings updated: %s", updated)
+ return {"updated": updated}
+
+
# ── Applications ─────────────────────────────────────────────────────────────
async def set_enabled_apps(state: dict, group_id: str, apps: list[str]) -> dict: