diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py | 211 |
1 files changed, 12 insertions, 199 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index b3618b5..b2374d3 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -72,12 +72,9 @@ from meshbay_common.adminop import ( OP_TRANSFER_LIMITS, OP_TMDB_CONFIG, OP_TMDB_ENABLED, - OP_VIDEO_ROOT, OP_TMDB_OVERRIDE, OP_TMDB_REMATCH, OP_MUSICBRAINZ_ENABLED, - OP_AUDIO_ROOT, - OP_PHOTO_ROOTS, OP_APP_DIRECTORIES, OP_CHAT_DIRECTORY, OP_CHAT_EPOCH, @@ -577,12 +574,6 @@ class WebRTCPeerSession: self._do_tmdb_config(msg) elif mtype == MNP.TMDB_ENABLED: self._do_tmdb_enabled(msg) - elif mtype == MNP.VIDEO_ROOT: - self._do_video_root(msg) - elif mtype == MNP.AUDIO_ROOT: - self._do_audio_root(msg) - elif mtype == MNP.PHOTO_ROOTS: - self._do_photo_roots(msg) elif mtype == MNP.APP_DIRECTORIES: self._do_app_directories(msg) elif mtype == MNP.CHAT_DIRECTORY: @@ -870,7 +861,7 @@ class WebRTCPeerSession: # below is sealed under a GEK-derived subkey, which gives it an # authentication tag from a key the hub does not hold. Until MNP 1.0 the # signed transcript named no ack field at all, so is_node_admin, - # enabled_apps, video_root and the rest were authenticated by the DTLS + # enabled_apps, the app directories and the rest were authenticated by DTLS # channel and nothing else. config = { "is_node_admin": self._is_node_admin(), @@ -879,14 +870,10 @@ class WebRTCPeerSession: # setting (or one whose context has not loaded it yet) hides # nothing. "enabled_apps": list(self._group_ctx().get("enabled_apps") or []), - # Which folder the Videos app treats as its entry point for - # this group — "" means the whole group index. - "video_root": self._group_ctx().get("video_root") or "", - # Per-group (2026-08-24 — used to be node-wide), same "read once, - # kept current in place by the signed op" shape as video_root - # above — surfaced here rather than only via tmdb_enabled_ack so - # a client that connects after the operator already configured - # it does not have to wait for a live change to find out. + # Read once and kept current in place by the signed op, and + # surfaced here rather than only via tmdb_enabled_ack, so a client + # that connects after the operator configured it does not have to + # wait for a live change to find out. "tmdb_enabled": bool(self._group_ctx().get("tmdb_enabled", True)), # Token/language stay node-wide (one shared credential/cache) — # via daemon_state, kept current by tmdb_config_ack. @@ -898,15 +885,6 @@ class WebRTCPeerSession: # fields above. No language field: MusicBrainz search doesn't # take one the way TMDB does. "musicbrainz_enabled": bool(self._group_ctx().get("musicbrainz_enabled", True)), - # Which folder the Music app treats as its entry point for this - # group — same shape as video_root above, "" means unset (the - # Music tab shows nothing yet). - "audio_root": self._group_ctx().get("audio_root") or "", - # Which folder(s) the Photos app treats as its entry points for - # this group — a *list*, unlike video_root/audio_root above - # (docs/photos.md §2.1). Empty means unset (the Photos tab shows - # nothing yet). - "photo_roots": list(self._group_ctx().get("photo_roots") or []), # The same three answers in one shape, plus every other app's — # `<app>_directories`, keyed by the app's registry name, always a # list. The scalars above are derived from these (daemon.py's @@ -2203,7 +2181,7 @@ class WebRTCPeerSession: """ Whether TMDB lookups run for this group at all. Per-group, unlike tmdb_config's token/language — see ops.set_tmdb_enabled. Signed like - video_root: it decides whether this group's members' Videos tab ever + app_directories: it decides whether this group's members' Videos tab ever makes outbound TMDB traffic. """ enabled = msg.get("enabled") @@ -2237,172 +2215,16 @@ class WebRTCPeerSession: except Exception: pass - def _do_video_root(self, msg: dict) -> None: - """ - Which folder (possibly a subfolder of a shared root) the Videos app - treats as its entry point for this group. Signed like apps_enabled: - it decides what every member's Videos tab shows. - - An empty path is always accepted (it means "the whole group index", - today's behaviour). A non-empty path must resolve to a real, - currently-readable directory — validated against the group's own - roots the same way directory creation/deletion already is, so a - stale or mistyped path is refused before a signature is even asked - for. - """ - path = msg.get("path") - if not isinstance(path, str): - self._send({"type": "error", "detail": "Missing or invalid 'path'"}) - return - path = path.strip("/") - if path: - ctx = self._group_ctx() - resolved = ctx["roots"].resolve(path) if ctx.get("roots") else None - if not resolved or not resolved.is_dir(): - self._send({"type": "error", "detail": "Not a directory in this group"}) - return - if not self._has_admin_authority(): - self._send({"type": "error", "detail": "No authorized key for this"}) - return - self._issue_admin_challenge(OP_VIDEO_ROOT, path) - - async def _admin_exec_video_root( - self, pending: dict, transcript: bytes, sig: bytes, - ) -> None: - path = pending["subject"] - if not await self._verify_admin_sig(transcript, sig): - self._send({"type": "error", "detail": "Signature verification failed"}) - self._audit("admin_auth_failed", f"video_root:{path}") - return - try: - await self._run_op(ops.set_video_root, self._group_id or "", path) - except ops.OpError as e: - self._send({"type": "error", "detail": e.message}) - return - self._audit("video_root", path) - - notice = {"type": MNP.VIDEO_ROOT_ACK, "v": MNP_VERSION, "path": path} - for uid, session in list(self._peer_registry().items()): - try: - session._send(notice) - except Exception: - pass - - def _do_audio_root(self, msg: dict) -> None: - """Same shape as _do_video_root above — the Music app's own entry point.""" - path = msg.get("path") - log.debug("audio_root request user=%s path=%r", - (self._user_id or "?")[:8], path) - if not isinstance(path, str): - self._send({"type": "error", "detail": "Missing or invalid 'path'"}) - return - path = path.strip("/") - if path: - ctx = self._group_ctx() - resolved = ctx["roots"].resolve(path) if ctx.get("roots") else None - if not resolved or not resolved.is_dir(): - self._send({"type": "error", "detail": "Not a directory in this group"}) - return - if not self._has_admin_authority(): - self._send({"type": "error", "detail": "No authorized key for this"}) - return - self._issue_admin_challenge(OP_AUDIO_ROOT, path) - - async def _admin_exec_audio_root( - self, pending: dict, transcript: bytes, sig: bytes, - ) -> None: - path = pending["subject"] - if not await self._verify_admin_sig(transcript, sig): - self._send({"type": "error", "detail": "Signature verification failed"}) - self._audit("admin_auth_failed", f"audio_root:{path}") - return - try: - await self._run_op(ops.set_audio_root, self._group_id or "", path) - except ops.OpError as e: - self._send({"type": "error", "detail": e.message}) - return - self._audit("audio_root", path) - - notice = {"type": MNP.AUDIO_ROOT_ACK, "v": MNP_VERSION, "path": path} - for uid, session in list(self._peer_registry().items()): - try: - session._send(notice) - except Exception: - pass - - def _do_photo_roots(self, msg: dict) -> None: - """ - Which folder(s) the Photos app treats as its entry points for this - group (docs/photos.md §2.1) — a *set*, replaced whole in one signed - op, same shape as apps_enabled rather than one op per root the way - video_root/audio_root are single values. - - An empty list is always accepted (nothing configured yet, today's - "Photos shows nothing" state). Every non-empty path must resolve to - a real, currently-readable directory, and no root may be nested - inside another in the same submitted set — both checked, and - refused, before a signature is ever asked for, same principle as - video_root's path check and apps_enabled's "empty set refused up - front". - """ - roots = msg.get("roots") - if not isinstance(roots, list) or not all(isinstance(r, str) for r in roots): - self._send({"type": "error", "detail": "Missing or invalid 'roots'"}) - return - roots = sorted({r.strip("/") for r in roots if r.strip("/")}) - ctx = self._group_ctx() - for path in roots: - resolved = ctx["roots"].resolve(path) if ctx.get("roots") else None - if not resolved or not resolved.is_dir(): - self._send({"type": "error", - "detail": f"Not a directory in this group: {path}"}) - return - # Case-insensitive nesting check (§6.8) — a root may not be a folder - # itself sitting inside another root in the same set. - folded = [r.casefold() for r in roots] - for i, a in enumerate(folded): - for j, b in enumerate(folded): - if i != j and (a == b or a.startswith(b + "/")): - self._send({"type": "error", - "detail": f"Root nested inside another: {roots[i]}"}) - return - if not self._has_admin_authority(): - self._send({"type": "error", "detail": "No authorized key for this"}) - return - self._issue_admin_challenge(OP_PHOTO_ROOTS, ",".join(roots)) - - async def _admin_exec_photo_roots( - self, pending: dict, transcript: bytes, sig: bytes, - ) -> None: - roots = pending["subject"].split(",") if pending["subject"] else [] - if not await self._verify_admin_sig(transcript, sig): - self._send({"type": "error", "detail": "Signature verification failed"}) - self._audit("admin_auth_failed", f"photo_roots:{pending['subject']}") - return - try: - await self._run_op(ops.set_photo_roots, self._group_id or "", roots) - except ops.OpError as e: - self._send({"type": "error", "detail": e.message}) - return - self._audit("photo_roots", pending["subject"]) - - notice = {"type": MNP.PHOTO_ROOTS_ACK, "v": MNP_VERSION, "roots": roots} - for uid, session in list(self._peer_registry().items()): - try: - session._send(notice) - except Exception: - pass - # ── App directories (generic) ──────────────────────────────────────── def _do_app_directories(self, msg: dict) -> None: """ Which folder(s) an application works over, for any application. - One handler where there were three near-identical ones (`video_root`, - `audio_root`, `photo_roots`) differing only in the key they wrote and - whether they carried a string or a list. Those three still exist for - clients that speak them; nothing new is added beside them. + One handler for every application, keyed by the app's own name: adding + an application adds no message type, and there is no per-app handler + differing only in the key it writes and whether it carries a string or + a list. `app` must be one this node knows (`ALLOWED_APPS`) — a client-supplied key is otherwise a way to write arbitrary rows into `group_settings`. @@ -2651,7 +2473,7 @@ class WebRTCPeerSession: """ Whether MusicBrainz lookups run for this group at all. Per-group from the start (docs/musicbay.md §3.2/§6) — signed like - tmdb_enabled/video_root: it decides whether this group's members' + tmdb_enabled: it decides whether this group's members' Music tab ever makes outbound MusicBrainz traffic. """ enabled = msg.get("enabled") @@ -4136,7 +3958,7 @@ class WebRTCPeerSession: def _do_tmdb_override(self, msg: dict) -> None: """ An operator correcting a wrong automatic TMDB match. Signed like - video_root/tmdb_config: it replaces what every member sees for a + app_directories/tmdb_config: it replaces what every member sees for a show/movie, node-wide (media_cache is shared, not per-viewer). For a **show**, applied to every entry sharing the representative @@ -5317,9 +5139,6 @@ class WebRTCPeerSession: elif pending["op"] == OP_TMDB_ENABLED: self._spawn( self._admin_exec_tmdb_enabled(pending, transcript, sig_bytes)) - elif pending["op"] == OP_VIDEO_ROOT: - self._spawn( - self._admin_exec_video_root(pending, transcript, sig_bytes)) elif pending["op"] == OP_TMDB_OVERRIDE: self._spawn( self._admin_exec_tmdb_override(pending, transcript, sig_bytes)) @@ -5329,12 +5148,6 @@ class WebRTCPeerSession: elif pending["op"] == OP_MUSICBRAINZ_ENABLED: self._spawn( self._admin_exec_musicbrainz_enabled(pending, transcript, sig_bytes)) - elif pending["op"] == OP_AUDIO_ROOT: - self._spawn( - self._admin_exec_audio_root(pending, transcript, sig_bytes)) - elif pending["op"] == OP_PHOTO_ROOTS: - self._spawn( - self._admin_exec_photo_roots(pending, transcript, sig_bytes)) elif pending["op"] == OP_ROOT_ADD: self._spawn( self._admin_exec_root_add(pending, transcript, sig_bytes)) |