diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/roster.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/roster.py | 134 |
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 |