/* * chat_core.c — жизненный цикл чата: init, destroy, глобальное состояние g_cc * * Под-модули (каждый в своём .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 "gui_bridge.h" #include "../../../src/utun_instance.h" #include "../../../src/ntp_time.h" #include "topo_node_sqlite.h" #include "../../../lib/mem.h" #include #include #include "../../../lib/platform_compat.h" /* ─── глобальное состояние (extern в chat_core_priv.h) ─── */ struct chat_core_ctx g_cc; /* ─── хелперы ─── */ 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); } int db_exec(const char* sql) { char* err = NULL; int rc = sqlite3_exec(g_cc.db, sql, NULL, NULL, &err); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: sql error: %s", CC_ID, err); sqlite3_free(err); } return rc; } /* ─── db_sync registry ─── */ struct DB_SYNC_INSTANCE* si_find(const char* ch_id) { for (int i = 0; i < g_cc.si_count; i++) if (strcmp(g_cc.si_ch_id[i], ch_id) == 0) return g_cc.si[i]; return NULL; } void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) { if (g_cc.si_count >= g_cc.si_capacity) { int nc = g_cc.si_capacity ? g_cc.si_capacity * 2 : 8; g_cc.si = u_realloc(g_cc.si, nc * sizeof(void*)); g_cc.si_ch_id = u_realloc(g_cc.si_ch_id, nc * sizeof(char*)); g_cc.si_capacity = nc; } g_cc.si[g_cc.si_count] = si; g_cc.si_ch_id[g_cc.si_count] = u_strdup(ch_id); g_cc.si_count++; uint32_t msg_cnt = db_sync_count(si); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%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) return -1; memset(&g_cc, 0, sizeof(g_cc)); if (inst->topo_sqlite_db) { g_cc.db = inst->topo_sqlite_db; g_cc.shared_db = 1; DEBUG_INFO(DEBUG_CATEGORY_CONNECTIVITY, "%s: using shared SQLite db=%p", CC_ID, (void*)g_cc.db); } else { int rc = sqlite3_open_v2(db_path, &g_cc.db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTIVITY, "%s: cannot open DB %s: %s", CC_ID, db_path, sqlite3_errmsg(g_cc.db)); sqlite3_close(g_cc.db); g_cc.db = NULL; return -1; } sqlite3_exec(g_cc.db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); sqlite3_exec(g_cc.db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); } g_cc.inst = inst; g_cc.my_node_id = inst->node_id; 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(g_cc.db, g_cc.my_node_id, my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey, (uint64_t)now_sec, (time_t)now_sec); topo_node_sqlite_node_set_online(g_cc.db, g_cc.my_node_id, 1); sqlite3_stmt* st = NULL; sqlite3_prepare_v2(g_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)g_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); } } g_cc.initialized = 1; chat_core_sync_my_addresses(); db_exec( "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" ");" ); /* seed stub accounts if empty */ { sqlite3_stmt* cnt_st = NULL; if (sqlite3_prepare_v2(g_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_CONNECTIVITY, "%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(g_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(g_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(ch); loaded++; } } sqlite3_finalize(stmt); DEBUG_INFO(DEBUG_CATEGORY_CONNECTIVITY, "%s: loaded %d channels from DB", CC_ID, loaded); } } DEBUG_INFO(DEBUG_CATEGORY_CONNECTIVITY, "%s: initialized, db=%s node_id=0x%016llx", CC_ID, db_path, (unsigned long long)g_cc.my_node_id); { uint8_t nid[8]; memcpy(nid, &g_cc.my_node_id, 8); gui_bridge_post(GUI_EVT_MY_NODE_ID, nid, 8); } gui_bridge_post(GUI_EVT_DB_READY, NULL, 0); return 0; } void chat_core_destroy(struct UTUN_INSTANCE* inst) { (void)inst; if (!g_cc.initialized) return; g_cc.initialized = 0; for (int i = 0; i < g_cc.si_count; i++) u_free(g_cc.si_ch_id[i]); u_free(g_cc.si_ch_id); u_free(g_cc.si); g_cc.si = NULL; g_cc.si_ch_id = NULL; g_cc.si_count = g_cc.si_capacity = 0; if (g_cc.db && !g_cc.shared_db) { sqlite3_close(g_cc.db); } g_cc.db = NULL; g_cc.inst = NULL; DEBUG_INFO(DEBUG_CATEGORY_CONNECTIVITY, "%s: destroyed", CC_ID); } sqlite3* chat_core_get_db(void) { return g_cc.db; } struct UTUN_INSTANCE* chat_core_get_inst(void) { return g_cc.inst; } int chat_core_is_initialized(void) { return g_cc.initialized; }