/* * 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 #include #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:" */ 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); }