summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/src/meshbay_node/cli/content.py
blob: 16f1df896d607af18dffc076b5d06b1196aa7dc4 (plain) (blame)
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)