diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-15 02:06:35 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-15 02:06:35 +0200 |
| commit | 419af660eeb8a4403e37a63c54a15586d04d5a8e (patch) | |
| tree | b0c0cdd831a5fb2864d5bedf581681aa0ec86023 /packages/meshbay-cli/src/meshbay_cli/commands/chat.py | |
| parent | 47583c5e405f3011a526df73cc9485c46598f214 (diff) | |
| download | meshbay-419af660eeb8a4403e37a63c54a15586d04d5a8e.tar.gz | |
feat(cli): chat — Tier 2 check on the signing device, interactive room
Each message's signing device is checked against the group's roster as the
page checks it (a port of transport-roster.js, held to it by the parity
test): chains of countersignatures walked from each account's roots, the
keys accepted per account and node kept in the store, the roster read again
once before a key is called changed. Only "changed" is said, under the
message; --json carries every verdict.
chat with no group opens the interactive chat: the groups, then one group's
conversation IRC-style with pushed messages arriving live and a line to
write in; Esc or /part goes back to the groups, /quit or Ctrl+Q quits. Like
the bare command, it opens on the sign-in screen when nothing is stored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/commands/chat.py')
| -rw-r--r-- | packages/meshbay-cli/src/meshbay_cli/commands/chat.py | 36 |
1 files changed, 30 insertions, 6 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/commands/chat.py b/packages/meshbay-cli/src/meshbay_cli/commands/chat.py index 61e9fa7..6fa084d 100644 --- a/packages/meshbay-cli/src/meshbay_cli/commands/chat.py +++ b/packages/meshbay-cli/src/meshbay_cli/commands/chat.py @@ -1,7 +1,9 @@ """ `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` +`chat` alone opens the interactive chat (`tui/chat.py`): the groups, one +group's conversation IRC-style, back to the groups. `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 @@ -15,20 +17,27 @@ 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 ..client.roster import RosterCheck 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" +HELP = ("chat: alone, interactively; with a group, its last 10 messages (-N for N), " + "or post with -m") _COUNT = re.compile(r"^-(\d+)$") +# The one verdict worth a line (roster.py): the application's words. +KEY_CHANGED = ("! This account is using a key you have not seen before: a new device " + "added elsewhere, or the node naming the wrong person.") 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("group", metavar="GROUP", nargs="?", + help="the group, as `groups` lists it; without it, the interactive " + "chat: choose a group, talk, leave it, choose another") 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", @@ -60,6 +69,16 @@ async def run(ctx: Context) -> int: a = ctx.args if a.last < 1: raise CliError(USAGE, "Ask for one message at least.") + if a.group is None: + if a.message is not None: + raise CliError(USAGE, "Name the group to post to.") + if not (sys.stdin.isatty() and sys.stdout.isatty()): + raise CliError(USAGE, "The interactive chat needs a terminal; name a group " + "to read it, or to post with -m.") + from ..tui.app import run_interactive + from ..tui.chat import ChatGroupsScreen + # As with no argument at all: the sign-in screen checks the hub. + return await run_interactive(ctx, ChatGroupsScreen) text = _text(a.message) if a.message is not None else None await ctx.hub.check_version() account = ctx.account() @@ -75,7 +94,9 @@ async def run(ctx: Context) -> int: 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)] + check = RosterCheck(account, session) + messages = [await open_row(r, keys, group["id"], check) + for r in await history(session, a.last)] finally: if session is not None: await session.close() @@ -83,8 +104,9 @@ async def run(ctx: Context) -> int: 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], + "text": m.text, "trust": m.trust, + **({"unreadable": m.unreadable} if m.unreadable else {})} + for m in messages], sys.stdout, indent=1, ensure_ascii=False) print() return 0 @@ -99,6 +121,8 @@ async def run(ctx: Context) -> int: print(head + lines[0]) for line in lines[1:]: print(" " * len(head) + line) + if m.trust == "changed": + print(" " * len(head) + KEY_CHANGED) return 0 |