aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/content.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/cli/content.py')
-rw-r--r--packages/meshbay-node/src/meshbay_node/cli/content.py133
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)