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.
236 lines
9.2 KiB
236 lines
9.2 KiB
/* |
|
* 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 <string.h> |
|
#include <stdio.h> |
|
#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; }
|
|
|