aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
diff options
context:
space:
mode:
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.py211
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))