summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/roster.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/roster.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py134
1 files changed, 93 insertions, 41 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index 5f38acd..af87f92 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -661,58 +661,110 @@ class Roster:
await self.set_setting(self.NODE_WIDE_GROUP_ID, self.SETTING_TMDB_LANGUAGE,
language, set_by)
- # Which folder is the Videos app's entry point for this group — per-group
- # (unlike the token/language above), since different groups share
- # different trees. Empty/unset means the whole group index, exactly as
- # today.
- SETTING_VIDEO_ROOT = "video_root"
-
- async def video_root(self, group_id: str) -> str:
- return await self.get_setting(group_id, self.SETTING_VIDEO_ROOT, "") or ""
+ # ── App directories ─────────────────────────────────────────────────────
+ #
+ # Which folder(s) inside the group's shared roots each application uses as
+ # its entry point. One storage shape for every app, keyed by the app's own
+ # name, so adding an application needs no change here at all — that is the
+ # whole point of the plugin architecture (docs/refactor-groups.md §1.6).
+ #
+ # Always a JSON list, even for an app that only ever wants one directory.
+ # Two shapes for one idea is how `video_root` (scalar) and `photo_roots`
+ # (list) ended up needing separate ops, separate MNP messages and separate
+ # settings widgets to say the same thing.
+ #
+ # Empty/unset means nothing configured yet, and every app reads that as
+ # "show nothing until an operator has chosen" rather than "the whole group
+ # index" — the discipline video_root established, kept.
+ SETTING_APP_DIRS_SUFFIX = "_directories"
- async def set_video_root(self, group_id: str, path: str, set_by: str = "") -> str:
- await self.set_setting(group_id, self.SETTING_VIDEO_ROOT, path or "", set_by)
- return path or ""
+ # What each app's directories used to be stored under, before they were
+ # one shape. Read as a fallback so an existing node keeps working with no
+ # migration step: the legacy key is never written again, and the first
+ # save through the new path leaves it behind.
+ # Keyed by the *registry* name the app is known by everywhere else
+ # (apps.js, ALLOWED_APPS, enabled_apps) — which for Music is "music", while
+ # its old setting was called `audio_root`. One identifier per app, and the
+ # place the two names meet is this table and nowhere else.
+ LEGACY_DIR_KEYS = {
+ "video": ("video_root", "scalar"),
+ "music": ("audio_root", "scalar"),
+ "photo": ("photo_roots", "list"),
+ }
- # Same shape as SETTING_VIDEO_ROOT — the Music app's own entry point,
- # added later (docs/musicbay.md's original "no root, works over the
- # whole shared tree" simplification turned out not to hold up against a
- # real messy library: the operator asked for the same scoping Videos
- # already had). Empty/unset means Music shows nothing yet, exactly like
- # an unset video_root — see daemon.py's enrichment gate.
- SETTING_AUDIO_ROOT = "audio_root"
+ # 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.
+ CTX_ALIASES = {
+ "video": ("video_root", "scalar"),
+ "music": ("audio_root", "scalar"),
+ "photo": ("photo_roots", "list"),
+ "chat": ("chat_directory", "scalar"),
+ }
- async def audio_root(self, group_id: str) -> str:
- return await self.get_setting(group_id, self.SETTING_AUDIO_ROOT, "") or ""
+ @classmethod
+ def app_dirs_key(cls, app_key: str) -> str:
+ return f"{app_key}{cls.SETTING_APP_DIRS_SUFFIX}"
- async def set_audio_root(self, group_id: str, path: str, set_by: str = "") -> str:
- await self.set_setting(group_id, self.SETTING_AUDIO_ROOT, path or "", set_by)
- return path or ""
+ @classmethod
+ def ctx_alias(cls, app_key: str, directories: list[str]) -> tuple[str, object] | None:
+ """The (name, value) an app's directories are also published under."""
+ alias = cls.CTX_ALIASES.get(app_key)
+ if not alias:
+ return None
+ name, shape = alias
+ if shape == "list":
+ return name, list(directories)
+ return name, (directories[0] if directories else "")
- # Which folder(s) are the Photos app's entry points for this group —
- # a *set*, unlike video_root/audio_root above: a photo library is
- # routinely scattered across several unrelated folders (docs/photos.md
- # §2.1), so there is no single natural root to pick. Stored the same way
- # `enabled_apps` already is (json.dumps(sorted(...))). Empty/unset means
- # nothing configured yet — same "show nothing until an operator has
- # chosen" discipline video_root/audio_root already established, not
- # "the whole group index".
- SETTING_PHOTO_ROOTS = "photo_roots"
+ async def app_directories(self, group_id: str, app_key: str) -> list[str]:
+ value = await self.get_setting(group_id, self.app_dirs_key(app_key))
+ if value is not None:
+ try:
+ return [str(p) for p in json.loads(value)]
+ except (ValueError, TypeError):
+ return []
- async def photo_roots(self, group_id: str) -> list[str]:
- value = await self.get_setting(group_id, self.SETTING_PHOTO_ROOTS)
- if value is None:
+ legacy = self.LEGACY_DIR_KEYS.get(app_key)
+ if not legacy:
return []
+ key, shape = legacy
+ raw = await self.get_setting(group_id, key)
+ if raw is None:
+ return []
+ if shape == "scalar":
+ return [raw] if raw else []
try:
- return list(json.loads(value))
+ return [str(p) for p in json.loads(raw)]
except (ValueError, TypeError):
return []
- async def set_photo_roots(self, group_id: str, roots: list[str],
- set_by: str = "") -> list[str]:
- await self.set_setting(group_id, self.SETTING_PHOTO_ROOTS,
- json.dumps(sorted(roots)), set_by)
- return roots
+ async def set_app_directories(self, group_id: str, app_key: str,
+ paths: list[str], set_by: str = "") -> list[str]:
+ clean = sorted({str(p).strip("/") for p in paths if str(p).strip("/")})
+ await self.set_setting(group_id, self.app_dirs_key(app_key),
+ json.dumps(clean), set_by)
+ return clean
+
+ # ── Chat ────────────────────────────────────────────────────────────────
+
+ # Whether the node fetches a page's title/preview when a member posts a
+ # link. Outbound third-party traffic on the operator's connection, from a
+ # message they did not write, so it is theirs to switch off — the same
+ # reasoning as the per-group TMDB switch. Unset means on, because that is
+ # what the node did before this existed.
+ SETTING_CHAT_LINK_PREVIEW = "chat_link_preview"
+
+ async def chat_link_preview(self, group_id: str) -> bool:
+ value = await self.get_setting(group_id, self.SETTING_CHAT_LINK_PREVIEW, "1")
+ return value != "0"
+
+ async def set_chat_link_preview(self, group_id: str, enabled: bool,
+ set_by: str = "") -> bool:
+ await self.set_setting(group_id, self.SETTING_CHAT_LINK_PREVIEW,
+ "1" if enabled else "0", set_by)
+ return enabled
# Whether TMDB lookups run for this group at all — per-group, unlike the
# token/language above: one node process can share a real media library