You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
772 lines
35 KiB
772 lines
35 KiB
/* |
|
* 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_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/json_flat.h" |
|
|
|
#include <string.h> |
|
#include <stdio.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 error: %s", CC_ID, err); |
|
sqlite3_free(err); |
|
} |
|
return rc; |
|
} |
|
|
|
/* ─── db_sync registry ─── */ |
|
|
|
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id) { |
|
if (!inst || !inst->db_sync || !ch_id || !ch_id[0]) return NULL; |
|
return db_sync_instance_find(inst, strtoull(ch_id, NULL, 10)); |
|
} |
|
|
|
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id) { |
|
uint32_t msg_cnt = db_sync_count(si); |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: si_register ch=%s mc=%u", CC_ID, ch_id, msg_cnt); |
|
} |
|
|
|
/* ─── жизненный цикл ─── */ |
|
|
|
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { |
|
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; } |
|
|
|
if (inst->chat_core) return 0; |
|
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; } |
|
inst->chat_core = cc; |
|
snprintf(cc->db_path, sizeof(cc->db_path), "%s", db_path); |
|
|
|
if (inst->topo_sqlite_db) { |
|
cc->db = inst->topo_sqlite_db; cc->shared_db = 1; |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)cc->db); |
|
} else { |
|
int rc = sqlite3_open_v2(db_path, &cc->db, |
|
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); |
|
if (rc != SQLITE_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: cannot open DB %s: %s", |
|
CC_ID, db_path, sqlite3_errmsg(cc->db)); |
|
sqlite3_close(cc->db); inst->chat_core = NULL; u_free(cc); return -1; |
|
} |
|
sqlite3_exec(cc->db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); |
|
sqlite3_exec(cc->db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); |
|
} |
|
|
|
cc->inst = inst; |
|
cc->my_node_id = inst->node_id; |
|
|
|
/* перенести chat-настройки из конфига (заполнены парсером) в per-instance состояние */ |
|
if (inst->config) memcpy(&inst->chat_settings, &inst->config->global.chat_settings, sizeof(inst->chat_settings)); |
|
|
|
sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst); |
|
|
|
/* записать себя в nodes + local_identity */ |
|
{ |
|
const char* my_name = inst->name[0] ? inst->name : "Me"; |
|
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); |
|
sqlite3_stmt* st = NULL; |
|
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 (st) { |
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); |
|
sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC); |
|
sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC); |
|
sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC); |
|
sqlite3_bind_int64(st, 5, now_sec); |
|
sqlite3_bind_int64(st, 6, now_sec); |
|
sqlite3_step(st); sqlite3_finalize(st); |
|
} |
|
} |
|
|
|
cc->initialized = 1; |
|
|
|
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */ |
|
chat_member_init(inst); |
|
|
|
/* create media_files table */ |
|
media_index_init(cc->db); |
|
|
|
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" |
|
");" |
|
); |
|
/* store media_base for block_req handler */ |
|
{ |
|
char mbase[512]; |
|
const char* ls = strrchr(db_path, '/'); |
|
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", (int)(ls - db_path), db_path); |
|
else snprintf(mbase, sizeof(mbase), "%s", db_path); |
|
char sql[1024]; snprintf(sql, sizeof(sql), |
|
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase); |
|
sqlite3_exec(cc->db, sql, NULL, NULL, NULL); |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase); |
|
} |
|
|
|
/* seed stub accounts if empty */ |
|
{ |
|
sqlite3_stmt* cnt_st = NULL; |
|
if (sqlite3_prepare_v2(cc->db, "SELECT COUNT(*) FROM accounts", |
|
-1, &cnt_st, NULL) == SQLITE_OK) { |
|
if (sqlite3_step(cnt_st) == SQLITE_ROW && sqlite3_column_int(cnt_st, 0) == 0) { |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: seeding 5 stub accounts", CC_ID); |
|
struct { int64_t id; const char* name; const char* letter; const char* color; } accs[] = { |
|
{1, "Alice", "A", "#4A90E2"}, {2, "Bob", "B", "#7ED321"}, |
|
{3, "Carol", "C", "#F5A623"}, {4, "Dave", "D", "#9013FE"}, |
|
{5, "Eve", "E", "#00B894"}, |
|
}; |
|
for (int i = 0; i < 5; i++) { |
|
sqlite3_stmt* ins = NULL; |
|
sqlite3_prepare_v2(cc->db, |
|
"INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter,created_at)" |
|
" VALUES(?,?,?,?,?)", -1, &ins, NULL); |
|
if (ins) { |
|
sqlite3_bind_int64(ins, 1, accs[i].id); |
|
sqlite3_bind_text(ins, 2, accs[i].name, -1, SQLITE_STATIC); |
|
sqlite3_bind_text(ins, 3, accs[i].color, -1, SQLITE_STATIC); |
|
sqlite3_bind_text(ins, 4, accs[i].letter, -1, SQLITE_STATIC); |
|
sqlite3_bind_int64(ins, 5, now_sec); |
|
sqlite3_step(ins); sqlite3_finalize(ins); |
|
} |
|
} |
|
} |
|
sqlite3_finalize(cnt_st); |
|
} |
|
} |
|
|
|
/* ensure infrastructure for all existing channels (survives restart) */ |
|
{ |
|
sqlite3_stmt* stmt = NULL; |
|
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels ORDER BY created_at ASC", |
|
-1, &stmt, NULL) == SQLITE_OK) { |
|
int loaded = 0; |
|
while (sqlite3_step(stmt) == SQLITE_ROW) { |
|
const char* ch = (const char*)sqlite3_column_text(stmt, 0); |
|
if (ch && ch[0]) { chat_core_ensure_channel_ready(inst, ch); loaded++; } |
|
} |
|
sqlite3_finalize(stmt); |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: loaded %d channels from DB", CC_ID, loaded); |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
void chat_core_destroy(struct UTUN_INSTANCE* inst) { |
|
struct chat_core_ctx* cc = CC(inst); |
|
if (!cc || !cc->initialized) return; |
|
cc->initialized = 0; |
|
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */ |
|
|
|
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); } |
|
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) ─── */ |
|
|
|
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) { |
|
chat_setting_set(inst, name, value); |
|
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(inst); |
|
} |
|
|
|
void chat_core_set_setting_trampoline(void* arg) { |
|
struct chat_setting_arg* a = (struct chat_setting_arg*)arg; |
|
const char* name = a->data; |
|
const char* value = name + strlen(name) + 1; |
|
chat_core_set_setting(a->inst, name, value); |
|
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 = CC(inst); |
|
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return 1; |
|
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id); |
|
sqlite3_stmt* st = NULL; |
|
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) != SQLITE_OK) return 1; |
|
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); |
|
int on = 1; |
|
if (sqlite3_step(st) == SQLITE_ROW) { |
|
const char* v = (const char*)sqlite3_column_text(st, 0); |
|
if (v) on = atoi(v) != 0; |
|
} |
|
sqlite3_finalize(st); |
|
return on; |
|
} |
|
|
|
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) { |
|
struct chat_core_ctx* cc = CC(inst); |
|
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return; |
|
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); |
|
chat_core_save_ui_state(inst, key, val); |
|
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); |
|
} |
|
|
|
void chat_core_set_sound_on_message_trampoline(void* arg) { |
|
struct chat_sound_arg* a = (struct chat_sound_arg*)arg; |
|
if (!a) return; |
|
chat_core_set_sound_on_message(a->inst, a->ch_id, a->on); |
|
u_free(arg); |
|
} |
|
|
|
/* ─── headless API ─── */ |
|
|
|
static void json_escape(const char* src, char* dst, size_t dst_sz) { |
|
size_t i = 0; const char* s = src; |
|
while (*s && i < dst_sz - 2) { |
|
const char* escape = NULL; |
|
switch (*s) { |
|
case '"': escape = "\\\""; break; |
|
case '\\': escape = "\\\\"; break; |
|
case '\n': escape = "\\n"; break; |
|
case '\r': escape = "\\r"; break; |
|
case '\t': escape = "\\t"; break; |
|
case '\b': escape = "\\b"; break; |
|
case '\f': escape = "\\f"; break; |
|
} |
|
if (escape) { |
|
if (i + 2 >= dst_sz) break; |
|
dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue; |
|
} |
|
dst[i++] = *s++; |
|
} |
|
dst[i] = '\0'; |
|
} |
|
|
|
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 = CC(inst); |
|
if (!cc || !cc->initialized || !buf || !out_len) return -1; |
|
char* w = buf; char* end = buf + buf_size; |
|
*w++ = '['; const char* sep = ""; |
|
sqlite3_stmt* st = NULL; |
|
if (sqlite3_prepare_v2(cc->db, |
|
"SELECT c.channel_id, c.name, c.owner_node_id, " |
|
"(SELECT COUNT(*) FROM nodes WHERE node_id=c.owner_node_id AND name IS NOT NULL) AS has_owner_name, " |
|
"(SELECT name FROM nodes WHERE node_id=c.owner_node_id) AS owner_name " |
|
"FROM channels c ORDER BY c.created_at ASC", |
|
-1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } |
|
while (sqlite3_step(st) == SQLITE_ROW) { |
|
const char* ch_id = (const char*)sqlite3_column_text(st, 0); |
|
const char* ch_name = (const char*)sqlite3_column_text(st, 1); |
|
uint64_t owner_id = (uint64_t)sqlite3_column_int64(st, 2); |
|
const char* owner_name = (const char*)sqlite3_column_text(st, 4); |
|
if (!ch_id) continue; |
|
|
|
uint32_t msg_count = chat_core_count(inst, ch_id); |
|
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); |
|
int peer_count = 0, online_count = 0; |
|
sqlite3_stmt* ps = NULL; |
|
char psql[200]; snprintf(psql, sizeof(psql), "SELECT node_id FROM \"%s\" WHERE deleted=0", peers_tbl); |
|
if (sqlite3_prepare_v2(cc->db, psql, -1, &ps, NULL) == SQLITE_OK) { |
|
while (sqlite3_step(ps) == SQLITE_ROW) { |
|
uint64_t pid = (uint64_t)sqlite3_column_int64(ps, 0); |
|
peer_count++; |
|
if (chat_core_member_online(inst, ch_id, pid)) online_count++; |
|
} |
|
sqlite3_finalize(ps); |
|
} |
|
|
|
char esc_name[256], esc_owner[256]; |
|
json_escape(ch_name ? ch_name : "", esc_name, sizeof(esc_name)); |
|
json_escape(owner_name ? owner_name : "", esc_owner, sizeof(esc_owner)); |
|
|
|
w += snprintf(w, (size_t)(end - w), |
|
"%s{\"id\":\"%s\",\"name\":\"%s\",\"owner_id\":\"0x%016llx\",\"owner_name\":\"%s\"," |
|
"\"peers\":%d,\"msgs\":%u,\"online\":%d}", |
|
sep, ch_id, esc_name, (unsigned long long)owner_id, esc_owner, peer_count, msg_count, online_count); |
|
sep = ","; |
|
} |
|
sqlite3_finalize(st); |
|
*w++ = ']'; |
|
*w = '\0'; |
|
*out_len = (size_t)(w - buf); |
|
return 0; |
|
} |
|
|
|
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 = CC(inst); |
|
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1; |
|
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); |
|
char* w = buf; char* end = buf + buf_size; |
|
*w++ = '['; const char* sep = ""; |
|
char sql[256]; |
|
snprintf(sql, sizeof(sql), |
|
"SELECT m.id, m.timestamp, m.node_id, CAST(m.data AS TEXT), n.name, m.local_attrs " |
|
"FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id " |
|
"ORDER BY m.timestamp DESC LIMIT ? OFFSET ?", tbl); |
|
sqlite3_stmt* st = NULL; |
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } |
|
sqlite3_bind_int(st, 1, count > 0 ? count : 100); |
|
sqlite3_bind_int(st, 2, offset); |
|
while (sqlite3_step(st) == SQLITE_ROW) { |
|
int64_t id = sqlite3_column_int64(st, 0); |
|
int64_t ts = sqlite3_column_int64(st, 1); |
|
uint64_t author_id = (uint64_t)sqlite3_column_int64(st, 2); |
|
const char* data = (const char*)sqlite3_column_text(st, 3); |
|
const char* author_name = (const char*)sqlite3_column_text(st, 4); |
|
const char* la = (const char*)sqlite3_column_text(st, 5); |
|
if (!data) data = ""; |
|
if (!la) la = ""; |
|
const char* ct = "text"; |
|
const char* ctpos = strstr(data, "\"ct\":\""); |
|
if (ctpos) { ctpos += 6; char ctbuf[32] = {0}; int ci = 0; while (*ctpos && *ctpos != '"' && ci < 31) ctbuf[ci++] = *ctpos++; ct = ctbuf; } |
|
char esc_data[4096], esc_aname[256], esc_la[256]; |
|
json_escape(data, esc_data, sizeof(esc_data)); |
|
json_escape(author_name ? author_name : "", esc_aname, sizeof(esc_aname)); |
|
json_escape(la, esc_la, sizeof(esc_la)); |
|
w += snprintf(w, (size_t)(end - w), |
|
"%s{\"id\":%lld,\"ts\":%lld,\"author_id\":\"0x%016llx\",\"author_name\":\"%s\"," |
|
"\"content_type\":\"%s\",\"data\":\"%.2000s\",\"local_attrs\":\"%s\"}", |
|
sep, (long long)id, (long long)ts, (unsigned long long)author_id, esc_aname, ct, esc_data, esc_la); |
|
sep = ","; |
|
} |
|
sqlite3_finalize(st); |
|
*w++ = ']'; |
|
*w = '\0'; |
|
*out_len = (size_t)(w - buf); |
|
return 0; |
|
} |
|
|
|
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 = CC(inst); |
|
if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1; |
|
*out_len = 0; |
|
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); |
|
char* w = buf; char* end = buf + buf_size; |
|
*w++ = '['; const char* sep = ""; |
|
char sql[256]; |
|
snprintf(sql, sizeof(sql), |
|
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags " |
|
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl); |
|
sqlite3_stmt* st = NULL; |
|
sqlite3_stmt* as = NULL; |
|
#define MEMBERS_APPEND(...) do { \ |
|
int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \ |
|
if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \ |
|
w += written; \ |
|
} while (0) |
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; } |
|
while (sqlite3_step(st) == SQLITE_ROW) { |
|
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); |
|
const char* name = (const char*)sqlite3_column_text(st, 1); |
|
int online = chat_core_member_online(inst, ch_id, nid); |
|
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(st, 2); |
|
const uint8_t* ed25519 = sqlite3_column_blob(st, 3); |
|
int x25519_len = sqlite3_column_bytes(st, 2); |
|
int ed25519_len = sqlite3_column_bytes(st, 3); |
|
const char* adm_tags = (const char*)sqlite3_column_text(st, 4); |
|
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1]; |
|
json_escape(name ? name : "", esc_name, sizeof(esc_name)); |
|
x25519_hex[0] = ed25519_hex[0] = '\0'; |
|
if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]); |
|
if (ed25519 && ed25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(ed25519_hex + i*2, 3, "%02x", ed25519[i]); |
|
|
|
/* check connected */ |
|
int connected = 0; |
|
if (inst && inst->connections) { |
|
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&nid); |
|
if (e) connected = 1; |
|
} |
|
|
|
json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags)); |
|
|
|
MEMBERS_APPEND( |
|
"%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d," |
|
"\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[", |
|
sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex); |
|
sep = ","; |
|
|
|
/* addresses */ |
|
char asql[200]; snprintf(asql, sizeof(asql), |
|
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol"); |
|
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) { |
|
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid); |
|
const char* asep2 = ""; |
|
while (sqlite3_step(as) == SQLITE_ROW) { |
|
int fam = sqlite3_column_int(as, 0); |
|
int proto = sqlite3_column_int(as, 1); |
|
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(as, 2); |
|
int addr_len = sqlite3_column_bytes(as, 2); |
|
int port = sqlite3_column_int(as, 3); |
|
int rtt = sqlite3_column_int(as, 4); |
|
char ip_str[64]; ip_str[0] = '\0'; |
|
if (fam == 4 && addr && addr_len >= 4) |
|
snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]); |
|
else if (fam == 6 && addr && addr_len >= 16) |
|
snprintf(ip_str, sizeof(ip_str), "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", |
|
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7], |
|
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]); |
|
else { snprintf(ip_str, sizeof(ip_str), "?"); } |
|
MEMBERS_APPEND( |
|
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}", |
|
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt); |
|
asep2 = ","; |
|
} |
|
sqlite3_finalize(as); as = NULL; |
|
} |
|
MEMBERS_APPEND("]}"); |
|
} |
|
MEMBERS_APPEND("]"); |
|
sqlite3_finalize(st); |
|
*out_len = (size_t)(w - buf); |
|
return 0; |
|
overflow: |
|
sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0; |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size); |
|
return -1; |
|
#undef MEMBERS_APPEND |
|
} |
|
|
|
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) { |
|
struct chat_core_ctx* cc = CC(inst); |
|
if (!cc || !cc->initialized || !out) return -1; |
|
out[0] = '\0'; |
|
sqlite3_stmt* st = NULL; |
|
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1; |
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); |
|
if (sqlite3_step(st) == SQLITE_ROW) { |
|
const char* n = (const char*)sqlite3_column_text(st, 0); |
|
if (n) snprintf(out, sz, "%s", n); |
|
} |
|
sqlite3_finalize(st); |
|
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; |
|
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 (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; |
|
} |
|
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 = CC(inst); |
|
if (!cc || !cc->initialized || !ch_id || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); 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; |
|
|
|
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 FROM \"%s\" WHERE node_id=?", peers_tbl); |
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) { |
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); |
|
if (sqlite3_step(st) == SQLITE_ROW) { |
|
const char* tags = (const char*)sqlite3_column_text(st, 0); |
|
if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) { |
|
sqlite3_finalize(st); chat_member_tags_free(t); return NULL; |
|
} |
|
} |
|
sqlite3_finalize(st); |
|
} |
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load ch=%s nid=0x%016llx keys=%d", |
|
CC_ID, ch_id, (unsigned long long)node_id, t->key_count); |
|
return t; |
|
} |
|
|
|
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) { |
|
if (!t || !key) 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) { |
|
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 = {0}; |
|
int rc = json_flat_parse(json, tags_json_cb, &parsed); |
|
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, "%s: tags_replace — invalid JSON", CC_ID); 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; |
|
} |
|
|
|
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t) { |
|
struct chat_core_ctx* cc = CC(inst); |
|
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1; |
|
} |
|
char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1; |
|
#define TAGS_ADVANCE(expr) do { \ |
|
int written = (expr); \ |
|
if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \ |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \ |
|
} \ |
|
off += written; \ |
|
} while (0) |
|
int vi = tags_find(t, "ver"); |
|
if (vi >= 0) ver = atoi(t->keys[vi].val) + 1; |
|
TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver)); |
|
|
|
for (int i = 0; i < t->key_count; i++) { |
|
struct chat_member_tags_key* k = &t->keys[i]; |
|
if (strcmp(k->key, "ver") == 0) continue; |
|
|
|
const char* out_val; |
|
if (k->raw) { |
|
uint64_t old_ts = 0; |
|
const char* old_eff = tags_append_effective(k->raw, &old_ts); |
|
int old_yes = (*old_eff == 'y'); |
|
int new_yes = (k->val[0] == 'y'); /* val = "yes" or "yes:<ts>" */ |
|
if (new_yes != old_yes) { |
|
uint64_t ts = (uint64_t)ntp_time_get_seconds(cc->inst); |
|
size_t oldl = strlen(k->raw); |
|
char* nraw = u_malloc(oldl + 24); |
|
if (!nraw) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — OOM for raw", CC_ID); return -1; } |
|
snprintf(nraw, oldl + 24, "%s%c%llu", k->raw, new_yes ? 'e' : 'd', (unsigned long long)ts); |
|
u_free(k->raw); k->raw = nraw; |
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit append %s=%c%llu ch=%s nid=0x%016llx old_last=%c", |
|
CC_ID, k->key, new_yes ? 'e' : 'd', (unsigned long long)ts, t->ch_id, (unsigned long long)t->node_id, old_yes ? 'e' : 'd'); |
|
} |
|
out_val = k->raw; |
|
} else { |
|
out_val = k->val; |
|
} |
|
char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1]; |
|
if (strlen(out_val) > MS_ADM_TAGS_MAX) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1; |
|
} |
|
json_escape(k->key, escaped_key, sizeof(escaped_key)); |
|
json_escape(out_val, escaped_value, sizeof(escaped_value)); |
|
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value)); |
|
} |
|
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}")); |
|
#undef TAGS_ADVANCE |
|
|
|
uint8_t ch_ed_priv[32]; |
|
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1; |
|
} |
|
uint8_t sign_msg[MS_ADM_TAGS_MAX + 8]; |
|
int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg)); |
|
if (mlen < 0) return -1; |
|
uint8_t sig[64]; |
|
if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1; |
|
} |
|
|
|
char peers_tbl[80]; peers_table_name(t->ch_id, peers_tbl, sizeof(peers_tbl)); |
|
sqlite3_stmt* st = NULL; |
|
char sql[384]; snprintf(sql, sizeof(sql), |
|
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage, source" |
|
" FROM \"%s\" WHERE node_id=?", peers_tbl); |
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1; |
|
} |
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)t->node_id); |
|
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member not found ch=%s nid=0x%016llx", CC_ID, t->ch_id, (unsigned long long)t->node_id); return -1; |
|
} |
|
const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(st, 0); |
|
const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(st, 1); |
|
const uint8_t* jsig = (const uint8_t*)sqlite3_column_blob(st, 2); |
|
uint64_t jts = (uint64_t)sqlite3_column_int64(st, 3); |
|
const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(st, 4); |
|
uint64_t uts = (uint64_t)sqlite3_column_int64(st, 5); |
|
const char* uinfo = (const char*)sqlite3_column_text(st, 6); |
|
int stg = sqlite3_column_int(st, 7); |
|
int src = sqlite3_column_int(st, 8); |
|
if (!x25 || !ed) { sqlite3_finalize(st); |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member has no keys ch=%s", CC_ID, t->ch_id); return -1; |
|
} |
|
if (src != 0) { sqlite3_finalize(st); |
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — cannot set flags on placeholder member (source=%d) ch=%s nid=0x%016llx", |
|
CC_ID, src, t->ch_id, (unsigned long long)t->node_id); |
|
return -1; |
|
} |
|
const char* stg_val = chat_member_tags_get(t, "storage"); |
|
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1; |
|
|
|
int rc = member_sync_put(inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts, |
|
uinfo ? uinfo : "", json, sig, stg, 0, NULL); |
|
sqlite3_finalize(st); |
|
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; } |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d", |
|
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off); |
|
return 0; |
|
} |
|
|
|
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); |
|
} |
|
|
|
/* ─── 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 || !CC(req->inst) || !CC(req->inst)->initialized) { 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 (req->json[0]) chat_member_tags_replace(t, req->json); |
|
chat_member_tags_commit(req->inst, t); |
|
chat_member_tags_free(t); |
|
u_free(arg); |
|
}
|
|
|