headless: expose DM mode selection and explicit attachment actions

This commit is contained in:
evgeny committed 2026-11-03 13:42:01 +01:00
1 parent 12b9841fca
commit 15406c8e8a
3 files changed
+54 -6

No files matched your search

+24
View File
@@ -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; }
}
+20 -6
View File
@@ -28,10 +28,11 @@
# create <name> create new channel
# invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]
# invite a node to add our channel
# dm_start <node_id> [ch_id] start direct chat (DM) with a group member
# dm_start <node_id> [ch_id] [--mode device|groups]
# dm_list list DM conversations
# dm_messages <conv_id> [count] read DM messages (decrypted)
# dm_send <conv_id> <text> send DM text message
# dm_file <conv_id> <author> <seq> <generation> <download|opened>
# 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 <utun://...>"),))
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]"),))
elif cmd == "dm_start": cmd_dm_start(*args[1:3] if len(args) > 1 else (_die("usage: dm_start <node_id> [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 <conv_id> [count]"),))
elif cmd == "dm_send": cmd_dm_send(*args[1:3] if len(args) > 2 else (_die("usage: dm_send <conv_id> <text>"),))
+10
View File
@@ -24,6 +24,16 @@ CHATCLI_PORT — порт по умолчанию (9999)
Команды
-------
dm_start <node_id_hex> [ch_id] [--mode device|groups]
device: конкретное устройство; groups: обе подгруппы с независимым fallback
на устройство. Возвращает conv_id, зафиксированный ядром.
dm_file <conv_id> <author_decimal> <seq> <generation> <download|opened>
download: явно загрузить вложение независимо от автозагрузки.
opened: подтвердить фактическое открытие пользователем уже проверенного файла.
Не отправлять opened после одной лишь автозагрузки или подготовки превью.
author/seq берутся из dm_messages, generation — из dm_list; числа десятичные.
ping
Проверка соединения и uptime сервера.
Пример: chatcli ping