Browse Source

Validate feedback data and provide poll command snapshots

master
evgeny 5 days ago
parent
commit
e6db9fef60
  1. 1
      src/chat/chat_event.h
  2. 3
      src/chat/chat_feedback.c
  3. 364
      src/chat/chat_poll.c
  4. 2
      src/chat/chat_poll.h

1
src/chat/chat_event.h

@ -75,6 +75,7 @@ extern "C" {
#define CHAT_EVT_AVATAR_RESULT 51 /* request_id/group:8LE, failed:1; результат публикации ссылки профиля */
#define CHAT_EVT_GROUP_AVATAR_RESULT 52 /* request_id/group:8LE, failed:1; результат публикации аватарки группы */
#define CHAT_EVT_FEEDBACK_CHANGED 53 /* [channel_len:1][channel][message_id:32][kind:1] — состояние после commit */
#define CHAT_EVT_FEEDBACK_RESULT 54 /* [request_id:8LE][success:1][JSON] — результат команды интерфейса */
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);

3
src/chat/chat_feedback.c

@ -1,4 +1,5 @@
#include "chat_feedback.h"
#include "chat_poll.h"
#include "merkle_sync.h"
#include "member_sync.h"
#include "chat_event.h"
@ -62,6 +63,7 @@ static int fb_encode(const struct chat_feedback_record* r, uint8_t* wire) {
|| (r->kind != CHAT_FEEDBACK_REACTION && r->kind != CHAT_FEEDBACK_ANSWER) || r->data_len > CHAT_FEEDBACK_DATA_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "feedback: invalid record fields"); return -1;
}
if (chat_feedback_validate_data(r->kind, r->data, r->data_len) < 0) return -1;
memcpy(wire, r->message_id, 32); fb_write64(wire + 32, r->node_id); wire[40] = r->kind;
fb_write64(wire + 41, r->ver); wire[49] = r->data_len >> 8; wire[50] = r->data_len & 255;
memcpy(wire + 51, r->data, r->data_len); memcpy(wire + 51 + r->data_len, r->signature, 64);
@ -225,6 +227,7 @@ static int fb_apply_row(struct UTUN_INSTANCE* inst, const char* channel, const s
static void fb_committed(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_feedback_record* r) {
uint8_t event[54]; size_t len = strlen(channel); event[0] = len;
memcpy(event + 1, channel, len); memcpy(event + 1 + len, r->message_id, 32); event[33 + len] = r->kind;
chat_feedback_check_message(inst, channel, r->message_id);
chat_event_post(inst, CHAT_EVT_FEEDBACK_CHANGED, event, (int)len + 34);
}

364
src/chat/chat_poll.c

@ -0,0 +1,364 @@
#include "chat_poll.h"
#include "chat_feedback.h"
#include "chat_core_priv.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/strbuf.h"
#include "../../lib/mem.h"
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* Канал — канонический положительный ID, подходящий для имени таблицы. */
static int poll_channel(struct UTUN_INSTANCE* inst, const char* channel, uint64_t* id) {
if (!inst || !inst->topo_sqlite_db || !channel || !channel[0] || channel[0] == '0' || strlen(channel) > 19) goto invalid;
for (const char* p = channel; *p; p++) if (*p < '0' || *p > '9') goto invalid;
errno = 0; char* end; uint64_t n = strtoull(channel, &end, 10);
if (errno || *end || n > INT64_MAX) goto invalid;
if (id) *id = n;
return 0;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid channel=%s", channel ? channel : "-"); return -1;
}
static int poll_db_error(struct UTUN_INSTANCE* inst, const char* operation) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: %s: %s", operation, sqlite3_errmsg(inst->topo_sqlite_db)); return -1;
}
/* Возвращает 1 для отсутствующего сообщения, 0 для найденного, -1 для ошибки.
* is_poll отдельно отличает обычное сообщение от опроса. БД содержит проверенную подпись. */
static int poll_target(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32],
struct chat_poll* poll, int* is_poll) {
if (poll_channel(inst, channel, NULL) < 0 || !message_id) return -1;
*is_poll = 0;
if (!inst->chat_core) return 1;
char sql[512];
snprintf(sql, sizeof(sql), "SELECT CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.ct') END,"
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.d') END,"
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.ch') END"
" FROM msg_%s WHERE message_id=?", channel);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) return poll_db_error(inst, "target query");
sqlite3_bind_blob(st, 1, message_id, 32, SQLITE_STATIC);
int step = sqlite3_step(st), result = 0;
if (step == SQLITE_DONE) result = 1;
else if (step != SQLITE_ROW) result = poll_db_error(inst, "target read");
else {
const char* type = (const char*)sqlite3_column_text(st, 0);
if (type && !strcmp(type, "poll")) {
const char* body = (const char*)sqlite3_column_text(st, 1);
const char* ch = (const char*)sqlite3_column_text(st, 2);
int len = sqlite3_column_bytes(st, 1);
if (!body || !ch || strcmp(ch, channel) || len < 4 || len > 4 + 4 * ((CHAT_POLL_WIRE_MAX + 2) / 3)
|| memcmp(body, "b64:", 4)) result = -1;
else {
uint8_t wire[CHAT_POLL_WIRE_MAX]; char canonical[4 * ((CHAT_POLL_WIRE_MAX + 2) / 3)];
size_t bytes = b64_decode(body + 4, len - 4, wire, sizeof(wire));
size_t encoded = b64_encode(wire, bytes, canonical, sizeof(canonical));
if (encoded != (size_t)len - 4 || memcmp(canonical, body + 4, encoded)
|| chat_poll_decode(wire, bytes, poll) < 0) result = -1;
else *is_poll = 1;
}
if (result < 0) DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid signed poll ch=%s", channel);
}
}
sqlite3_finalize(st); return result;
}
/* Семантика ответа не влияет на сохранение подписанных байтов и Merkle-корень. */
static int poll_answer_allowed(const struct chat_poll* poll, const struct chat_poll_answer* answer) {
if (!answer->count && !answer->custom[0]) return 1;
if (answer->custom[0] && !poll->allow_custom_answer) return 0;
if (poll->mode == CHAT_POLL_SINGLE && answer->count + (answer->custom[0] != 0) != 1) return 0;
for (unsigned i = 0; i < answer->count; i++) if (answer->choices[i] >= poll->option_count) return 0;
return 1;
}
int chat_poll_create(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_poll* poll, uint8_t out_id[32]) {
if (poll_channel(inst, channel, NULL) < 0 || !out_id) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid create arguments"); return -1;
}
uint8_t wire[CHAT_POLL_WIRE_MAX]; char body[4 + 4 * ((CHAT_POLL_WIRE_MAX + 2) / 3) + 1];
int bytes = chat_poll_encode(poll, wire, sizeof(wire)); if (bytes < 0) return -1;
memcpy(body, "b64:", 4); size_t len = 4 + b64_encode(wire, bytes, body + 4, sizeof(body) - 5); body[len] = 0;
struct chat_msg_submit req = { .inst = inst, .data = (uint8_t*)body, .data_len = len };
snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); snprintf(req.content_type, sizeof(req.content_type), "poll");
if (chat_core_submit_message(inst, &req) < 0) return -1;
memcpy(out_id, req.message_id, 32);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "poll: published ch=%s mode=%u options=%u custom=%u bytes=%d",
channel, poll->mode, poll->option_count, poll->allow_custom_answer, bytes); return 0;
}
/* Формат текста JSON и вывод выбора используются одинаково для mine и списка ответов. */
static int poll_json_answer(struct strbuf* out, const struct chat_poll_answer* answer) {
if (strbuf_addf(out, "{\"choices\":[") < 0) return -1;
for (unsigned i = 0; i < answer->count; i++)
if (strbuf_addf(out, "%s%u", i ? "," : "", answer->choices[i]) < 0) return -1;
if (strbuf_addf(out, "],\"custom\":") < 0 || strbuf_add_json_string(out, answer->custom) < 0) return -1;
return strbuf_addf(out, "}") < 0 ? -1 : 0;
}
/* Результаты производны от последних допустимых состояний каждого автора.
* Чужие неверные ответы сохраняются для сходимости, но не участвуют в подсчёте. */
int chat_feedback_snapshot(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32], char** json) {
if (json) *json = NULL;
if (!json || poll_channel(inst, channel, NULL) < 0 || !message_id) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid snapshot arguments"); return -1;
}
struct chat_poll poll; int is_poll, target = poll_target(inst, channel, message_id, &poll, &is_poll);
if (target < 0) return -1;
unsigned counts[CHAT_REACTION_COUNT] = {0}, votes[CHAT_POLL_OPTIONS_MAX] = {0}, voters = 0, custom_count = 0;
uint8_t mine[CHAT_REACTION_COUNT] = {0}; uint64_t reaction_ver = 0, answer_ver = 0;
struct chat_poll_answer own_answer = {0};
struct strbuf replies = strbuf_new(), out = strbuf_new();
sqlite3_stmt* st = NULL; int result = -1, step;
if (strbuf_addf(&replies, "[") < 0) goto done;
char sql[256];
snprintf(sql, sizeof(sql), "SELECT node_id,kind,ver,data FROM feedback_%s WHERE message_id=? ORDER BY node_id,kind", channel);
if (sqlite3_prepare_v2(inst->topo_sqlite_db, sql, -1, &st, NULL) != SQLITE_OK) { poll_db_error(inst, "snapshot query"); goto done; }
sqlite3_bind_blob(st, 1, message_id, 32, SQLITE_STATIC);
while ((step = sqlite3_step(st)) == SQLITE_ROW) {
uint64_t author = (uint64_t)sqlite3_column_int64(st, 0), ver = (uint64_t)sqlite3_column_int64(st, 2);
const uint8_t* data = sqlite3_column_blob(st, 3); size_t len = sqlite3_column_bytes(st, 3);
int kind = sqlite3_column_int(st, 1);
if (kind == CHAT_FEEDBACK_REACTION) {
uint8_t ids[CHAT_REACTION_SELECTED_MAX]; size_t count;
if (chat_reactions_decode(data, len, ids, &count) < 0) goto done;
if (author == inst->node_id) reaction_ver = ver;
if (!target) for (size_t i = 0; i < count; i++) { counts[ids[i]]++; if (author == inst->node_id) mine[ids[i]] = 1; }
} else if (kind == CHAT_FEEDBACK_ANSWER) {
struct chat_poll_answer answer;
if (chat_answer_decode(data, len, &answer) < 0) goto done;
if (author == inst->node_id) answer_ver = ver;
if (target || !is_poll || !poll_answer_allowed(&poll, &answer)) continue;
if (author == inst->node_id) own_answer = answer;
if (!answer.count && !answer.custom[0]) continue;
for (unsigned i = 0; i < answer.count; i++) votes[answer.choices[i]]++;
if (answer.custom[0]) custom_count++;
if (strbuf_addf(&replies, "%s{\"node_id\":\"0x%016llx\",\"ver\":%llu,\"answer\":",
voters ? "," : "", (unsigned long long)author, (unsigned long long)ver) < 0
|| poll_json_answer(&replies, &answer) < 0 || strbuf_addf(&replies, "}") < 0) goto done;
voters++;
}
}
if (step != SQLITE_DONE) { poll_db_error(inst, "snapshot read"); goto done; }
char hex[65]; for (unsigned i = 0; i < 32; i++) snprintf(hex + i * 2, 3, "%02x", message_id[i]);
if (strbuf_addf(&replies, "]") < 0 || strbuf_addf(&out,
"{\"ch\":\"%s\",\"message_id\":\"%s\",\"pending\":%s,\"reaction_ver\":%llu,\"reactions\":[",
channel, hex, target ? "true" : "false", (unsigned long long)reaction_ver) < 0) goto done;
unsigned shown = 0;
for (unsigned i = 0; i < CHAT_REACTION_COUNT; i++) if (counts[i]) {
if (strbuf_addf(&out, "%s{\"id\":%u,\"emoji\":", shown++ ? "," : "", i) < 0
|| strbuf_add_json_string(&out, chat_reaction_emoji(i)) < 0
|| strbuf_addf(&out, ",\"count\":%u,\"mine\":%s}", counts[i], mine[i] ? "true" : "false") < 0) goto done;
}
if (strbuf_addf(&out, "],\"poll\":") < 0) goto done;
if (is_poll) {
if (strbuf_addf(&out, "{\"mode\":\"%s\",\"allow_custom_answer\":%s,\"question\":",
poll.mode == CHAT_POLL_SINGLE ? "single" : "multiple", poll.allow_custom_answer ? "true" : "false") < 0
|| strbuf_add_json_string(&out, poll.question) < 0 || strbuf_addf(&out, ",\"options\":[") < 0) goto done;
for (unsigned i = 0; i < poll.option_count; i++) {
if (strbuf_addf(&out, "%s{\"id\":%u,\"text\":", i ? "," : "", i) < 0
|| strbuf_add_json_string(&out, poll.options[i]) < 0
|| strbuf_addf(&out, ",\"count\":%u,\"percent\":%.2f}", votes[i], voters ? 100.0 * votes[i] / voters : 0.0) < 0) goto done;
}
if (strbuf_addf(&out, "],\"voters\":%u,\"custom_count\":%u,\"ver\":%llu,\"mine\":",
voters, custom_count, (unsigned long long)answer_ver) < 0
|| poll_json_answer(&out, &own_answer) < 0
|| strbuf_addf(&out, ",\"answers\":%s}", strbuf_str(&replies)) < 0) goto done;
} else if (strbuf_addf(&out, "null") < 0) goto done;
if (strbuf_addf(&out, "}") < 0) goto done;
*json = strbuf_detach(&out); result = 0;
done:
if (st) sqlite3_finalize(st);
strbuf_free(&replies); strbuf_free(&out);
if (result < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: snapshot failed ch=%s", channel);
return result;
}
/* Сохраняем диагностику семантики при новом состоянии/получении исходного сообщения. */
void chat_feedback_check_message(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32]) {
char* snapshot = NULL;
if (chat_feedback_snapshot(inst, channel, message_id, &snapshot) == 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "poll: evaluated ch=%s state=%s", channel, snapshot); u_free(snapshot);
}
}
/* Читает поле JSON с его типом. Отсутствующее поле имеет type=NULL. */
static sqlite3_stmt* poll_field(struct UTUN_INSTANCE* inst, const char* json, const char* path) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT json_type(?1,?2),json_extract(?1,?2)", -1, &st, NULL) != SQLITE_OK) {
poll_db_error(inst, "command field"); return NULL;
}
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) return st;
sqlite3_finalize(st); poll_db_error(inst, "command field read"); return NULL;
}
/* Требуем полную строку, не разрешая тихое обрезание и встроенный NUL. */
static int poll_field_text(struct UTUN_INSTANCE* inst, const char* json, const char* path, char* out, size_t capacity, int optional) {
sqlite3_stmt* st = poll_field(inst, json, path); if (!st) return -1;
const char* type = (const char*)sqlite3_column_text(st, 0), *text = (const char*)sqlite3_column_text(st, 1);
size_t bytes = sqlite3_column_bytes(st, 1); int result = -1;
if (!type && optional) { out[0] = 0; result = 0; }
else if (type && !strcmp(type, "text") && text && bytes < capacity && strlen(text) == bytes) {
memcpy(out, text, bytes + 1); result = 0;
}
sqlite3_finalize(st);
if (result < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid command field=%s", path);
return result;
}
/* Выбор пунктов/эмодзи: только массив целых номеров, без преобразования строк/дробей. */
static int poll_field_ids(struct UTUN_INSTANCE* inst, const char* json, const char* path, uint8_t* ids, size_t capacity) {
sqlite3_stmt* field = poll_field(inst, json, path); if (!field) return -1;
const char* type = (const char*)sqlite3_column_text(field, 0); int absent = !type;
int array = type && !strcmp(type, "array"); sqlite3_finalize(field);
if (absent) return 0;
if (!array) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: expected array field=%s", path); return -1; }
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT type,value FROM json_each(?1,?2)", -1, &st, NULL) != SQLITE_OK) {
poll_db_error(inst, "command array"); return -1;
}
sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC);
int step, count = 0;
while ((step = sqlite3_step(st)) == SQLITE_ROW) {
const char* element = (const char*)sqlite3_column_text(st, 0); sqlite3_int64 n = sqlite3_column_int64(st, 1);
if (count == (int)capacity || !element || strcmp(element, "integer") || n < 0 || n > 255) {
count = -1; DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid array element field=%s", path); break;
}
ids[count++] = (uint8_t)n;
}
sqlite3_finalize(st);
if (count >= 0 && step != SQLITE_DONE) { poll_db_error(inst, "command array read"); return -1; }
return count;
}
static int poll_field_bool(struct UTUN_INSTANCE* inst, const char* json, const char* path, int default_value) {
sqlite3_stmt* st = poll_field(inst, json, path); if (!st) return -2;
const char* type = (const char*)sqlite3_column_text(st, 0);
int value = !type ? default_value : !strcmp(type, "true") ? 1 : !strcmp(type, "false") ? 0 : -2;
sqlite3_finalize(st);
if (value == -2) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: expected boolean field=%s", path);
return value;
}
/* Ошибка доступна интерфейсу, детали оставляем в логе ядра. */
static int poll_command_error(const char* reason, char** result) {
struct strbuf out = strbuf_new();
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: command rejected reason=%s", reason);
if (strbuf_addf(&out, "{\"error\":") >= 0 && strbuf_add_json_string(&out, reason) == 0 && strbuf_addf(&out, "}") >= 0)
*result = strbuf_detach(&out);
strbuf_free(&out); return -1;
}
/* Общий командный API исключает отдельные правила голосования в каждом интерфейсе. */
int chat_feedback_command(struct UTUN_INSTANCE* inst, const char* command, char** result) {
if (result) *result = NULL;
if (!inst || !inst->topo_sqlite_db || !command || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid command arguments"); return -1;
}
sqlite3_stmt* valid = NULL;
if (strlen(command) > 16384 || sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT json_valid(?1)", -1, &valid, NULL) != SQLITE_OK)
return poll_command_error("Invalid command size", result);
sqlite3_bind_text(valid, 1, command, -1, SQLITE_STATIC);
int ok = sqlite3_step(valid) == SQLITE_ROW && sqlite3_column_int(valid, 0) == 1; sqlite3_finalize(valid);
if (!ok) return poll_command_error("Malformed JSON", result);
char cmd[32], channel[21], hex[65]; uint8_t message_id[32];
if (poll_field_text(inst, command, "$.cmd", cmd, sizeof(cmd), 0) < 0) return poll_command_error("Missing command", result);
if (!strcmp(cmd, "reaction_catalog")) {
struct strbuf out = strbuf_new(); int failed = strbuf_addf(&out, "{\"catalog\":[") < 0;
for (unsigned i = 0; i < CHAT_REACTION_COUNT && !failed; i++) {
failed = strbuf_addf(&out, "%s{\"id\":%u,\"emoji\":", i ? "," : "", i) < 0
|| strbuf_add_json_string(&out, chat_reaction_emoji(i)) < 0 || strbuf_addf(&out, "}") < 0;
}
if (!failed && strbuf_addf(&out, "]}") >= 0) *result = strbuf_detach(&out);
strbuf_free(&out); return *result ? 0 : poll_command_error("Out of memory", result);
}
if (poll_field_text(inst, command, "$.ch", channel, sizeof(channel), 0) < 0 || poll_channel(inst, channel, NULL) < 0)
return poll_command_error("Invalid channel", result);
if (!strcmp(cmd, "poll_create")) {
struct chat_poll poll = {0}; char mode[16];
if (poll_field_text(inst, command, "$.question", poll.question, sizeof(poll.question), 0) < 0
|| poll_field_text(inst, command, "$.mode", mode, sizeof(mode), 0) < 0) return poll_command_error("Invalid poll text or mode", result);
poll.mode = !strcmp(mode, "single") ? CHAT_POLL_SINGLE : !strcmp(mode, "multiple") ? CHAT_POLL_MULTIPLE : 0;
int custom = poll_field_bool(inst, command, "$.allow_custom_answer", 0);
if (custom < 0) return poll_command_error("Invalid custom answer option", result);
poll.allow_custom_answer = custom;
sqlite3_stmt* field = poll_field(inst, command, "$.options"); if (!field) return poll_command_error("Invalid options", result);
const char* type = (const char*)sqlite3_column_text(field, 0); ok = type && !strcmp(type, "array"); sqlite3_finalize(field);
if (!ok) return poll_command_error("Options must be an array", result);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT type,value FROM json_each(?1,'$.options')", -1, &st, NULL) != SQLITE_OK) {
poll_db_error(inst, "options"); return poll_command_error("Cannot read options", result);
}
sqlite3_bind_text(st, 1, command, -1, SQLITE_STATIC);
int step;
while ((step = sqlite3_step(st)) == SQLITE_ROW) {
const char* value_type = (const char*)sqlite3_column_text(st, 0), *text = (const char*)sqlite3_column_text(st, 1);
int len = sqlite3_column_bytes(st, 1);
if (poll.option_count == CHAT_POLL_OPTIONS_MAX || !value_type || strcmp(value_type, "text") || !text
|| len <= 0 || len > CHAT_POLL_OPTION_MAX || strlen(text) != (size_t)len) { ok = 0; break; }
memcpy(poll.options[poll.option_count++], text, len + 1);
}
sqlite3_finalize(st);
if (!ok || step != SQLITE_DONE || chat_poll_create(inst, channel, &poll, message_id) < 0)
return poll_command_error("Invalid poll or publication failed", result);
return chat_feedback_snapshot(inst, channel, message_id, result);
}
if (poll_field_text(inst, command, "$.message_id", hex, sizeof(hex), 0) < 0 || strlen(hex) != 64
|| sc_hex_to_binary(hex, message_id, 32) != 0) return poll_command_error("Invalid message ID", result);
if (!strcmp(cmd, "feedback")) return chat_feedback_snapshot(inst, channel, message_id, result);
struct chat_poll poll; int is_poll, target = poll_target(inst, channel, message_id, &poll, &is_poll);
if (target != 0) return poll_command_error("Message is not available", result);
uint8_t data[CHAT_FEEDBACK_DATA_MAX]; int bytes, kind;
if (!strcmp(cmd, "vote")) {
if (!is_poll) return poll_command_error("Message is not a poll", result);
struct chat_poll_answer answer = {0};
int n = poll_field_ids(inst, command, "$.choices", answer.choices, sizeof(answer.choices));
if (n < 0 || poll_field_text(inst, command, "$.custom", answer.custom, sizeof(answer.custom), 1) < 0)
return poll_command_error("Invalid answer", result);
answer.count = n; bytes = chat_answer_encode(&answer, data, sizeof(data)); kind = CHAT_FEEDBACK_ANSWER;
if (!poll_answer_allowed(&poll, &answer)) return poll_command_error("Answer violates poll rules", result);
} else if (!strcmp(cmd, "react")) {
uint8_t ids[CHAT_REACTION_SELECTED_MAX + 1]; size_t count = 0;
int clear = poll_field_bool(inst, command, "$.clear", 0); if (clear < 0) return poll_command_error("Invalid clear flag", result);
if (!clear) {
struct chat_feedback_record old;
int found = chat_feedback_get(inst, channel, message_id, inst->node_id, CHAT_FEEDBACK_REACTION, &old);
if (found < 0 || (found && chat_reactions_decode(old.data, old.data_len, ids, &count) < 0))
return poll_command_error("Cannot read reactions", result);
sqlite3_stmt* st = poll_field(inst, command, "$.emoji"); if (!st) return poll_command_error("Invalid emoji", result);
const char* type = (const char*)sqlite3_column_text(st, 0); sqlite3_int64 emoji = sqlite3_column_int64(st, 1);
ok = type && !strcmp(type, "integer") && emoji >= 0 && emoji < CHAT_REACTION_COUNT; sqlite3_finalize(st);
if (!ok) return poll_command_error("Invalid emoji", result);
size_t at = 0; while (at < count && ids[at] != emoji) at++;
int on = poll_field_bool(inst, command, "$.on", at == count);
if (on < 0) return poll_command_error("Invalid reaction state", result);
if (on && at == count) {
if (count == CHAT_REACTION_SELECTED_MAX) return poll_command_error("Too many reactions", result);
ids[count++] = emoji;
} else if (!on && at < count) { memmove(ids + at, ids + at + 1, count - at - 1); count--; }
}
bytes = chat_reactions_encode(ids, count, data, sizeof(data)); kind = CHAT_FEEDBACK_REACTION;
} else return poll_command_error("Unknown interaction command", result);
if (bytes < 0 || chat_feedback_set(inst, channel, message_id, kind, data, bytes, NULL) < 0)
return poll_command_error("Cannot save state", result);
return chat_feedback_snapshot(inst, channel, message_id, result);
}
/* Ответ UI асинхронен: успех/ошибка и исходный request_id всегда идут вместе. */
void chat_feedback_command_trampoline(void* arg) {
struct chat_feedback_request* req = arg;
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: missing command request"); return; }
char* json = NULL; int rc = chat_feedback_command(req->inst, (const char*)(req + 1), &json);
if (!json) json = u_strdup("{\"error\":\"Interaction failed\"}");
size_t bytes = json ? strlen(json) : 0; uint8_t* event = u_malloc(bytes + 9);
if (event) {
memcpy(event, &req->request_id, 8); event[8] = rc == 0;
if (bytes) memcpy(event + 9, json, bytes);
chat_event_post(req->inst, CHAT_EVT_FEEDBACK_RESULT, event, bytes + 9); u_free(event);
} else DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "poll: command result allocation failed");
u_free(json); u_free(req);
}

2
src/chat/chat_poll.h

@ -57,6 +57,8 @@ int chat_feedback_snapshot(struct UTUN_INSTANCE* inst, const char* channel, cons
* vote: choices:[0..31], custom:string; пустые значения отменяют голос.
* react: emoji:0..15, on:boolean необязателен (без него переключить), clear:true снимает все. */
int chat_feedback_command(struct UTUN_INSTANCE* inst, const char* command, char** result);
/* Проверка отложенных ответов и диагностика при получении сообщения/нового состояния. */
void chat_feedback_check_message(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32]);
/* Команда из UI: req и следующая за ней NUL-строка выделены одним блоком через u_malloc.
* Результат возвращается событием CHAT_EVT_FEEDBACK_RESULT и включает request_id. */
struct chat_feedback_request { struct UTUN_INSTANCE* inst; uint64_t request_id; };

Loading…
Cancel
Save