summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py19
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py32
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py12
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py211
4 files changed, 29 insertions, 245 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 371c2f0..5036803 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -1198,15 +1198,10 @@ class NodeDaemon:
async def _app_directories_ctx(self, group_id: str) -> dict:
"""
- Each app's configured directories, plus the legacy scalar names the
- rest of the tree still reads.
+ Each app's configured directories, plus the second name an app is
+ also published under where something reads one (`chat_directory`).
- The scalars are derived here rather than stored, so the two can never
- disagree: `video_root` is the first of `video_directories` and exists
- for MNP 1.0 clients and for the handful of call sites that predate the
- list. A group with several video directories reports the first as its
- `video_root` — which is what an old client can represent, and all it
- could ever have shown.
+ Derived here rather than stored, so the two can never disagree.
"""
dirs = {}
for app in self.APP_DIR_KEYS:
@@ -1406,7 +1401,7 @@ class NodeDaemon:
shared index before they have chosen which folder is actually their
media library would burn both TMDB's rate limit and the node's CPU
on files that were never meant to be in the Videos app at all. Once a
- root is set, `_enrich_video_root_now` (called from ops.set_video_root)
+ root is set, `_enrich_video_root_now` (called when it changes)
separately sweeps whatever it already contains — this path alone only
ever sees entries new since the last broadcast.
"""
@@ -1433,7 +1428,7 @@ class NodeDaemon:
async def _enrich_video_root_now(self, group_id: str) -> None:
"""
Videos app: sweep a group's existing index for enrichment right
- after its video_root is set or changed (ops.set_video_root).
+ after its video root is set or changed.
The ordinary path above only ever looks at entries new since the
last broadcast, so a folder that already had files sitting in it
@@ -1528,7 +1523,7 @@ class NodeDaemon:
async def _enrich_audio_root_now(self, group_id: str) -> None:
"""
Music app: sweep a group's existing index right after its
- audio_root is set or changed (ops.set_audio_root). Mirrors
+ audio root is set or changed. Mirrors
`_enrich_video_root_now` exactly — the ordinary path above only
ever looks at entries new since the last broadcast, so a folder
that already had files in it before it became the audio_root would
@@ -1594,7 +1589,7 @@ class NodeDaemon:
async def _enrich_photo_roots_now(self, group_id: str) -> None:
"""
Photos app: sweep a group's existing index right after its
- photo_roots set changes (ops.set_photo_roots). Mirrors
+ photo roots change. Mirrors
`_enrich_video_root_now`/`_enrich_audio_root_now` — the ordinary
path above only ever looks at entries new since the last broadcast,
so a folder that already had photos in it before it was added to
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index 5b8e22d..4d3422d 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -1641,9 +1641,8 @@ async def set_app_directories(state: dict, group_id: str, app_key: str,
One function for every app, keyed by the app's own name: adding an
application is a registry entry and a settings component, not another
- near-identical op here. It replaces `set_video_root`, `set_audio_root` and
- `set_photo_roots`, which differed only in the key they wrote and whether
- they took a string or a list.
+ near-identical op here — one per app differing only in the key it wrote
+ and whether it took a string or a list.
Empty means nothing configured, which every app reads as "show nothing
until an operator has chosen" — never "the whole group index". Pointing an
@@ -1661,9 +1660,9 @@ async def set_app_directories(state: dict, group_id: str, app_key: str,
await roster.set_app_directories(group_id, app_key, clean,
set_by=state.get("node_user_id", ""))
ctx[f"{app_key}_directories"] = clean
- # The scalar the handshake ack still publishes for MNP 1.0 clients is
- # derived, and has to be re-derived here: leaving it behind would make the
- # ack disagree with the list within a single run, and only until a restart
+ # An app whose directories are also published under a second name (chat's
+ # single destination) has that name re-derived here: leaving it behind
+ # would make the two disagree within a single run, and only until a restart
# — the shape of bug that reads as "it works after a restart".
from meshbay_node.roster import Roster
alias = Roster.ctx_alias(app_key, clean)
@@ -1696,27 +1695,6 @@ async def set_app_directory(state: dict, group_id: str, app_key: str,
return {**result, "path": dirs[0] if dirs else ""}
-# The per-app wrappers MNP still names. They exist so an MNP 1.0 client's
-# `video_root` / `audio_root` / `photo_roots` messages keep working; nothing
-# new should be added here — a new app calls the generic pair above.
-
-async def set_video_root(state: dict, group_id: str, path: str) -> dict:
- result = await set_app_directory(state, group_id, "video", path)
- return {"path": result["path"], "group_id": group_id}
-
-
-async def set_audio_root(state: dict, group_id: str, path: str) -> dict:
- # "music", not "audio": the app's registry key is what identifies it
- # everywhere, and `audio_root` is only the name the setting used to have.
- result = await set_app_directory(state, group_id, "music", path)
- return {"path": result["path"], "group_id": group_id}
-
-
-async def set_photo_roots(state: dict, group_id: str, roots: list[str]) -> dict:
- result = await set_app_directories(state, group_id, "photo", roots)
- return {"roots": result["directories"], "group_id": group_id}
-
-
# ── Chat ─────────────────────────────────────────────────────────────────────
async def set_chat_directory(state: dict, group_id: str, path: str) -> dict:
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index ae0b4cf..b7fbf8e 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -817,14 +817,12 @@ class Roster:
"photo": ("photo_roots", "list"),
}
- # The name each app's directories are *also* published under, for readers
- # that predate the list — the handshake ack's `video_root`, and the group
- # context the ack builds from. Derived from the list, never stored beside
- # it, so the two cannot disagree; the shape says how to derive it.
+ # The name an app's directories are *also* published under, where
+ # something reads that name. Chat is the only one: it has a single
+ # destination, the handshake ack publishes it as `chat_directory`, and the
+ # paperclip reads it. Derived from the list, never stored beside it, so the
+ # two cannot disagree; the shape says how to derive it.
CTX_ALIASES = {
- "video": ("video_root", "scalar"),
- "music": ("audio_root", "scalar"),
- "photo": ("photo_roots", "list"),
"chat": ("chat_directory", "scalar"),
}
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))