aboutsummaryrefslogtreecommitdiffstats
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.py53
1 files changed, 37 insertions, 16 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index e2f749f..5f38acd 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -31,6 +31,8 @@ from pathlib import Path
import aiosqlite
+from meshbay_common.paths import fold
+
log = logging.getLogger(__name__)
# Crockford base32 without I, L, O and U: no character pair a human can confuse
@@ -543,10 +545,40 @@ class Roster:
# ── Group settings ──────────────────────────────────────────────────────
- # Whether members who are not the operator may upload. Default is yes: a
- # group that nobody may add to is the unusual case, and an existing node
- # must not change behaviour because a table was added under it.
- SETTING_MEMBER_UPLOAD = "member_upload"
+ # Whether a root is ejected. Runtime state, one key per root, keyed by the
+ # *folded* name so it agrees with the case-insensitive comparison the rest
+ # of the root code makes. It lives here rather than in node.toml because it
+ # is not configuration — an operator's hand-written config file should not
+ # be rewritten because a USB drive was unplugged — and it has to survive a
+ # restart, or the rescan that follows reads an empty mount point as an
+ # erased library, which is the whole thing eject exists to prevent.
+ SETTING_ROOT_EJECTED_PREFIX = "root_ejected:"
+
+ @classmethod
+ def root_ejected_key(cls, root_name: str) -> str:
+ return cls.SETTING_ROOT_EJECTED_PREFIX + fold(root_name)
+
+ async def set_root_ejected(self, group_id: str, root_name: str,
+ ejected: bool, set_by: str = "") -> None:
+ await self.set_setting(group_id, self.root_ejected_key(root_name),
+ "1" if ejected else "0", set_by)
+
+ async def ejected_roots(self, group_id: str) -> set[str]:
+ """
+ The folded names of this group's ejected roots.
+
+ Matched in Python rather than with `LIKE 'root_ejected:%'`: `_` is a
+ single-character wildcard there, so that pattern also matches keys this
+ does not own. A group has a handful of settings rows, so reading them
+ all costs nothing and the prefix test is then exact.
+ """
+ prefix = self.SETTING_ROOT_EJECTED_PREFIX
+ async with self._db.execute(
+ "SELECT key, value FROM group_settings WHERE group_id = ?",
+ (group_id,)) as cur:
+ rows = await cur.fetchall()
+ return {r["key"][len(prefix):] for r in rows
+ if r["key"].startswith(prefix) and r["value"] == "1"}
async def get_setting(self, group_id: str, key: str,
default: str | None = None) -> str | None:
@@ -567,17 +599,6 @@ class Roster:
(group_id, key, value, set_by, _now()))
await self._db.commit()
- async def member_upload_allowed(self, group_id: str) -> bool:
- """Whether an ordinary member may upload to this group."""
- value = await self.get_setting(group_id, self.SETTING_MEMBER_UPLOAD, "1")
- return value != "0"
-
- async def set_member_upload(self, group_id: str, allowed: bool,
- set_by: str = "") -> bool:
- await self.set_setting(group_id, self.SETTING_MEMBER_UPLOAD,
- "1" if allowed else "0", set_by)
- return allowed
-
# Which group "applications" (Chat, Files, and whatever registers later in
# apps.js) are shown to members. Unset means every app that exists — an
# existing group's tabs must not disappear because a node was upgraded.
@@ -609,7 +630,7 @@ class Roster:
# user_id)` authorizing the operator node-wide (desktop-client-v1.md
# §6.3). Unset means "the shipped default token, TMDB's own default
# language" — the same "absent means the old behaviour" discipline
- # member_upload/enabled_apps already follow.
+ # enabled_apps already follows.
#
# Whether TMDB is used *at all*, though, is per-group (moved off the
# node-wide sentinel below, 2026-08-24): an operator running a real media