diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/content.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/cli/content.py | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/cli/content.py b/packages/meshbay-node/src/meshbay_node/cli/content.py new file mode 100644 index 0000000..16f1df8 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/cli/content.py @@ -0,0 +1,133 @@ +"""A group's content: its files, its video matches, its chat.""" + +import sys + +from meshbay_common import MNP_VERSION + +from meshbay_node.cli.api import _daemon_api, _resolve_group +from meshbay_node.config import DEFAULT_CONFIG_PATH, load_config + + +def file(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + sub = args.subcommand or "list" + group_id = _resolve_group(cfg, args.group) + + if sub == "list": + out = _daemon_api(cfg, f"/api/groups/{group_id}/files") + files = sorted(out.get("files", []), key=lambda f: (f["path"], f["name"])) + if not files: + print("no files indexed") + return + for f in files: + print(f" {f['id'][:12]} {f['size']:>12} {f['path']}/{f['name']}") + print(f"\n{len(files)} file(s). Remove one with: " + f"meshbay-node file rm <id>") + return + + if sub == "rm": + # Milestone 14.11 — the last operator action that needed a browser. + if not args.target: + print("usage: meshbay-node file rm <file-id> [--group NAME]") + sys.exit(1) + out = _daemon_api(cfg, f"/api/groups/{group_id}/files",) + matches = [f for f in out.get("files", []) + if f["id"].startswith(args.target)] + if not matches: + print(f"no file whose id starts with {args.target!r}") + sys.exit(1) + if len(matches) > 1: + print(f"{args.target!r} matches {len(matches)} files — be more specific:") + for f in matches[:10]: + print(f" {f['id'][:16]} {f['path']}/{f['name']}") + sys.exit(1) + target = matches[0] + if not args.yes: + print(f"Delete {target['path']}/{target['name']} " + f"({target['size']} bytes) from disk?") + print("This removes the file itself, not just the listing.") + if input("delete? [y/N] ").strip().lower() not in ("y", "yes"): + print("cancelled") + return + _daemon_api(cfg, f"/api/groups/{group_id}/files/{target['id']}", + method="DELETE") + print(f"deleted {target['path']}/{target['name']}") + return + + print("usage: meshbay-node file list|rm <id> [--group NAME] [--yes]") + sys.exit(1) + + +def video(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + if (args.subcommand or "") != "rematch": + print("usage: meshbay-node video rematch [--group NAME] [--yes]") + sys.exit(1) + group_id = _resolve_group(cfg, args.group) + if not args.yes: + print("Re-resolve every automatic TMDB match for this group's videos?") + print("Manual 'Fix match' corrections are kept. Re-resolution is lazy —") + print("each poster re-queries TMDB the next time it is opened.") + if input("proceed? [y/N] ").strip().lower() not in ("y", "yes"): + print("cancelled") + return + out = _daemon_api(cfg, f"/api/groups/{group_id}/video/rematch", method="POST") + print(f"cleared {out.get('removed', 0)} automatic match(es) " + f"across {out.get('videos', 0)} video file(s)") + return + + +def chat(args) -> None: + cfg = load_config(args.config or DEFAULT_CONFIG_PATH) + sub = args.subcommand or "status" + group_id = _resolve_group(cfg, args.group) + + if sub == "status": + out = _daemon_api(cfg, f"/api/groups/{group_id}/chat") + print(f"encryption always on (MNP {MNP_VERSION})") + print(f"epoch {out.get('epoch', 0)}") + print(f"messages {out.get('encrypted_messages', 0)} encrypted, " + f"{out.get('plaintext_messages', 0)} in the clear") + if out.get("plaintext_messages"): + print("\nThose messages were written before this node spoke MNP " + "2.0 and are\nstill readable off this disk. " + "`chat encrypt-history` converts them.") + return + + if sub == "rotate": + out = _daemon_api(cfg, f"/api/groups/{group_id}/chat/epoch", + method="POST") + print(f"chat epoch {out['epoch']} opened") + print("Everyone still in the group keeps reading the history; " + "whoever left\ncannot read what is written from now on.") + return + + if sub == "encrypt-history": + if not args.yes: + print("This rewrites the only copy of this group's older " + "messages.") + print("A backup of chat.db is taken first, beside it.") + if input("re-encrypt now? [y/N] ").strip().lower() not in ("y", "yes"): + print("cancelled") + return + out = _daemon_api(cfg, f"/api/groups/{group_id}/chat/encrypt-history", + method="POST", timeout=300) + print(f"re-encrypted {out['converted']} message(s) under epoch " + f"{out['epoch']}") + print(f"backup {out['backup']}") + return + + if sub == "prune": + days = int(args.target or 0) + if days < 1: + print("usage: meshbay-node chat prune <days> [--group G]") + sys.exit(1) + out = _daemon_api( + cfg, f"/api/groups/{group_id}/chat/prune?max_age_days={days}", + method="POST") + print(f"removed {out['removed']} message(s) older than {days} day(s)") + return + + print("usage: meshbay-node chat " + "status|rotate|encrypt-history|prune [--group G]") + sys.exit(1) |