From 15406c8e8aa9c191a2a3d7dbceb55dbeecdb721c Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 13:42:01 +0100 Subject: [PATCH] headless: expose DM mode selection and explicit attachment actions --- src/chat/chat_headless_control.c | 24 ++++++++++++++++++++++++ tools/chatcli | 26 ++++++++++++++++++++------ tools/chatcli_commands.txt | 10 ++++++++++ 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/chat/chat_headless_control.c b/src/chat/chat_headless_control.c index bd469a643..f2ef01bd6 100644 --- a/src/chat/chat_headless_control.c +++ b/src/chat/chat_headless_control.c @@ -22,6 +22,8 @@ #include "invite_link.h" #include "invite_build.h" #include "../dm/dm_core.h" +#include "../dm/dm_media.h" +#include "../dm/dm_history.h" #include "../dm/dm_crypto.h" #include "../call/call.h" #include "../call/call_proto.h" @@ -914,6 +916,27 @@ static void hc_handle_dm_send(struct headless_client* cli, int id, const char* j send_response(cli, id, "{\"sent\":true}", NULL); } +/* Ручная загрузка либо явное открытие; строковые числа сохраняют все 64 бита JSON. */ +static void hc_handle_dm_file(struct headless_client* cli, int id, const char* json) { + (void)json; + char conv[64], action[16], author[32], seq[32], generation[32]; + if (json_get_str(cli, "conv_id", conv, sizeof(conv)) < 0 || json_get_str(cli, "action", action, sizeof(action)) < 0 || + json_get_str(cli, "author", author, sizeof(author)) < 0 || json_get_str(cli, "seq", seq, sizeof(seq)) < 0 || + json_get_str(cli, "generation", generation, sizeof(generation)) < 0 || + (strcmp(action, "download") && strcmp(action, "opened"))) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless DM file: invalid arguments request=%d", id); + send_response(cli, id, NULL, "required: conv_id, author, seq, generation, action=download|opened (strings)"); return; + } + char *a_end, *s_end, *g_end; errno = 0; + uint64_t a = strtoull(author, &a_end, 10), s = strtoull(seq, &s_end, 10), g = strtoull(generation, &g_end, 10); + if (errno || !a || !s || !g || author[0] == '-' || seq[0] == '-' || generation[0] == '-' || + *a_end || *s_end || *g_end || !dm_history_current(cli->hc->inst, conv, g) || + dm_media_user_action(cli->hc->inst, conv, a, s, !strcmp(action, "opened"))) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless DM file: rejected request=%d conv=%s action=%s", id, conv, action); + send_response(cli, id, NULL, "attachment action rejected (check identity, generation and local file)"); + } else send_response(cli, id, "{\"accepted\":true}", NULL); +} + /* ── Звонки (P2P аудио) ── */ static uint64_t hc_gen_call_id(void) { @@ -1076,6 +1099,7 @@ static void hc_handle_command(struct headless_client* cli, const char* json) { hc_handle_feedback(cli, id, json); goto done; } if (!strncmp(cmd, "devgroup_", 9)) { hc_handle_devgroup(cli, id, cmd); goto done; } + if (!strcmp(cmd, "dm_file")) { hc_handle_dm_file(cli, id, json); goto done; } for (int i = 0; cmd_names[i]; i++) { if (strcmp(cmd, cmd_names[i]) == 0) { cmd_handlers[i](cli, id, json); goto done; } } diff --git a/tools/chatcli b/tools/chatcli index 1094779c2..edf65363c 100755 --- a/tools/chatcli +++ b/tools/chatcli @@ -28,10 +28,11 @@ # create create new channel # invite_to [proto] # invite a node to add our channel -# dm_start [ch_id] start direct chat (DM) with a group member +# dm_start [ch_id] [--mode device|groups] # dm_list list DM conversations # dm_messages [count] read DM messages (decrypted) # dm_send send DM text message +# dm_file # listen interactive event listener import sys, os, json, socket, struct, argparse @@ -140,10 +141,15 @@ def cmd_invite_to(ch_id, node_id, pubkey, addr, proto=None): params["proto"] = int(proto) _req("invite_to", **params) -def cmd_dm_start(node_id, ch=None): - params = {"node_id": node_id} - if ch: - params["ch"] = ch +def cmd_dm_start(args): + parser = argparse.ArgumentParser(prog="chatcli dm_start") + parser.add_argument("node_id") + parser.add_argument("ch", nargs="?") + parser.add_argument("--mode", choices=("device", "groups"), default="device") + options = parser.parse_args(args) + params = {"node_id": options.node_id, "mode": options.mode} + if options.ch: + params["ch"] = options.ch _req("dm_start", **params) def cmd_dm_list(): @@ -158,6 +164,13 @@ def cmd_dm_messages(conv_id, count=None): def cmd_dm_send(conv_id, text): _req("dm_send", conv_id=conv_id, data=text) +def cmd_dm_file(args): + parser = argparse.ArgumentParser(prog="chatcli dm_file") + for name in ("conv_id", "author", "seq", "generation"): + parser.add_argument(name) + parser.add_argument("action", choices=("download", "opened")) + _req("dm_file", **vars(parser.parse_args(args))) + def cmd_interaction(cmd, args): parser = argparse.ArgumentParser(prog="chatcli " + cmd) if cmd == "reactions": @@ -278,7 +291,8 @@ def main(): elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect "),)) elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create "),)) elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to [proto]"),)) - elif cmd == "dm_start": cmd_dm_start(*args[1:3] if len(args) > 1 else (_die("usage: dm_start [ch_id]"),)) + elif cmd == "dm_start": cmd_dm_start(args[1:]) + elif cmd == "dm_file": cmd_dm_file(args[1:]) elif cmd == "dm_list": cmd_dm_list() elif cmd in ("dm_messages","dm_msgs"): cmd_dm_messages(*args[1:3] if len(args) > 1 else (_die("usage: dm_messages [count]"),)) elif cmd == "dm_send": cmd_dm_send(*args[1:3] if len(args) > 2 else (_die("usage: dm_send "),)) diff --git a/tools/chatcli_commands.txt b/tools/chatcli_commands.txt index 153b9e7a3..a245f5ce6 100644 --- a/tools/chatcli_commands.txt +++ b/tools/chatcli_commands.txt @@ -24,6 +24,16 @@ CHATCLI_PORT — порт по умолчанию (9999) Команды ------- +dm_start [ch_id] [--mode device|groups] + device: конкретное устройство; groups: обе подгруппы с независимым fallback + на устройство. Возвращает conv_id, зафиксированный ядром. + +dm_file + download: явно загрузить вложение независимо от автозагрузки. + opened: подтвердить фактическое открытие пользователем уже проверенного файла. + Не отправлять opened после одной лишь автозагрузки или подготовки превью. + author/seq берутся из dm_messages, generation — из dm_list; числа десятичные. + ping Проверка соединения и uptime сервера. Пример: chatcli ping