summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/commands/chat.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/chat.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/commands/chat.py108
1 files changed, 108 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/commands/chat.py b/packages/meshbay-cli/src/meshbay_cli/commands/chat.py
new file mode 100644
index 0000000..61e9fa7
--- /dev/null
+++ b/packages/meshbay-cli/src/meshbay_cli/commands/chat.py
@@ -0,0 +1,108 @@
+"""
+`chat`: a group's conversation — the last messages, or one more posted.
+
+`chat GROUP` prints the last ten, oldest first; `-N` (as `head -N`) or `-n N`
+asks for another count. `chat GROUP -m TEXT` posts TEXT (`-m -` reads it from
+stdin), sealed and signed as the application seals and signs it. Each message
+is shown with its time and the name its sender sealed in it; one that cannot
+be read is shown as such, never left out.
+"""
+
+import json
+import re
+import sys
+
+from ..client.chat import chat_keys, history, open_row, post
+from ..client.groups import my_groups, open_group
+from ..client.paths import group_arg, handle, resolve_group
+from ..errors import USAGE, CliError
+from ..output import date, err, safe, sentence
+from .context import Context
+
+HELP = "read a group's chat (the last 10 messages, -N for N), or post with -m"
+
+_COUNT = re.compile(r"^-(\d+)$")
+
+
+def add_parser(sub, parent) -> None:
+ p = sub.add_parser("chat", parents=[parent], help=HELP, description=sentence(HELP)
+ + ". Messages are end-to-end encrypted and signed; the node "
+ "stores what it cannot read.")
+ p.add_argument("group", metavar="GROUP", help="the group, as `groups` lists it")
+ p.add_argument("-n", "--last", type=int, default=10, metavar="N",
+ help="how many of the last messages to show (also -N, as -100)")
+ p.add_argument("-m", "--message", metavar="TEXT",
+ help="post TEXT instead of reading; - reads it from stdin")
+
+
+def rewrite_count(argv: list[str], actions) -> list[str]:
+ """`chat -100 GROUP` as `chat --last 100 GROUP`: argparse reads -100 as a
+ negative number, not as an option. Only when the action is `chat`."""
+ action = next((i for i, a in enumerate(argv) if a in actions), None)
+ if action is None or argv[action] != "chat":
+ return argv
+ out = argv[:action + 1]
+ for a in argv[action + 1:]:
+ m = _COUNT.match(a)
+ out += ["--last", m.group(1)] if m else [a]
+ return out
+
+
+def _text(message: str) -> str:
+ text = sys.stdin.read() if message == "-" else message
+ text = text.rstrip("\n")
+ if not text.strip():
+ raise CliError(USAGE, "Nothing to post.")
+ return text
+
+
+async def run(ctx: Context) -> int:
+ a = ctx.args
+ if a.last < 1:
+ raise CliError(USAGE, "Ask for one message at least.")
+ text = _text(a.message) if a.message is not None else None
+ await ctx.hub.check_version()
+ account = ctx.account()
+ session = None
+ try:
+ group = resolve_group(await my_groups(account), group_arg(a.group))
+ session = await open_group(account, group)
+ keys = await chat_keys(session)
+ if text is not None:
+ await post(session, keys, text, account.username)
+ if a.json:
+ print(json.dumps({"posted": True, "group": handle(group)}))
+ elif not a.quiet:
+ err(f"Posted to {safe(handle(group))}.")
+ return 0
+ messages = [open_row(r, keys, group["id"]) for r in await history(session, a.last)]
+ finally:
+ if session is not None:
+ await session.close()
+ await account.close()
+
+ if a.json:
+ json.dump([{"id": m.id, "timestamp": m.timestamp, "sender": m.sender,
+ "text": m.text, **({"unreadable": m.unreadable} if m.unreadable
+ else {})} for m in messages],
+ sys.stdout, indent=1, ensure_ascii=False)
+ print()
+ return 0
+ if not messages and not a.quiet:
+ err(f"No message in {safe(handle(group))} yet.")
+ for m in messages:
+ head = f"{date(int(m.timestamp))} {safe(m.sender)}: "
+ if m.unreadable:
+ print(f"{head}[this message cannot be read: {m.unreadable}]")
+ continue
+ lines = safe_lines(m.text)
+ print(head + lines[0])
+ for line in lines[1:]:
+ print(" " * len(head) + line)
+ return 0
+
+
+def safe_lines(text: str) -> list[str]:
+ """A message's lines, each escaped: a line break stays a line break, every
+ other control character is shown, never sent to the terminal."""
+ return [safe(line) for line in text.split("\n")] or [""]