Files
utun2/src/chat/chat_core.c
T

984 lines
55 KiB
C

/*
* chat_core.c — жизненный цикл чата: init, destroy, per-instance состояние (inst->chat_core)
*
* Под-модули (каждый в своём .c):
* chat_msg.c — отправка сообщений, DB-операции для chat_sync
* chat_channel.c — создание/подключение каналов, db_sync instance
* chat_profile.c — имя узла, адреса, ui_state
* chat_status.c — сбор статуса NTP+connections
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
*/
#include "chat_core_priv.h"
#include "chat_whisper.h"
#include "chat_attachment.h"
#include "group_avatar.h"
#include "chat_event.h"
#include "chat_setting.h"
#include "chat_member.h"
#include "member_sync.h"
#include "chat_sync.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../transport_layer/secure_channel.h"
#include "../media_delivery/media_index.h"
#include "../../lib/mem.h"
#include "../../lib/strbuf.h"
#include "../../lib/json_flat.h"
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include "../../lib/platform_compat.h"
/* ─── хелперы ─── */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) {
size_t i = 0;
while (*ch_id && i < out_sz - 1) {
char c = *ch_id++;
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9') || c == '_')
out[i++] = c;
else
out[i++] = '_';
}
out[i] = '\0';
}
void msg_table_name(const char* ch_id, char* buf, size_t sz) {
char san[64]; sanitize_ch_id(ch_id, san, sizeof(san));
snprintf(buf, sz, "msg_%s", san);
}
void peers_table_name(const char* ch_id, char* buf, size_t sz) {
char san[64]; sanitize_ch_id(ch_id, san, sizeof(san));
snprintf(buf, sz, "peers_%s", san);
}
static int db_exec(struct chat_core_ctx* cc, const char* sql) {
char* err = NULL;
int rc = sqlite3_exec(cc->db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: SQL rc=%d (%s) statement=%.120s: %s", CC_ID,
rc, sqlite3_errstr(rc), sql, err ? err : sqlite3_errmsg(cc->db));
sqlite3_free(err);
}
return rc;
}
/* ─── db_sync registry ─── */
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id) {
uint64_t channel = chat_channel_id(ch_id);
if (!inst || !inst->db_sync || !channel) return NULL;
return db_sync_instance_find(inst, channel);
}
/* ─── жизненный цикл ─── */
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path || !inst->core_started || !inst->topo_sqlite_db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: running core,shared DB,and path required"); return -1;
}
if (inst->chat_core) {
if (inst->chat_core->initialized && inst->chat_core->db == inst->topo_sqlite_db && !strcmp(inst->chat_core->db_path, db_path)) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: incomplete context or different DB/path already exists"); return -1;
}
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
if (strlen(db_path) >= sizeof(cc->db_path)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: database path exceeds capacity=%zu", sizeof(cc->db_path)); u_free(cc); return -1;
}
inst->chat_core = cc;
memcpy(cc->db_path, db_path, strlen(db_path) + 1); cc->inst = inst; cc->my_node_id = inst->node_id;
cc->db = inst->topo_sqlite_db;
const char* stage = "transaction admission"; int transaction = 0, subscribed = 0, rows = 0, columns = 0;
char** channels = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat init: using core SQLite db=%p", (void*)cc->db);
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: external transaction rejected"); goto fail;
}
stage = "begin";
if (db_exec(cc, "BEGIN IMMEDIATE /* chat init */") != SQLITE_OK) goto fail;
transaction = 1; stage = "avatar schema";
if (group_avatar_schema_init(cc->db) < 0) goto fail;
stage = "media schema"; if (media_index_init(cc->db)) goto fail;
sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst);
if (now_sec <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: invalid identity time=%lld", (long long)now_sec); goto fail; }
/* Схема нужна до первой записи идентичности, в том числе для новой пустой БД. */
stage = "chat schema";
if (db_exec(cc,
"CREATE TABLE IF NOT EXISTS local_identity ("
" id INTEGER PRIMARY KEY CHECK (id = 1),"
" node_id INTEGER NOT NULL UNIQUE,"
" name TEXT NOT NULL,"
" x25519_pubkey BLOB NOT NULL,"
" x25519_privkey BLOB,"
" ed25519_pubkey BLOB,"
" created_at INTEGER DEFAULT (unixepoch()),"
" updated_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE TABLE IF NOT EXISTS accounts ("
" node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id),"
" display_name TEXT NOT NULL,"
" avatar_color TEXT DEFAULT '#4A90E2',"
" avatar_letter TEXT NOT NULL,"
" is_contact INTEGER DEFAULT 1,"
" created_at INTEGER DEFAULT (unixepoch())"
");"
"CREATE TABLE IF NOT EXISTS ui_state ("
" key TEXT PRIMARY KEY,"
" value TEXT"
");"
) != SQLITE_OK) goto fail;
/* Идентичность публикуется в GUI только после успешной записи узла и локального профиля. */
stage = "local identity";
{
const char* my_name = inst->name[0] ? inst->name : "Me";
if (topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey, (uint64_t)now_sec, (time_t)now_sec) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local node registration failed", CC_ID); goto fail;
}
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(cc->db,
"INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)"
" VALUES(1,?,?,?,?,?,?)", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 5, now_sec);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 6, now_sec);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_DONE && sqlite3_changes(cc->db) != 1) rc = SQLITE_CONSTRAINT;
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local identity write rc=%d: %s", CC_ID, rc, sqlite3_errmsg(cc->db));
int final = sqlite3_finalize(st);
if (final != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local identity finalize rc=%d: %s", CC_ID, final, sqlite3_errmsg(cc->db));
if (rc != SQLITE_DONE || final != SQLITE_OK) goto fail;
}
/* store media_base for block_req handler */
stage = "media base";
{
char mbase[MEDIA_BASE_MAX];
const char* ls = strrchr(db_path, '/');
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", ls == db_path ? 1 : (int)(ls - db_path), db_path);
else snprintf(mbase, sizeof(mbase), ".");
sqlite3_stmt* stored = NULL;
int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &stored, NULL);
if (rc == SQLITE_OK) rc = sqlite3_step(stored);
int finish = stored ? sqlite3_finalize(stored) : SQLITE_OK;
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || finish != SQLITE_OK ||
(rc == SQLITE_ROW ? media_index_get_base(cc->db, mbase) : media_index_set_base(cc->db, mbase))) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media_base initialization failed rc=%d: %s", rc, sqlite3_errmsg(cc->db));
goto fail;
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat init: prepared media_base=%s", mbase);
}
/* Список отделён от SQLite до подготовки runtime и проверен до commit метаданных. */
stage = "channel catalog";
{
int rc = sqlite3_get_table(cc->db, "SELECT channel_id,length(CAST(channel_id AS BLOB)) FROM channels ORDER BY created_at ASC",
&channels, &rows, &columns, NULL);
if (rc != SQLITE_OK || (rows && columns != 2)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: catalog rc=%d rows=%d columns=%d: %s", rc, rows, columns, sqlite3_errmsg(cc->db)); goto fail;
}
for (int i = 0; i < rows; i++) {
const char *ch = channels[(size_t)(i + 1) * columns], *bytes = channels[(size_t)(i + 1) * columns + 1];
if (!chat_channel_id(ch) || !bytes || strlen(ch) != strtoul(bytes, NULL, 10)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: invalid stored channel row=%d", i); goto fail;
}
}
}
stage = "member subscriptions"; if (chat_member_init(inst)) goto fail;
subscribed = 1; stage = "commit"; if (db_exec(cc, "COMMIT /* chat init */") != SQLITE_OK) goto fail;
transaction = 0; cc->initialized = 1;
if (inst->config) memcpy(&inst->chat_settings, &inst->config->global.chat_settings, sizeof(inst->chat_settings));
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat init: metadata committed node=%016llx channels=%d", (unsigned long long)cc->my_node_id, rows);
stage = "channel activation";
for (int i = 0; i < rows; i++) {
const char* ch = channels[(size_t)(i + 1) * columns];
if (chat_core_ensure_channel_ready(inst, ch)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: committed channel activation failed ch=%s;service restart required", ch); goto fail;
}
}
sqlite3_free_table(channels); channels = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: initialized, db=%s node_id=0x%016llx",
CC_ID, db_path, (unsigned long long)cc->my_node_id);
{ uint8_t nid[8]; memcpy(nid, &cc->my_node_id, 8); chat_event_post(inst, CHAT_EVT_MY_NODE_ID, nid, 8); }
chat_event_post(inst, CHAT_EVT_DB_READY, NULL, 0);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: failed stage=%s ready=%d: %s", stage, cc->initialized, sqlite3_errmsg(cc->db));
sqlite3_free_table(channels);
if (transaction && !sqlite3_get_autocommit(cc->db)) {
if (db_exec(cc, "ROLLBACK") != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: rollback failed");
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat init: transaction remains open;requesting owner stop/DB close"); utun_instance_stop(inst);
}
}
if (cc->initialized) { chat_core_destroy(inst); return -1; }
if (subscribed) chat_member_destroy(inst);
inst->chat_core = NULL; u_free(cc); return -1;
}
void chat_core_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized) return;
cc->initialized = 0;
chat_whisper_destroy(inst);
chat_member_destroy(inst);
chat_attachment_destroy(inst);
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
inst->chat_core = NULL;
u_free(cc);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
}
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->initialized : 0; }
/* ─── chat settings (GUI → uasync) ─── */
int chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
int rc = chat_setting_set(inst, name, value);
if (!rc && !strcmp(name, "group_autoconnect")) chat_core_apply_group_autoconnect(inst);
return rc;
}
void chat_core_set_setting_trampoline(void* arg) {
struct chat_setting_arg* a = (struct chat_setting_arg*)arg;
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat setting: null request"); return; }
const char* end = memchr(a->data, 0, sizeof(a->data));
if (!end || end == a->data + sizeof(a->data) - 1 || !memchr(end + 1, 0, sizeof(a->data) - (size_t)(end + 1 - a->data)))
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "chat setting: unterminated name/value request");
else chat_core_set_setting(a->inst, a->data, end + 1);
u_free(arg);
}
/* ─── per-channel «play sound on new message» ─── */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !chat_channel_id(ch_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: invalid read context/channel"); return -1;
}
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL), on = 1;
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
int type = sqlite3_column_type(st, 0);
const char* v = (const char*)sqlite3_column_text(st, 0);
if (type != SQLITE_TEXT || sqlite3_column_bytes(st, 0) != 1 || !v || (*v != '0' && *v != '1')) rc = SQLITE_CORRUPT;
else on = *v == '1';
}
int final = sqlite3_finalize(st);
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: read ch=%s rc=%d finalize=%d: %s", ch_id, rc, final, sqlite3_errmsg(cc->db)); return -1;
}
return on;
}
int chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
if (!chat_channel_id(ch_id)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: invalid channel"); return -1; }
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
char val[2]; snprintf(val, sizeof(val), "%d", on ? 1 : 0);
if (chat_core_save_ui_state(inst, key, val)) return -1;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: sound_on_message ch=%s = %d", CC_ID, ch_id, on ? 1 : 0);
chat_sync_broadcast_sound_setting(inst, ch_id, on ? 1 : 0);
return 0;
}
void chat_core_set_sound_on_message_trampoline(void* arg) {
struct chat_sound_arg* a = (struct chat_sound_arg*)arg;
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: null request"); return; }
if (!memchr(a->ch_id, 0, sizeof(a->ch_id))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sound setting: unterminated channel request");
else chat_core_set_sound_on_message(a->inst, a->ch_id, a->on);
u_free(arg);
}
/* ─── headless API ─── */
/* Результат чтения публикуется целиком;даже неверные аргументы не оставляют старый JSON. */
static struct chat_core_ctx* json_context(struct UTUN_INSTANCE* inst, char* buf, size_t size, size_t* out_len) {
if (out_len) *out_len = 0;
if (buf && size) buf[0] = 0;
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !buf || size < 3 || !out_len) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat JSON: invalid context/output capacity=%zu", size); return NULL;
}
return cc;
}
static int json_copy(const struct strbuf* json, char* buf, size_t capacity, size_t* out_len) {
if (json->len >= capacity) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat JSON: output exceeds capacity bytes=%zu capacity=%zu", json->len, capacity); return -1;
}
memcpy(buf, strbuf_str(json), json->len + 1); *out_len = json->len; return 0;
}
/* Не превращать NOMEM или скрытый NUL SQLite TEXT в пустую/усечённую строку. */
static int json_read_text(sqlite3_stmt* st, int column, const char** out) {
*out = NULL; int type = sqlite3_column_type(st, column);
if (type == SQLITE_NULL) return SQLITE_OK;
if (type != SQLITE_TEXT) return SQLITE_CORRUPT;
const char* text = (const char*)sqlite3_column_text(st, column);
if (!text) return SQLITE_NOMEM;
if (strlen(text) != (size_t)sqlite3_column_bytes(st, column)) return SQLITE_CORRUPT;
*out = text; return SQLITE_OK;
}
int chat_json_strings(sqlite3* db, const char* text, size_t limit,
int (*visit)(const char* key, const char* value, void* arg), void* arg) {
if (!db || !text || limit > INT_MAX || strnlen(text, limit + 1) > limit) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: invalid arguments/size limit=%zu", limit); return -1;
}
sqlite3_stmt* st = NULL; const char* stage = "validate prepare";
int rc = sqlite3_prepare_v2(db, "SELECT json_valid(?1),CASE WHEN json_valid(?1) THEN json_type(?1) END", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_ROW) goto error;
const char* type = NULL; rc = json_read_text(st, 1, &type); if (rc != SQLITE_OK) goto error;
if (!sqlite3_column_int(st, 0) || !type || strcmp(type, "object")) { stage = "not a valid object"; rc = SQLITE_CONSTRAINT; goto error; }
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "field schema";
rc = sqlite3_prepare_v2(db, "SELECT count(*),count(DISTINCT key),coalesce(sum(type<>'text'),0) FROM json_each(?1)", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_ROW) goto error;
if (sqlite3_column_int64(st, 0) != sqlite3_column_int64(st, 1) || sqlite3_column_int64(st, 2)) {
stage = "duplicate/nonstring fields"; rc = SQLITE_CONSTRAINT; goto error;
}
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "fields";
rc = sqlite3_prepare_v2(db, "SELECT key,value FROM json_each(?1)", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto error;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char *key, *value; rc = json_read_text(st, 0, &key); if (rc == SQLITE_OK) rc = json_read_text(st, 1, &value);
if (rc != SQLITE_OK) goto error;
if (!key || !value || (visit && visit(key, value, arg))) { stage = "field callback"; rc = SQLITE_CONSTRAINT; goto error; }
}
if (rc != SQLITE_DONE) goto error;
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: cleanup finalize rc=%d", rc);
return -1;
}
/* Сохранённый канал может ещё не иметь history-таблицы при отложенной runtime-подготовке. */
static int json_message_count(sqlite3* db, const char* channel, sqlite3_int64* count) {
char table[80], sql[128]; msg_table_name(channel, table, sizeof(table)); sqlite3_stmt* st = NULL;
const char* stage = "history table lookup";
int rc = sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, table, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
int found = rc == SQLITE_ROW;
if (rc != SQLITE_ROW && rc != SQLITE_DONE) goto error;
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
if (!found) {
*count = 0; DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat JSON: history not prepared ch=%s", channel); return 0;
}
stage = "history count"; snprintf(sql, sizeof(sql), "SELECT count(*) FROM \"%s\"", table);
rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_ROW) goto error;
sqlite3_int64 result = sqlite3_column_int64(st, 0);
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
*count = result; return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat JSON: ch=%s stage=%s rc=%d: %s", channel, stage, rc, sqlite3_errmsg(db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat JSON: count cleanup finalize rc=%d", rc);
return -1;
}
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = json_context(inst, buf, buf_size, out_len); if (!cc) return -1;
struct strbuf json = strbuf_new(); sqlite3_stmt *st = NULL, *peers = NULL; const char* stage = "prepare";
int rc = sqlite3_prepare_v2(cc->db, "SELECT c.channel_id,c.name,c.owner_node_id,n.name FROM channels c"
" LEFT JOIN nodes n ON n.node_id=c.owner_node_id ORDER BY c.created_at,c.channel_id", -1, &st, NULL);
if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM; if (strbuf_addf(&json, "[") < 0) goto error;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char *channel, *name, *owner_name; stage = "channel text";
rc = json_read_text(st, 0, &channel);
if (rc == SQLITE_OK) rc = json_read_text(st, 1, &name);
if (rc == SQLITE_OK) rc = json_read_text(st, 3, &owner_name);
if (rc != SQLITE_OK) goto error;
uint64_t owner = (uint64_t)sqlite3_column_int64(st, 2);
if (!chat_channel_id(channel)) { stage = "invalid stored channel ID"; rc = SQLITE_CORRUPT; goto error; }
sqlite3_int64 messages, count = 0, online = 0;
stage = "history count";
if (json_message_count(cc->db, channel, &messages)) { rc = SQLITE_ERROR; goto error; }
char table[80], sql[160]; peers_table_name(channel, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" WHERE deleted=0", table);
stage = "members"; rc = sqlite3_prepare_v2(cc->db, sql, -1, &peers, NULL); if (rc != SQLITE_OK) goto error;
while ((rc = sqlite3_step(peers)) == SQLITE_ROW) {
count++; online += chat_core_member_online(inst, channel, (uint64_t)sqlite3_column_int64(peers, 0));
}
if (rc != SQLITE_DONE) goto error;
stage = "members finalize"; rc = sqlite3_finalize(peers); peers = NULL; if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM;
if (strbuf_addf(&json, "%s{\"id\":", json.len > 1 ? "," : "") < 0 || strbuf_add_json_string(&json, channel) < 0 ||
strbuf_addf(&json, ",\"name\":") < 0 || strbuf_add_json_string(&json, name) < 0 ||
strbuf_addf(&json, ",\"owner_id\":\"0x%016llx\",\"owner_name\":", (unsigned long long)owner) < 0 ||
strbuf_add_json_string(&json, owner_name) < 0 ||
strbuf_addf(&json, ",\"peers\":%lld,\"msgs\":%lld,\"online\":%lld}", (long long)count, (long long)messages, (long long)online) < 0) goto error;
if (json.len >= buf_size) { rc = SQLITE_TOOBIG; goto error; }
stage = "channels step";
}
if (rc != SQLITE_DONE) goto error;
stage = "channels finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM; if (strbuf_addf(&json, "]") < 0) goto error;
int result = json_copy(&json, buf, buf_size, out_len); strbuf_free(&json); return result;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channels JSON: stage=%s rc=%d bytes=%zu capacity=%zu: %s",
stage, rc, json.len, buf_size, sqlite3_errmsg(cc->db));
int finish = sqlite3_finalize(peers);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channels JSON: members cleanup rc=%d", finish);
finish = sqlite3_finalize(st);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channels JSON: cleanup rc=%d", finish);
strbuf_free(&json); return -1;
}
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = json_context(inst, buf, buf_size, out_len); if (!cc) return -1;
if (!chat_channel_id(ch_id) || offset < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "messages JSON: invalid channel/offset=%d", offset); return -1;
}
char table[80], sql[1024]; msg_table_name(ch_id, table, sizeof(table));
snprintf(sql, sizeof(sql),
"SELECT json_object('id',m.id,'message_id',lower(hex(m.message_id)),'ts',m.timestamp,"
"'author_id',printf('0x%%016llx',m.node_id),'author_name',coalesce(n.name,''),"
"'content_type',CASE WHEN json_valid(CAST(m.data AS TEXT)) THEN json_extract(CAST(m.data AS TEXT),'$.ct') ELSE 'text' END,"
"'data',coalesce(CAST(m.data AS TEXT),''),'local_attrs',coalesce(m.local_attrs,''),'ver',m.ver,'edited_at',m.edited_at)"
" FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id"
" ORDER BY m.timestamp DESC,m.message_id DESC LIMIT ? OFFSET ?", table);
sqlite3_stmt* st = NULL; const char* stage = "prepare";
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) { stage = "bind count"; rc = sqlite3_bind_int(st, 1, count > 0 ? count : 100); }
if (rc == SQLITE_OK) { stage = "bind offset"; rc = sqlite3_bind_int(st, 2, offset); }
if (rc != SQLITE_OK) goto error;
size_t off = 1; buf[0] = '['; stage = "step";
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char* row = (const char*)sqlite3_column_text(st, 0); size_t bytes = sqlite3_column_bytes(st, 0);
size_t separator = off > 1, available = buf_size - off;
if (!row || available < separator + 2 || bytes > available - separator - 2) {
stage = "JSON capacity/read"; rc = row ? SQLITE_TOOBIG : SQLITE_NOMEM; goto error;
}
if (separator) buf[off++] = ',';
memcpy(buf + off, row, bytes); off += bytes;
}
if (rc != SQLITE_DONE) goto error;
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
buf[off++] = ']'; buf[off] = 0; *out_len = off; return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "messages JSON: ch=%s stage=%s rc=%d capacity=%zu: %s", ch_id, stage, rc, buf_size, sqlite3_errmsg(cc->db));
int finish = sqlite3_finalize(st);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "messages JSON: cleanup rc=%d", finish);
buf[0] = 0; return -1;
}
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = json_context(inst, buf, buf_size, out_len); if (!cc) return -1;
if (!chat_channel_id(ch_id)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "members JSON: invalid channel"); return -1; }
char table[80], sql[256]; peers_table_name(ch_id, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT p.node_id,n.name,n.x25519_pubkey,n.ed25519_pubkey,p.adm_tags,p.deleted"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id ORDER BY p.node_id", table);
sqlite3_stmt *st = NULL, *addresses = NULL; struct strbuf json = strbuf_new(); const char* stage = "prepare";
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL); if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM; if (strbuf_addf(&json, "[") < 0) goto error;
stage = "members step";
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
uint64_t node = (uint64_t)sqlite3_column_int64(st, 0);
const char *name, *tags; stage = "member text";
rc = json_read_text(st, 1, &name); if (rc == SQLITE_OK) rc = json_read_text(st, 4, &tags);
if (rc != SQLITE_OK) goto error;
char keys[2][65] = {{0}};
for (int i = 0; i < 2; i++) if (sqlite3_column_type(st, i + 2) != SQLITE_NULL) {
stage = "key size/type"; rc = SQLITE_CORRUPT;
if (sqlite3_column_type(st, i + 2) != SQLITE_BLOB || sqlite3_column_bytes(st, i + 2) != 32) goto error;
const uint8_t* key = sqlite3_column_blob(st, i + 2); if (!key) { rc = SQLITE_NOMEM; goto error; }
for (int b = 0; b < 32; b++) snprintf(keys[i] + b * 2, 3, "%02x", key[b]);
}
int online = !sqlite3_column_int(st, 5) && chat_core_member_online(inst, ch_id, node);
int connected = inst->connections && queue_find_data_by_index(inst->connections, (const uint8_t*)&node);
stage = "JSON"; rc = SQLITE_NOMEM;
if (strbuf_addf(&json, "%s{\"node_id\":\"0x%016llx\",\"name\":", json.len > 1 ? "," : "", (unsigned long long)node) < 0 ||
strbuf_add_json_string(&json, name) < 0 || strbuf_addf(&json, ",\"online\":%d,\"connected\":%d,\"adm_tags\":", online, connected) < 0 ||
strbuf_add_json_string(&json, tags) < 0 ||
strbuf_addf(&json, ",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", keys[0], keys[1]) < 0) goto error;
stage = "addresses prepare";
rc = sqlite3_prepare_v2(cc->db, "SELECT family,protocol,address,port,rtt FROM node_addresses WHERE node_id=? ORDER BY family,protocol",
-1, &addresses, NULL);
if (rc == SQLITE_OK) { stage = "addresses bind"; rc = sqlite3_bind_int64(addresses, 1, (sqlite3_int64)node); }
if (rc != SQLITE_OK) goto error;
const char* separator = ""; stage = "addresses step";
while ((rc = sqlite3_step(addresses)) == SQLITE_ROW) {
int family = sqlite3_column_int(addresses, 0), protocol = sqlite3_column_int(addresses, 1);
int bytes = sqlite3_column_bytes(addresses, 2), port = sqlite3_column_int(addresses, 3);
const void* address = sqlite3_column_blob(addresses, 2); char ip[64];
if ((family != 4 && family != 6) || sqlite3_column_type(addresses, 2) != SQLITE_BLOB ||
bytes != (family == 4 ? 4 : 16) || !address ||
(protocol != 1 && protocol != 2 && protocol != 6) || port < 0 || port > UINT16_MAX ||
!inet_ntop(family == 4 ? AF_INET : AF_INET6, address, ip, sizeof(ip))) {
stage = "invalid stored address"; rc = SQLITE_CORRUPT; goto error;
}
stage = "JSON"; rc = SQLITE_NOMEM;
if (strbuf_addf(&json, "%s{\"ip\":", separator) < 0 || strbuf_add_json_string(&json, ip) < 0 ||
strbuf_addf(&json, ",\"port\":%d,\"proto\":\"%s\",\"rtt\":%lld}", port,
protocol == 1 ? "UDP" : protocol == 2 ? "TCP" : "TCP+REALITY", (long long)sqlite3_column_int64(addresses, 4)) < 0) goto error;
separator = ","; if (json.len >= buf_size) { rc = SQLITE_TOOBIG; goto error; }
stage = "addresses step";
}
if (rc != SQLITE_DONE) goto error;
stage = "addresses finalize"; rc = sqlite3_finalize(addresses); addresses = NULL; if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM; if (strbuf_addf(&json, "]}") < 0) goto error;
if (json.len >= buf_size) { rc = SQLITE_TOOBIG; goto error; }
stage = "members step";
}
if (rc != SQLITE_DONE) goto error;
stage = "members finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "JSON"; rc = SQLITE_NOMEM; if (strbuf_addf(&json, "]") < 0) goto error;
int result = json_copy(&json, buf, buf_size, out_len); strbuf_free(&json); return result;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "members JSON: ch=%s stage=%s rc=%d bytes=%zu capacity=%zu: %s",
ch_id, stage, rc, json.len, buf_size, sqlite3_errmsg(cc->db));
int finish = sqlite3_finalize(addresses);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "members JSON: addresses cleanup rc=%d", finish);
finish = sqlite3_finalize(st);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "members JSON: cleanup rc=%d", finish);
strbuf_free(&json); return -1;
}
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (out && sz) out[0] = 0;
if (!cc || !cc->initialized || !cc->db || !out || !sz) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "node name: invalid context/output node=%016llx", (unsigned long long)node_id); return -1;
}
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
if (n) snprintf(out, sz, "%s", n);
else if (sqlite3_errcode(cc->db) == SQLITE_NOMEM) rc = SQLITE_NOMEM;
}
int final = sqlite3_finalize(st);
if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) {
out[0] = 0; DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "node name: lookup node=%016llx rc=%d finalize=%d: %s",
(unsigned long long)node_id, rc, final, sqlite3_errmsg(cc->db)); return -1;
}
return 0;
}
/* ─── adm_tags editing (channel owner only) ─── */
struct chat_member_tags_key {
char key[CHAT_MEMBER_TAGS_KEY_SZ];
char* val;
char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */
};
struct chat_member_tags {
char ch_id[64];
uint64_t node_id;
sqlite3* db; /* Заимствована до остановки сервиса. */
char baseline[MS_ADM_TAGS_MAX + 1]; /* Проверенный owner-блок на момент загрузки/commit. */
uint8_t signature[64], x25519[32], ed25519[32];
int version, parsing_edit;
int key_count;
struct chat_member_tags_key keys[CHAT_MEMBER_TAGS_MAX_KEYS];
};
static int tags_find(struct chat_member_tags* t, const char* key) {
for (int i = 0; i < t->key_count; i++)
if (strcmp(t->keys[i].key, key) == 0) return i;
return -1;
}
static const char* tags_append_effective(const char* raw, uint64_t* ts_out) {
if (!raw || !raw[0]) { if (ts_out) *ts_out = 0; return "no"; }
const char* last = raw;
for (const char* p = raw; *p; p++)
if (*p == 'e' || *p == 'd') last = p;
if (ts_out) *ts_out = strtoull(last + 1, NULL, 10);
return *last == 'e' ? "yes" : "no";
}
static int tags_is_append_key(const char* key) {
return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
struct chat_member_tags* t = (struct chat_member_tags*)arg;
if (!key[0] || t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1;
}
if (t->parsing_edit && tags_is_append_key(key) && strcmp(json_val, "yes") && strcmp(json_val, "no")) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: edited role must be yes/no key=%s", key); return -1;
}
struct chat_member_tags_key* k = &t->keys[t->key_count];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (!k->val || (tags_is_append_key(key) && !k->raw)) {
u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k));
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1;
}
t->key_count++;
return 0;
}
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !chat_channel_id(ch_id) || !node_id || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: invalid load context/channel/member"); return NULL;
}
struct chat_member_tags* t = u_calloc(1, sizeof(*t));
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — OOM", CC_ID); return NULL; }
snprintf(t->ch_id, sizeof(t->ch_id), "%s", ch_id);
t->node_id = node_id; t->db = cc->db;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags,adm_tags_sig,x25519_pubkey,ed25519_pubkey,source FROM \"%s\" WHERE node_id=?", peers_tbl);
const char* stage = "prepare"; int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) { stage = "bind"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); }
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
if (rc != SQLITE_ROW) goto error;
stage = "fields"; rc = SQLITE_CORRUPT;
int type = sqlite3_column_type(st, 0), bytes = sqlite3_column_bytes(st, 0), sig_type = sqlite3_column_type(st, 1);
const char* text = (const char*)sqlite3_column_text(st, 0);
const void *signature = sqlite3_column_blob(st, 1), *x = sqlite3_column_blob(st, 2), *ed = sqlite3_column_blob(st, 3);
if ((type != SQLITE_NULL && type != SQLITE_TEXT) || (type == SQLITE_TEXT && (!text || strlen(text) != (size_t)bytes)) ||
bytes > MS_ADM_TAGS_MAX || sqlite3_column_type(st, 2) != SQLITE_BLOB || sqlite3_column_bytes(st, 2) != 32 || !x ||
sqlite3_column_type(st, 3) != SQLITE_BLOB || sqlite3_column_bytes(st, 3) != 32 || !ed ||
sqlite3_column_type(st, 4) != SQLITE_INTEGER || sqlite3_column_int(st, 4)) goto error;
if (bytes) memcpy(t->baseline, text, (size_t)bytes + 1);
if (sig_type != SQLITE_NULL) {
if (sig_type != SQLITE_BLOB || sqlite3_column_bytes(st, 1) != 64 || !signature) goto error;
memcpy(t->signature, signature, 64);
}
memcpy(t->x25519, x, 32); memcpy(t->ed25519, ed, 32);
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
t->version = member_sync_owner_version(cc->db, t->baseline);
if (t->version < 0) goto invalid;
if (t->version) {
uint8_t key[32], body[MS_ADM_TAGS_MAX + 8];
int length = member_sync_build_owner_msg(node_id, t->baseline, body, sizeof(body));
if (length < 0 || topo_node_sqlite_channel_get(cc->db, ch_id, NULL, 0, NULL, NULL, key, NULL) ||
sc_ed25519_verify(key, body, (size_t)length, t->signature) != SC_OK) goto invalid;
if (chat_json_strings(cc->db, t->baseline, MS_ADM_TAGS_MAX, tags_json_cb, t)) goto invalid;
} else if (memcmp(t->signature, (uint8_t[64]){0}, 64)) goto invalid;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "tags: loaded ch=%s node=%016llx version=%d keys=%d",
ch_id, (unsigned long long)node_id, t->version, t->key_count);
return t;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: invalid signed owner block ch=%s node=%016llx", ch_id, (unsigned long long)node_id);
chat_member_tags_free(t); return NULL;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: load ch=%s node=%016llx stage=%s rc=%d: %s",
ch_id, (unsigned long long)node_id, stage, rc, sqlite3_errmsg(cc->db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: load cleanup finalize=%d", rc);
chat_member_tags_free(t); return NULL;
}
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) {
if (!t || !key || !key[0] || !strcmp(key, "ver") ||
(val && tags_is_append_key(key) && strcmp(val, "yes") && strcmp(val, "no"))) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: invalid edit key/role or reserved version"); return -1;
}
if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; }
if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
int idx = tags_find(t, key);
if (!val) {
if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; }
return 0;
}
char* copy = u_strdup(val);
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; }
if (idx >= 0) {
u_free(t->keys[idx].val); t->keys[idx].val = copy;
} else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS - (tags_find(t, "ver") < 0)) {
struct chat_member_tags_key* k = &t->keys[t->key_count++];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
k->val = copy;
k->raw = NULL;
} else {
u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
}
return 0;
}
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) {
if (!t || !key) return NULL;
int idx = tags_find(t, key);
return idx >= 0 ? t->keys[idx].val : NULL;
}
int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1;
}
struct chat_member_tags parsed = {.db = t->db, .parsing_edit = 1};
int rc = chat_json_strings(t->db, json, MS_ADM_TAGS_MAX, tags_json_cb, &parsed);
char version[16]; snprintf(version, sizeof(version), "%d", t->version);
const char* requested = chat_member_tags_get(&parsed, "ver");
if (requested && strcmp(requested, version)) rc = -1;
if (!requested && parsed.key_count == CHAT_MEMBER_TAGS_MAX_KEYS) rc = -1;
if (rc != 0) {
for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); }
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "tags: replace rejected JSON/version/key limit"); return -1;
}
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys));
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count);
return 0;
}
/* Сравнение желаемых значений;отсутствующий admin/moder означает отключённую роль. */
static int tags_same_edit(struct chat_member_tags* edit, struct chat_member_tags* current) {
for (int pass = 0; pass < 2; pass++) {
struct chat_member_tags *a = pass ? current : edit, *b = pass ? edit : current;
for (int i = 0; i < a->key_count; i++) {
const char *key = a->keys[i].key, *value = a->keys[i].val, *other = chat_member_tags_get(b, key);
if (!strcmp(key, "ver")) continue;
if (!other && tags_is_append_key(key)) other = "no";
if (!other || strcmp(value, other)) return 0;
}
}
return 1;
}
static int tags_json_pair(struct strbuf* json, const char* key, const char* value) {
if (strbuf_addf(json, ",") < 0 || strbuf_add_json_string(json, key) < 0 || strbuf_addf(json, ":") < 0 ||
strbuf_add_json_string(json, value) < 0 || json->len > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: JSON allocation/size limit key=%s", key); return -1;
}
return 0;
}
/* История ролей берётся из проверенного текущего блока,новая версия строится отдельно от editor. */
static int tags_build_json(struct UTUN_INSTANCE* inst, struct chat_member_tags* edit,
struct chat_member_tags* current, struct strbuf* json) {
if (current->version == INT_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: version exhausted"); return -1; }
if (strbuf_addf(json, "{\"ver\":\"%d\"", current->version + 1) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: JSON allocation failed"); return -1;
}
for (int i = 0; i < edit->key_count; i++) {
const char* key = edit->keys[i].key;
if (strcmp(key, "ver") && !tags_is_append_key(key) && tags_json_pair(json, key, edit->keys[i].val)) return -1;
}
const char* roles[] = {"admin", "moder"};
for (size_t i = 0; i < 2; i++) {
const char* desired = chat_member_tags_get(edit, roles[i]);
int index = tags_find(current, roles[i]);
const char* history = index < 0 ? "" : current->keys[index].raw;
int enabled = desired && !strcmp(desired, "yes"), previous = !strcmp(tags_append_effective(history, NULL), "yes");
char appended[MS_ADM_TAGS_MAX + 1];
if (enabled != previous) {
time_t seconds = ntp_time_get_seconds(inst);
int bytes = snprintf(appended, sizeof(appended), "%s%c%llu", history, enabled ? 'e' : 'd', (unsigned long long)seconds);
if (seconds <= 0 || bytes < 0 || (size_t)bytes >= sizeof(appended)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: role history/time limit key=%s", roles[i]); return -1;
}
history = appended;
}
if (history[0] && tags_json_pair(json, roles[i], history)) return -1;
}
if (strbuf_addf(json, "}") < 0 || json->len > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: complete JSON exceeds memory/size limit"); return -1;
}
return 0;
}
/* Выполнить UPDATE внутри транзакции владельца; пустое имя снимает локальный псевдоним. */
static int member_write_nickname(sqlite3* db, const char* channel, uint64_t node, const char* nickname) {
char table[80], sql[160]; peers_table_name(channel, table, sizeof(table));
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET local_nick=? WHERE node_id=?", table);
sqlite3_stmt* st = NULL; const char* stage = "prepare";
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) { stage = "bind nickname"; rc = sqlite3_bind_text(st, 1, nickname[0] ? nickname : NULL, -1, SQLITE_STATIC); }
if (rc == SQLITE_OK) { stage = "bind node"; rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)node); }
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
if (rc == SQLITE_DONE && sqlite3_changes(db) != 1) { stage = "missing member"; rc = SQLITE_NOTFOUND; }
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: ch=%s node=%016llx stage=%s rc=%d: %s",
channel, (unsigned long long)node, stage, rc, sqlite3_errmsg(db));
int finish = sqlite3_finalize(st);
if (finish != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: nickname finalize=%d: %s", finish, sqlite3_errmsg(db));
return rc == SQLITE_DONE && finish == SQLITE_OK ? 0 : -1;
}
/* Общая фиксация owner-блока с необязательным локальным псевдонимом в той же транзакции. */
static int member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t, const char* nickname) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!t || !cc || !cc->initialized || !cc->db || t->db != cc->db || !sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: commit requires active owner DB without outer transaction"); return -1;
}
int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* tags */", NULL, NULL, NULL);
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db)); return -1; }
struct strbuf json = strbuf_new(); int result = -1, changes = 0;
struct chat_member_tags* current = chat_member_tags_load(inst, t->ch_id, t->node_id);
if (!current) goto done;
if (strcmp(current->baseline, t->baseline) || memcmp(current->signature, t->signature, 64)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "tags: stale editor ch=%s node=%016llx expected=%d current=%d",
t->ch_id, (unsigned long long)t->node_id, t->version, current->version); result = -2; goto done;
}
if (tags_same_edit(t, current)) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "tags: unchanged ch=%s node=%016llx ver=%d", t->ch_id,
(unsigned long long)t->node_id, t->version);
if (nickname) goto write_nickname;
result = 0; goto done;
}
if (tags_build_json(inst, t, current, &json)) goto done;
uint8_t private_key[32] = {0}, body[MS_ADM_TAGS_MAX + 8];
int bytes = member_sync_build_owner_msg(t->node_id, json.buf, body, sizeof(body));
int signed_ok = bytes >= 0 && !topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, private_key) &&
sc_ed25519_sign(private_key, body, (size_t)bytes, current->signature) == SC_OK;
OPENSSL_cleanse(private_key, sizeof(private_key));
if (!signed_ok) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: channel signing failed ch=%s", t->ch_id); goto done; }
for (int i = 0; i < current->key_count; i++) { u_free(current->keys[i].raw); u_free(current->keys[i].val); }
memset(current->keys, 0, sizeof(current->keys)); current->key_count = 0;
if (chat_json_strings(cc->db, json.buf, MS_ADM_TAGS_MAX, tags_json_cb, current)) goto done;
memcpy(current->baseline, json.buf, json.len + 1); current->version++;
changes = member_sync_put(inst, t->ch_id, t->node_id, current->x25519, current->ed25519,
NULL, 0, NULL, 0, NULL, json.buf, current->signature, 0, 0, NULL);
if (changes < 0 || !(changes & MS_APPLY_CHANGED)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: owner version not written ch=%s flags=%d", t->ch_id, changes); goto done;
}
write_nickname:
if (nickname && member_write_nickname(cc->db, t->ch_id, t->node_id, nickname)) goto done;
rc = sqlite3_exec(cc->db, "COMMIT /* tags */", NULL, NULL, NULL);
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: commit rc=%d: %s", rc, sqlite3_errmsg(cc->db)); goto done; }
if (!changes) { result = 0; goto done; }
char channel[64]; memcpy(channel, t->ch_id, sizeof(channel)); uint64_t node = t->node_id;
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
*t = *current; u_free(current); strbuf_free(&json);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "tags: committed ch=%s node=%016llx ver=%d keys=%d", channel,
(unsigned long long)node, t->version, t->key_count);
member_sync_record_committed(inst, channel, node, (unsigned)changes); return 0;
done:
chat_member_tags_free(current); strbuf_free(&json);
if (!sqlite3_get_autocommit(cc->db)) {
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db)); result = -1; }
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: transaction remains open;requesting owner stop/DB close");
utun_instance_stop(inst); result = -1;
}
}
return result;
}
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t) {
return member_tags_commit(inst, t, NULL);
}
void chat_member_tags_free(struct chat_member_tags* t) {
if (!t) return;
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
u_free(t);
}
/* Частичный JSON меняет только указанные поля; ver проверяет исходный снимок формы. */
static int member_tags_patch(struct chat_member_tags* t, const char* json) {
int rc = 0;
if (json[0]) {
struct chat_member_tags* patch = u_calloc(1, sizeof(*patch));
if (!patch) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: patch allocation failed"); rc = -1; }
else {
patch->db = t->db; patch->version = t->version;
rc = chat_member_tags_replace(patch, json);
for (int i = 0; !rc && i < patch->key_count; i++) {
struct chat_member_tags_key* key = &patch->keys[i];
if (strcmp(key->key, "ver")) rc = chat_member_tags_set(t, key->key, key->val);
}
chat_member_tags_free(patch);
}
}
return rc;
}
/* Ник и подписанные поля подтверждаются вместе; SQL/подпись/конфликт не дают частичного сохранения. */
int chat_core_save_member(struct UTUN_INSTANCE* inst, const char* channel, uint64_t node,
const char* nickname, const char* owner_json) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !chat_channel_id(channel) || !node || !nickname || !owner_json ||
!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: invalid context/channel/member or outer transaction"); return -1;
}
int result = -1;
if (owner_json[0]) {
struct chat_member_tags* t = chat_member_tags_load(inst, channel, node);
if (!t) return -1;
if (!member_tags_patch(t, owner_json)) result = member_tags_commit(inst, t, nickname);
chat_member_tags_free(t);
} else {
int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* nickname */", NULL, NULL, NULL);
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db)); return -1; }
if (!member_write_nickname(cc->db, channel, node, nickname)) {
rc = sqlite3_exec(cc->db, "COMMIT /* nickname */", NULL, NULL, NULL);
if (rc == SQLITE_OK) result = 0;
else DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: commit rc=%d: %s", rc, sqlite3_errmsg(cc->db));
}
if (!sqlite3_get_autocommit(cc->db)) {
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db));
if (!sqlite3_get_autocommit(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: transaction remains open; requesting owner stop");
utun_instance_stop(inst); result = -1;
}
}
}
if (result) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member save: refused ch=%s node=%016llx result=%d",
channel, (unsigned long long)node, result);
else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "member save: committed ch=%s node=%016llx nickname_bytes=%zu owner_edit=%d",
channel, (unsigned long long)node, strlen(nickname), !!owner_json[0]);
uint8_t event[65]; size_t length = strlen(channel); event[0] = (uint8_t)length; memcpy(event + 1, channel, length);
chat_event_post(inst, CHAT_EVT_MEMBERS_CHANGED, event, length + 1);
}
return result;
}
/* ─── trampoline for chatgui ─── */
void chat_member_tags_commit_trampoline(void* arg) {
struct chat_member_tags_req* req = (struct chat_member_tags_req*)arg;
if (!req || !req->inst || !memchr(req->ch_id, 0, sizeof(req->ch_id)) || !memchr(req->json, 0, sizeof(req->json))) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "tags: invalid trampoline request"); u_free(arg); return;
}
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
if (!t) { u_free(arg); return; }
if (!member_tags_patch(t, req->json)) chat_member_tags_commit(req->inst, t);
chat_member_tags_free(t);
u_free(arg);
}