1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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)
|