Browse Source

Define canonical poll, answer and reaction formats

master
evgeny 5 days ago
parent
commit
d0aa60fe1a
  1. 68
      src/chat/chat_poll.h
  2. 151
      src/chat/chat_poll_codec.c

68
src/chat/chat_poll.h

@ -0,0 +1,68 @@
/* Общий формат опросов, ответов и реакций. Подписанное poll-сообщение неизменяемо.
* single допускает один пункт ИЛИ свой ответ; multiple — пункты и необязательный свой ответ.
* Пустой ответ/набор реакций кодируется нулевой длиной и сохраняется с новым ver.
* Все сетевые/DB API вызываются в uasync; кодеки не зависят от экземпляра.
* JSON-снимки и результаты команд выделяются через u_malloc, освобождаются u_free. */
#ifndef CHAT_POLL_H
#define CHAT_POLL_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
struct UTUN_INSTANCE;
#define CHAT_POLL_SINGLE 1
#define CHAT_POLL_MULTIPLE 2
#define CHAT_POLL_OPTIONS_MAX 32
#define CHAT_POLL_QUESTION_MAX 512
#define CHAT_POLL_OPTION_MAX 256
#define CHAT_POLL_CUSTOM_MAX 1024
#define CHAT_POLL_WIRE_MAX 2048
#define CHAT_REACTION_COUNT 16
#define CHAT_REACTION_SELECTED_MAX 8
struct chat_poll {
uint8_t mode, allow_custom_answer, option_count;
char question[CHAT_POLL_QUESTION_MAX + 1];
char options[CHAT_POLL_OPTIONS_MAX][CHAT_POLL_OPTION_MAX + 1];
};
struct chat_poll_answer {
uint8_t count, choices[CHAT_POLL_OPTIONS_MAX];
char custom[CHAT_POLL_CUSTOM_MAX + 1];
};
/* Кодеки возвращают длину/0, либо -1. Decode требует точное каноническое представление. */
int chat_poll_encode(const struct chat_poll* poll, uint8_t* out, size_t capacity);
int chat_poll_decode(const uint8_t* data, size_t len, struct chat_poll* poll);
int chat_answer_encode(const struct chat_poll_answer* answer, uint8_t* out, size_t capacity);
int chat_answer_decode(const uint8_t* data, size_t len, struct chat_poll_answer* answer);
int chat_reactions_encode(const uint8_t* ids, size_t count, uint8_t* out, size_t capacity);
int chat_reactions_decode(const uint8_t* data, size_t len, uint8_t ids[CHAT_REACTION_SELECTED_MAX], size_t* count);
/* Постоянный общий каталог: существующие номера не менять. NULL при неверном номере. */
const char* chat_reaction_emoji(unsigned id);
/* Структурная проверка data, независимая от наличия исходного сообщения. 0/-1. */
int chat_feedback_validate_data(uint8_t kind, const uint8_t* data, size_t len);
/* Публикация подписанного poll: 0/-1, out_id — общий ID сохранённого сообщения. */
int chat_poll_create(struct UTUN_INSTANCE* inst, const char* channel, const struct chat_poll* poll, uint8_t out_id[32]);
/* Снимок сообщения и результатов: pending, poll, reactions, mine и ответы. 0/-1. */
int chat_feedback_snapshot(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message_id[32], char** json);
/* JSON-команды poll_create, vote, react, feedback, reaction_catalog. 0/-1.
* Ошибка возвращает JSON с error; успех — снимок или каталог.
* 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);
/* Команда из UI: req и следующая за ней NUL-строка выделены одним блоком через u_malloc.
* Результат возвращается событием CHAT_EVT_FEEDBACK_RESULT и включает request_id. */
struct chat_feedback_request { struct UTUN_INSTANCE* inst; uint64_t request_id; };
void chat_feedback_command_trampoline(void* arg);
#ifdef __cplusplus
}
#endif
#endif

151
src/chat/chat_poll_codec.c

@ -0,0 +1,151 @@
#include "chat_poll.h"
#include "chat_feedback.h"
#include "../../lib/debug_config.h"
#include <string.h>
#include <stdlib.h>
static const char* const reaction_catalog[CHAT_REACTION_COUNT] = {
"👍", "👎", "❤️", "😂", "😮", "😢", "😡", "🎉", "🔥", "🤔", "💯", "👏", "🙏", "😍", "✨", "🤖"
};
const char* chat_reaction_emoji(unsigned id) { return id < CHAT_REACTION_COUNT ? reaction_catalog[id] : NULL; }
/* Проверяет UTF-8, исключая NUL, неверные/избыточные кодировки и surrogate. */
static int poll_utf8(const uint8_t* s, size_t len) {
for (size_t i = 0; i < len;) {
unsigned c = s[i++], value, bytes, minimum;
if (c < 128) { if (!c || (c < 32 && c != 9 && c != 10 && c != 13)) return -1; continue; }
if (c >= 0xc2 && c <= 0xdf) { value = c & 31; bytes = 1; minimum = 0x80; }
else if (c >= 0xe0 && c <= 0xef) { value = c & 15; bytes = 2; minimum = 0x800; }
else if (c >= 0xf0 && c <= 0xf4) { value = c & 7; bytes = 3; minimum = 0x10000; }
else return -1;
if (bytes > len - i) return -1;
while (bytes--) { c = s[i++]; if ((c & 0xc0) != 0x80) return -1; value = (value << 6) | (c & 63); }
if (value < minimum || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) return -1;
}
return 0;
}
static int poll_error(const char* reason) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "poll: invalid data reason=%s", reason); return -1;
}
/* Проверка строки ограничена ёмкостью структуры; нет strlen за её границей. */
static int poll_string_size(const char* s, size_t capacity) {
const char* end = memchr(s, 0, capacity);
if (!end || poll_utf8((const uint8_t*)s, (size_t)(end - s)) < 0) return poll_error("text is not bounded UTF-8");
return (int)(end - s);
}
/* Текст на проводе — BE16 длины и байты UTF-8. */
static int poll_put_string(const char* s, size_t capacity, uint8_t* out, size_t available) {
int len = poll_string_size(s, capacity);
if (len < 0) return -1;
if (!len || available < (size_t)len + 2) return poll_error("empty text or poll exceeds size limit");
out[0] = (unsigned)len >> 8; out[1] = len & 255; memcpy(out + 2, s, len);
return len + 2;
}
static int poll_get_string(const uint8_t* data, size_t len, char* out, size_t capacity) {
if (len < 2) return poll_error("truncated text length");
size_t bytes = ((size_t)data[0] << 8) | data[1];
if (!bytes || bytes >= capacity || bytes > len - 2 || poll_utf8(data + 2, bytes) < 0)
return poll_error("invalid text length or UTF-8");
memcpy(out, data + 2, bytes); out[bytes] = 0; return (int)bytes + 2;
}
/* Формат poll v1: version, mode, flags, option_count, question, options. */
int chat_poll_encode(const struct chat_poll* poll, uint8_t* out, size_t capacity) {
if (!poll || !out || capacity < 6 || (poll->mode != CHAT_POLL_SINGLE && poll->mode != CHAT_POLL_MULTIPLE)
|| poll->allow_custom_answer > 1 || poll->option_count < 2 || poll->option_count > CHAT_POLL_OPTIONS_MAX)
return poll_error("poll mode, flags or option count");
if (capacity > CHAT_POLL_WIRE_MAX) capacity = CHAT_POLL_WIRE_MAX;
out[0] = 1; out[1] = poll->mode; out[2] = poll->allow_custom_answer; out[3] = poll->option_count;
int bytes = poll_put_string(poll->question, sizeof(poll->question), out + 4, capacity - 4);
if (bytes < 0) return -1;
size_t off = 4 + bytes;
for (unsigned i = 0; i < poll->option_count; i++) {
bytes = poll_put_string(poll->options[i], sizeof(poll->options[i]), out + off, capacity - off);
if (bytes < 0) return -1;
for (unsigned j = 0; j < i; j++) if (!strcmp(poll->options[i], poll->options[j])) return poll_error("duplicate option");
off += bytes;
}
return (int)off;
}
int chat_poll_decode(const uint8_t* data, size_t len, struct chat_poll* poll) {
if (!data || !poll || len < 6 || len > CHAT_POLL_WIRE_MAX || data[0] != 1
|| (data[1] != CHAT_POLL_SINGLE && data[1] != CHAT_POLL_MULTIPLE) || data[2] > 1
|| data[3] < 2 || data[3] > CHAT_POLL_OPTIONS_MAX) return poll_error("poll header");
memset(poll, 0, sizeof(*poll)); poll->mode = data[1]; poll->allow_custom_answer = data[2]; poll->option_count = data[3];
int bytes = poll_get_string(data + 4, len - 4, poll->question, sizeof(poll->question));
if (bytes < 0) return -1;
size_t off = 4 + bytes;
for (unsigned i = 0; i < poll->option_count; i++) {
bytes = poll_get_string(data + off, len - off, poll->options[i], sizeof(poll->options[i]));
if (bytes < 0) return -1;
for (unsigned j = 0; j < i; j++) if (!strcmp(poll->options[i], poll->options[j])) return poll_error("duplicate option");
off += bytes;
}
return off == len ? 0 : poll_error("trailing poll bytes");
}
static int poll_compare_id(const void* a, const void* b) { return (int)*(const uint8_t*)a - *(const uint8_t*)b; }
/* Ответ: count, отсортированные номера, BE16 длины собственного текста, UTF-8. */
int chat_answer_encode(const struct chat_poll_answer* answer, uint8_t* out, size_t capacity) {
if (!answer || !out || answer->count > CHAT_POLL_OPTIONS_MAX) return poll_error("answer count");
int bytes = poll_string_size(answer->custom, sizeof(answer->custom));
if (bytes < 0) return -1;
if (!answer->count && !bytes) return 0;
size_t len = 3 + answer->count + bytes;
if (len > capacity) return poll_error("answer capacity");
out[0] = answer->count; memcpy(out + 1, answer->choices, answer->count);
qsort(out + 1, answer->count, 1, poll_compare_id);
for (unsigned i = 1; i <= answer->count; i++)
if (out[i] >= CHAT_POLL_OPTIONS_MAX || (i > 1 && out[i] == out[i - 1])) return poll_error("answer choices");
out[1 + answer->count] = (unsigned)bytes >> 8; out[2 + answer->count] = bytes & 255;
memcpy(out + 3 + answer->count, answer->custom, bytes); return (int)len;
}
int chat_answer_decode(const uint8_t* data, size_t len, struct chat_poll_answer* answer) {
if (!answer || (len && !data)) return poll_error("answer arguments");
memset(answer, 0, sizeof(*answer)); if (!len) return 0;
if (len < 3 || data[0] > CHAT_POLL_OPTIONS_MAX || len < (size_t)data[0] + 3) return poll_error("answer header");
size_t n = data[0], bytes = ((size_t)data[1 + n] << 8) | data[2 + n];
if ((!n && !bytes) || bytes > CHAT_POLL_CUSTOM_MAX || len != 3 + n + bytes || poll_utf8(data + 3 + n, bytes) < 0)
return poll_error("answer text or trailing bytes");
for (size_t i = 1; i <= n; i++)
if (data[i] >= CHAT_POLL_OPTIONS_MAX || (i > 1 && data[i] <= data[i - 1])) return poll_error("noncanonical choices");
answer->count = n; memcpy(answer->choices, data + 1, n); memcpy(answer->custom, data + 3 + n, bytes); return 0;
}
int chat_reactions_encode(const uint8_t* ids, size_t count, uint8_t* out, size_t capacity) {
if (!out || (count && !ids) || count > CHAT_REACTION_SELECTED_MAX) return poll_error("reaction count");
if (!count) return 0;
if (capacity < count + 1) return poll_error("reaction capacity");
out[0] = count; memcpy(out + 1, ids, count); qsort(out + 1, count, 1, poll_compare_id);
for (size_t i = 1; i <= count; i++)
if (out[i] >= CHAT_REACTION_COUNT || (i > 1 && out[i] == out[i - 1])) return poll_error("reaction ids");
return (int)count + 1;
}
int chat_reactions_decode(const uint8_t* data, size_t len, uint8_t ids[CHAT_REACTION_SELECTED_MAX], size_t* count) {
if (!count || !ids || (len && !data)) return poll_error("reaction arguments");
*count = 0; if (!len) return 0;
if (!data[0] || data[0] > CHAT_REACTION_SELECTED_MAX || len != (size_t)data[0] + 1) return poll_error("reaction length");
for (size_t i = 1; i < len; i++)
if (data[i] >= CHAT_REACTION_COUNT || (i > 1 && data[i] <= data[i - 1])) return poll_error("noncanonical reactions");
*count = data[0]; memcpy(ids, data + 1, *count); return 0;
}
int chat_feedback_validate_data(uint8_t kind, const uint8_t* data, size_t len) {
if (kind == CHAT_FEEDBACK_REACTION) {
uint8_t ids[CHAT_REACTION_SELECTED_MAX]; size_t count;
return chat_reactions_decode(data, len, ids, &count);
}
if (kind == CHAT_FEEDBACK_ANSWER) {
struct chat_poll_answer answer; return chat_answer_decode(data, len, &answer);
}
return poll_error("feedback kind");
}
Loading…
Cancel
Save