4 changed files with 370 additions and 0 deletions
@ -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); |
||||
} |
||||
Loading…
Reference in new issue