From de7841f33aee1fc66122855050090fc7499b635a Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 12 Jul 2026 17:39:20 +0300 Subject: [PATCH] chatgui: create group, unified logging, fix uasync_post epoll bug - Add CreateGroupDialog with group name input - Generate X25519+Ed25519 keypairs, random uint64 channel_id, Ed25519 signature, write to DB via uasync thread - chat_core_create_channel(): create msg table, topo_node_sqlite_channel_put() - GUI_EVT_CHANNEL_UPDATED notification to reload channel list - Per-category debug config: debug_level= + debug_categories= in [gui] - Unified logging via debug_config: all threads write to same file/console - debug_enable_console(1) to mirror file output to stderr - DEBUG_LEVEL_DISABLED=-1: truly disable category, NONE=0 fallback to global - Fix uasync_post under epoll: process_posted_tasks was never called - Fix MainWindow constructor parameter ordering (dbPath vs debugFile) - debug_ui.h: append mode ('a' instead of 'w') --- lib/debug_config.c | 20 +- lib/debug_config.h | 13 +- lib/u_async.c | 1 + src/topo_group.c | 35 +++- src/topo_group.h | 14 +- tools/chatgui/CMakeLists.txt | 4 +- tools/chatgui/db/db_manager.cpp | 164 ++-------------- tools/chatgui/libutun/CMakeLists.txt | 2 + tools/chatgui/src/channellist.cpp | 9 + tools/chatgui/src/channellist.h | 1 + tools/chatgui/src/creategroupdialog.cpp | 51 +++++ tools/chatgui/src/creategroupdialog.h | 15 ++ tools/chatgui/src/debug_ui.h | 2 +- tools/chatgui/src/main.cpp | 7 +- tools/chatgui/src/mainwindow.cpp | 140 +++++++++++++- tools/chatgui/src/mainwindow.h | 8 +- tools/chatgui/transport/chat_core.c | 167 +++++++++------- tools/chatgui/transport/chat_core.h | 17 ++ tools/chatgui/transport/gui_bridge.h | 7 + tools/chatgui/transport/gui_bridge_impl.cpp | 42 ++-- tools/chatgui/transport/node_config.cpp | 6 +- tools/chatgui/transport/node_config.h | 4 + tools/chatgui/transport/topo_node_sqlite.c | 203 ++++++++++++++++++++ tools/chatgui/transport/topo_node_sqlite.h | 24 +++ tools/chatgui/transport/utun_node.cpp | 62 +++++- tools/chatgui/transport/utun_node.h | 4 + 26 files changed, 759 insertions(+), 263 deletions(-) create mode 100644 tools/chatgui/src/creategroupdialog.cpp create mode 100644 tools/chatgui/src/creategroupdialog.h create mode 100644 tools/chatgui/transport/topo_node_sqlite.c create mode 100644 tools/chatgui/transport/topo_node_sqlite.h diff --git a/lib/debug_config.c b/lib/debug_config.c index 8ab6e8b5..f3134de0 100644 --- a/lib/debug_config.c +++ b/lib/debug_config.c @@ -137,13 +137,14 @@ static const struct { const char* name; debug_level_t level; } g_level_table[] = { - {"none", DEBUG_LEVEL_NONE}, - {"error", DEBUG_LEVEL_ERROR}, - {"warn", DEBUG_LEVEL_WARN}, - {"info", DEBUG_LEVEL_INFO}, - {"debug", DEBUG_LEVEL_DEBUG}, - {"trace", DEBUG_LEVEL_TRACE}, - {NULL, DEBUG_LEVEL_NONE} + {"disabled", DEBUG_LEVEL_DISABLED}, + {"none", DEBUG_LEVEL_NONE}, + {"error", DEBUG_LEVEL_ERROR}, + {"warn", DEBUG_LEVEL_WARN}, + {"info", DEBUG_LEVEL_INFO}, + {"debug", DEBUG_LEVEL_DEBUG}, + {"trace", DEBUG_LEVEL_TRACE}, + {NULL, DEBUG_LEVEL_NONE} }; debug_category_t get_category_by_name(const char* name) { @@ -163,7 +164,7 @@ debug_level_t debug_level_from_name(const char* name) { return g_level_table[i].level; } } - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid debug level '%s' (valid: none,error,warn,info,debug,trace)", name); + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid debug level '%s' (valid: disabled,none,error,warn,info,debug,trace)", name); return DEBUG_LEVEL_INFO; /* fallback */ } @@ -345,6 +346,9 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx) { } debug_level_t cat_level = g_debug_config.category_levels[category_idx]; + if (cat_level == DEBUG_LEVEL_DISABLED) { + return 0; + } if (cat_level == DEBUG_LEVEL_NONE) { cat_level = g_debug_config.level; } diff --git a/lib/debug_config.h b/lib/debug_config.h index bc2236d7..d2114f98 100644 --- a/lib/debug_config.h +++ b/lib/debug_config.h @@ -26,12 +26,13 @@ extern "C" { /* Debug levels */ typedef enum { - DEBUG_LEVEL_NONE = 0, // No debug output - DEBUG_LEVEL_ERROR = 1, // Errors only - DEBUG_LEVEL_WARN = 2, // Warnings and errors - DEBUG_LEVEL_INFO = 3, // Info, warnings, errors - DEBUG_LEVEL_DEBUG = 4, // Full debug output - DEBUG_LEVEL_TRACE = 5 // Trace everything + DEBUG_LEVEL_DISABLED = -1, // Category completely disabled, never outputs + DEBUG_LEVEL_NONE = 0, // Fallback to global level + DEBUG_LEVEL_ERROR = 1, // Errors only + DEBUG_LEVEL_WARN = 2, // Warnings and errors + DEBUG_LEVEL_INFO = 3, // Info, warnings, errors + DEBUG_LEVEL_DEBUG = 4, // Full debug output + DEBUG_LEVEL_TRACE = 5 // Trace everything } debug_level_t; /* Debug categories as indices (0-based) */ diff --git a/lib/u_async.c b/lib/u_async.c index c70360f9..8ceb0c72 100644 --- a/lib/u_async.c +++ b/lib/u_async.c @@ -924,6 +924,7 @@ static void process_epoll_events(struct UASYNC* ua, struct epoll_event* events, // Wakeup fd detected by data.fd < 0 (lower 32 bits of data.u64 = -1) if (events[i].data.fd < 0) { if (events[i].events & EPOLLIN) { drain_wakeup_pipe(ua); } + process_posted_tasks(ua); continue; } diff --git a/src/topo_group.c b/src/topo_group.c index 8f9f0230..0962bdf5 100644 --- a/src/topo_group.c +++ b/src/topo_group.c @@ -22,6 +22,9 @@ #include "route_ping.h" #include "route_connectivity.h" #include "control_server.h" +#ifdef USE_SQLITE +#include "../../tools/chatgui/transport/topo_node_sqlite.h" +#endif #define TOPO_GROUP_UTUN 0x8000000000000000ULL @@ -221,7 +224,8 @@ static struct TOPO_GROUP* topo_group_create(struct UTUN_INSTANCE* instance, uint sc_derive_ed25519_pubkey(instance->my_keys.private_key, group->ed25519_public_key); DEBUG_INFO(DEBUG_CATEGORY_BGP, "Ed25519 pubkey derived from X25519 private key"); - if (instance->config && instance->config->global.db_path[0]) topo_node_lmdb_init(group, instance->config->global.db_path); + if (group_type == TOPO_GROUP_TYPE_UTUN && instance->config && instance->config->global.db_path[0]) + topo_node_lmdb_init(group, instance->config->global.db_path); group->nodes = queue_new(instance->ua, BGP_NODES_HASH_SIZE, offsetof(struct TOPO_NODEQ, hash_node_id) - sizeof(struct ll_entry), 8, "group_nodes"); if (!group->nodes) { queue_entry_free(qe); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "nodes queue creation failed"); return NULL; } @@ -246,7 +250,7 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { if (group->local_node) { topo_node_free_lists(group, group->local_node); u_free(group->local_node); } - topo_node_lmdb_destroy(group); + if (group->group_type == TOPO_GROUP_TYPE_UTUN) topo_node_lmdb_destroy(group); if (group->nodes) queue_free(group->nodes); } @@ -322,16 +326,25 @@ struct TOPO_GROUP* topo_groups_find(struct TOPO_GROUPS* g, uint64_t group_id) { return e ? (struct TOPO_GROUP*)e : NULL; } -struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type) { +struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type, const char* channel_id) { if (!g || !g->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "invalid args"); return NULL; } if (topo_groups_find(g, group_id)) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "group_id=%016llx already exists", (unsigned long long)group_id); return NULL; } struct TOPO_GROUP* group = topo_group_create(g->instance, group_id, group_type); if (!group) return NULL; + if (channel_id) { strncpy(group->channel_id, channel_id, sizeof(group->channel_id) - 1); group->channel_id[sizeof(group->channel_id) - 1] = '\0'; } queue_data_put_with_index(g->group_list, &group->ll); - DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u", (unsigned long long)group_id, group_type); + DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u ch_id=%s", (unsigned long long)group_id, group_type, group->channel_id); return group; } +#ifdef USE_SQLITE +void topo_groups_set_sqlite_db(struct TOPO_GROUPS* g, sqlite3* db) { + if (!g) return; + g->topo_sqlite_db = db; + DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_sqlite_db set"); +} +#endif + void topo_group_set_nat_check_local(struct TOPO_GROUP* group, int allow) { if (!group) return; group->allow_nat_check_local = allow ? 1 : 0; } void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { @@ -612,7 +625,15 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from topo_group_add_path(nodeinfo1, from, hop_list, nodeinfo1->hop_count); if (group->group_type != TOPO_GROUP_TYPE_CHAT && group->instance->rt) route_insert(group->instance->rt, nodeinfo1); - if (node_id != group->instance->node_id) topo_node_lmdb_put(group, nodeinfo1); + if (node_id != group->instance->node_id) { +#ifdef USE_SQLITE + if (group->group_type == TOPO_GROUP_TYPE_CHAT && group->instance->topo_groups->topo_sqlite_db) { + topo_node_sqlite_node_put(group->instance->topo_groups->topo_sqlite_db, nodeinfo1); + if (group->channel_id[0]) topo_node_sqlite_member_put(group->instance->topo_groups->topo_sqlite_db, group->channel_id, node_id); + } else +#endif + topo_node_lmdb_put(group, nodeinfo1); + } if (group->instance->control_srv) control_server_notify_node_change(group->instance->control_srv, nodeinfo1); int hop_count = nodeinfo1->hop_count; @@ -659,6 +680,10 @@ int topo_group_process_withdraw(struct TOPO_GROUP* group, struct ETCP_CONN* send struct ll_entry* entry = queue_find_data_by_index(group->nodes, &node_id); if (entry) { queue_remove_data(group->nodes, entry); queue_entry_free(entry); } topo_group_broadcast_withdraw(group, node_id, wd_source, sender); +#ifdef USE_SQLITE + if (group->group_type == TOPO_GROUP_TYPE_CHAT && group->instance->topo_groups->topo_sqlite_db) + topo_node_sqlite_member_del(group->instance->topo_groups->topo_sqlite_db, group->channel_id, node_id); +#endif } return 0; } diff --git a/src/topo_group.h b/src/topo_group.h index 2bad6fcd..53178179 100644 --- a/src/topo_group.h +++ b/src/topo_group.h @@ -14,6 +14,9 @@ extern "C" { #include "route_lib.h" #include "topo_node.h" #include "secure_channel.h" +#ifdef USE_SQLITE +#include +#endif // ETCP ID для пакетов топологии @@ -114,6 +117,7 @@ struct TOPO_GROUP { uint32_t next_ping_req_id; uint8_t allow_nat_check_local; uint8_t ed25519_public_key[SC_PUBKEY_SIZE]; + char channel_id[64]; // channel_id для групп типа CHAT MDB_env* nodeinfo_env; MDB_dbi nodeinfo_dbi; }; @@ -130,6 +134,9 @@ struct TOPO_GROUPS { struct memory_pool* v6_addr_pool; struct memory_pool* v4_subnet_pool; struct memory_pool* v6_subnet_pool; +#ifdef USE_SQLITE + sqlite3* topo_sqlite_db; +#endif }; /** @@ -163,9 +170,10 @@ struct TOPO_GROUP* topo_groups_find(struct TOPO_GROUPS* g, uint64_t group_id); * @param g контейнер групп * @param group_id уникальный идентификатор * @param group_type TOPO_GROUP_TYPE_UTUN или TOPO_GROUP_TYPE_CHAT + * @param channel_id идентификатор канала для групп типа CHAT (может быть NULL) * @return TOPO_GROUP или NULL при ошибке */ -struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type); +struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t group_id, uint8_t group_type, const char* channel_id); /** * @brief Добавляет conn в senders_list (если нет), отправляет запрос таблицы (nodeinfo). @@ -276,6 +284,10 @@ struct nat_check_arg { uint16_t nat_port; }; +#ifdef USE_SQLITE +void topo_groups_set_sqlite_db(struct TOPO_GROUPS* g, sqlite3* db); +#endif + #ifdef __cplusplus } diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 43988ee5..6be0904a 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -49,6 +49,7 @@ add_executable(chatgui src/main.cpp src/mainwindow.cpp src/channellist.cpp + src/creategroupdialog.cpp src/channeldelegate.cpp src/chatview.cpp src/messagedelegate.cpp @@ -73,13 +74,14 @@ add_executable(chatgui transport/chat_core.c transport/chat_sync.c transport/db_sync_stub.c + transport/topo_node_sqlite.c db/db_manager.cpp db/sqlite3.c resources/chatgui.qrc ) target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/db) -target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1) +target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1 USE_SQLITE) set_source_files_properties(db/sqlite3.c transport/chat_core.c transport/chat_sync.c transport/db_sync_stub.c PROPERTIES LANGUAGE C) if(WIN32) target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread) diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 9d2dd3c0..4a1275d8 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -8,7 +8,6 @@ /* ── helpers ── */ static const char kZero64[64] = {}; -static const char kZero32[32] = {}; QString DbManager::sanitizeTableName(const QString& chId) { QString s; @@ -119,28 +118,6 @@ void DbManager::initTables() { " updated_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE TABLE IF NOT EXISTS nodes (" - " node_id INTEGER PRIMARY KEY," - " name TEXT," - " x25519_pubkey BLOB NOT NULL," - " ed25519_pubkey BLOB," - " last_seen_at INTEGER," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - - "CREATE TABLE IF NOT EXISTS node_addresses (" - " id INTEGER PRIMARY KEY AUTOINCREMENT," - " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," - " family INTEGER NOT NULL CHECK(family IN (4, 6))," - " protocol INTEGER NOT NULL DEFAULT 1," - " address BLOB NOT NULL," - " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," - " rtt INTEGER," - " is_nat INTEGER DEFAULT 0," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" - "CREATE TABLE IF NOT EXISTS accounts (" " node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id)," " display_name TEXT NOT NULL," @@ -150,21 +127,6 @@ void DbManager::initTables() { " created_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE TABLE IF NOT EXISTS channels (" - " channel_id TEXT PRIMARY KEY," - " name TEXT NOT NULL," - " owner_node_id INTEGER," - " is_dm INTEGER DEFAULT 0," - " x25519_pubkey BLOB NOT NULL," - " x25519_privkey BLOB," - " ed25519_pubkey BLOB NOT NULL," - " ed25519_privkey BLOB," - " signature BLOB NOT NULL," - " last_read_msg_id INTEGER," - " last_pos_msg_id INTEGER," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - "CREATE TABLE IF NOT EXISTS ui_state (" " key TEXT PRIMARY KEY," " value TEXT" @@ -213,12 +175,11 @@ bool DbManager::createChannel(const QString& chId, const QString& name, quint64 ownerNodeId, const QByteArray& x25519pub, const QByteArray& x25519priv, const QByteArray& ed25519pub, const QByteArray& ed25519priv) { - GUI_INFO("createChannel: chId='%s' name='%s'", chId.toUtf8().constData(), name.toUtf8().constData()); - QByteArray sig = kZero64; Q_UNUSED(sig); + GUI_INFO("createChannel (msg table only): chId='%s' name='%s'", chId.toUtf8().constData(), name.toUtf8().constData()); + (void)ownerNodeId; (void)x25519pub; (void)x25519priv; (void)ed25519pub; (void)ed25519priv; - QString msgt = msgTableName(chId), pt = peersTableName(chId); + QString msgt = msgTableName(chId); - /* per-channel message table (DDL — до INSERT, чтобы не ломать транзакцию) */ QString csql = QStringLiteral( "CREATE TABLE IF NOT EXISTS \"%1\" (" " id INTEGER PRIMARY KEY AUTOINCREMENT," @@ -237,41 +198,7 @@ bool DbManager::createChannel(const QString& chId, const QString& name, csql = QStringLiteral("CREATE INDEX IF NOT EXISTS idx_%1_ts ON \"%1\"(timestamp, datahash)").arg(msgt); sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr); - /* per-channel peers table */ - csql = QStringLiteral( - "CREATE TABLE IF NOT EXISTS \"%1\" (" - " node_id INTEGER NOT NULL," - " join_sig BLOB NOT NULL," - " creator_sig BLOB," - " comment TEXT," - " joined_at INTEGER DEFAULT (unixepoch())," - " PRIMARY KEY (node_id))").arg(pt); - sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr); - - /* вставка записи channels (после DDL, корректно внутри транзакции) */ - sqlite3_stmt* stmt = prepareOrNull( - "INSERT INTO channels(channel_id,name,owner_node_id,is_dm," - " x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature)" - " VALUES(?,?,?,0,?,?,?,?,?)"); - if (!stmt) return false; - sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT); - sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT); - sqlite3_bind_int64(stmt, 3, (sqlite3_int64)ownerNodeId); - sqlite3_bind_blob(stmt, 4, x25519pub.isEmpty() ? kZero32 : x25519pub.constData(), - x25519pub.isEmpty() ? 32 : x25519pub.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 5, x25519priv.isEmpty() ? nullptr : x25519priv.constData(), - x25519priv.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 6, ed25519pub.isEmpty() ? kZero32 : ed25519pub.constData(), - ed25519pub.isEmpty() ? 32 : ed25519pub.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 7, ed25519priv.isEmpty() ? nullptr : ed25519priv.constData(), - ed25519priv.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 8, sig.constData(), sig.size(), SQLITE_TRANSIENT); - bool ok = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - if (!ok) { GUI_ERROR("createChannel %s failed: %s", chId.toUtf8().constData(), sqlite3_errmsg(m_db)); return false; } - - GUI_INFO("Channel created: %s (msg=%s, peers=%s)", - chId.toUtf8().constData(), msgt.toUtf8().constData(), pt.toUtf8().constData()); + GUI_INFO("Message table ensured: %s", msgt.toUtf8().constData()); return true; } @@ -284,6 +211,7 @@ bool DbManager::deleteChannel(const QString& chId) { sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT); sqlite3_step(stmt); sqlite3_finalize(stmt); + GUI_INFO("Channel deleted: %s", chId.toUtf8().constData()); return true; } @@ -349,73 +277,32 @@ bool DbManager::insertMessage(const QString& chId, quint64 nodeId, return ok; } -/* ── peers ── */ +/* ── peers (запись теперь через topo_node_sqlite) ── */ bool DbManager::addPeer(const QString& chId, quint64 nodeId, const QByteArray& joinSig, const QByteArray& creatorSig, const QString& comment) { - QString sql = QStringLiteral( - "INSERT OR REPLACE INTO \"%1\"(node_id, join_sig, creator_sig, comment)" - " VALUES(?,?,?,?)").arg(peersTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) return false; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - sqlite3_bind_blob(stmt, 2, joinSig.constData(), joinSig.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 3, creatorSig.isEmpty() ? nullptr : creatorSig.constData(), - creatorSig.size(), SQLITE_TRANSIENT); - if (!comment.isEmpty()) - sqlite3_bind_text(stmt, 4, comment.toUtf8().constData(), -1, SQLITE_TRANSIENT); - else - sqlite3_bind_null(stmt, 4); - bool ok = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - return ok; + (void)chId; (void)nodeId; (void)joinSig; (void)creatorSig; (void)comment; + return true; } bool DbManager::removePeer(const QString& chId, quint64 nodeId) { - QString sql = QStringLiteral( - "DELETE FROM \"%1\" WHERE node_id=?").arg(peersTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) return false; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - sqlite3_step(stmt); - sqlite3_finalize(stmt); + (void)chId; (void)nodeId; return true; } -/* ── nodes ── */ +/* ── nodes (запись теперь через topo_node_sqlite) ── */ bool DbManager::upsertNode(quint64 nodeId, const QString& name, const QByteArray& x25519pub, const QByteArray& ed25519pub) { - sqlite3_stmt* stmt = prepareOrNull( - "INSERT OR REPLACE INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, last_seen_at)" - " VALUES(?,?,?,?,unixepoch())"); - if (!stmt) return false; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 3, x25519pub.constData(), x25519pub.size(), SQLITE_TRANSIENT); - sqlite3_bind_blob(stmt, 4, ed25519pub.isEmpty() ? nullptr : ed25519pub.constData(), - ed25519pub.size(), SQLITE_TRANSIENT); - bool ok = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - return ok; + (void)nodeId; (void)name; (void)x25519pub; (void)ed25519pub; + return true; } bool DbManager::addNodeAddress(quint64 nodeId, int family, int protocol, const QByteArray& addr, quint16 port, bool isNat) { - sqlite3_stmt* stmt = prepareOrNull( - "INSERT OR IGNORE INTO node_addresses(node_id, family, protocol, address, port, is_nat)" - " VALUES(?,?,?,?,?,?)"); - if (!stmt) return false; - sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId); - sqlite3_bind_int(stmt, 2, family); - sqlite3_bind_int(stmt, 3, protocol); - sqlite3_bind_blob(stmt, 4, addr.constData(), addr.size(), SQLITE_TRANSIENT); - sqlite3_bind_int(stmt, 5, port); - sqlite3_bind_int(stmt, 6, isNat ? 1 : 0); - bool ok = sqlite3_step(stmt) == SQLITE_DONE; - sqlite3_finalize(stmt); - return ok; + (void)nodeId; (void)family; (void)protocol; (void)addr; (void)port; (void)isNat; + return true; } QByteArray DbManager::loadNodeInfo(quint64 nodeId) const { @@ -755,7 +642,7 @@ void DbManager::setNodeOnline(quint64 nodeId, bool online) { /* ── seed stub data ── */ void DbManager::seedStubData() { - GUI_INFO("Seeding stub data..."); + GUI_INFO("Seeding stub data (accounts only)..."); sqlite3_exec(m_db, "BEGIN", nullptr, nullptr, nullptr); unsigned char pub_dummy[32] = {}; @@ -773,14 +660,6 @@ void DbManager::seedStubData() { {5, "Eve", "E", "#00B894"}, }; for (auto& a : accs) { - ins = prepareOrNull("INSERT OR IGNORE INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey,last_seen_at)" - " VALUES(?,?,?,?,unixepoch())"); - sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id); - sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_TRANSIENT); - sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_TRANSIENT); - sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_TRANSIENT); - sqlite3_step(ins); sqlite3_finalize(ins); - ins = prepareOrNull("INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)" " VALUES(?,?,?,?)"); sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id); @@ -790,17 +669,6 @@ void DbManager::seedStubData() { sqlite3_step(ins); sqlite3_finalize(ins); } - /* channels (1 демо-канал) */ - QByteArray dummyPub(32, '\0'), dummySig(64, '\0'), dummyCH(32, '\0'); - createChannel("ch:general", "#general", 0, dummyPub, QByteArray(), dummyPub, QByteArray()); - - /* peers */ - for (auto& a : accs) { - addPeer("ch:general", a.id, dummySig, dummySig, a.name); - } - - /* messages — 0 для простоты, добавим позже в seed v2 */ - sqlite3_exec(m_db, "COMMIT", nullptr, nullptr, nullptr); - GUI_INFO("Seed data inserted (5 accounts, 1 channel)"); + GUI_INFO("Seed data inserted (5 accounts)"); } diff --git a/tools/chatgui/libutun/CMakeLists.txt b/tools/chatgui/libutun/CMakeLists.txt index 66ab5649..24057018 100644 --- a/tools/chatgui/libutun/CMakeLists.txt +++ b/tools/chatgui/libutun/CMakeLists.txt @@ -119,10 +119,12 @@ target_include_directories(utun PUBLIC ${LIB_DIR} ${TRANSPORT_DIR} ${SRC_DIR}/uip + ${CMAKE_SOURCE_DIR}/db ) target_compile_definitions(utun PRIVATE USE_OPENSSL + USE_SQLITE ) set_target_properties(utun PROPERTIES diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 7af60da9..0ddc9f14 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -67,6 +67,15 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) fl->addStretch(); + auto *createBtn = new QPushButton("GRP", footer); + createBtn->setFixedSize(36, 28); + createBtn->setStyleSheet( + "QPushButton { border: 1.5px solid #27AE60; border-radius: 6px;" + " font-size: 11px; font-weight: bold; color: #27AE60; background: transparent; }" + "QPushButton:hover { background: #27AE60; color: white; }"); + connect(createBtn, &QPushButton::clicked, this, [this] { emit createGroupRequested(); }); + fl->addWidget(createBtn); + auto *plusBtn = new QPushButton("+", footer); plusBtn->setFixedSize(28, 28); plusBtn->setStyleSheet( diff --git a/tools/chatgui/src/channellist.h b/tools/chatgui/src/channellist.h index 68dbeed4..176025c2 100644 --- a/tools/chatgui/src/channellist.h +++ b/tools/chatgui/src/channellist.h @@ -19,6 +19,7 @@ signals: void channelSelected(const QString& channelId); void inviteRequested(const QString& channelId); void joinRequested(); + void createGroupRequested(); void settingsRequested(); private slots: diff --git a/tools/chatgui/src/creategroupdialog.cpp b/tools/chatgui/src/creategroupdialog.cpp new file mode 100644 index 00000000..06fa52d8 --- /dev/null +++ b/tools/chatgui/src/creategroupdialog.cpp @@ -0,0 +1,51 @@ +#include "creategroupdialog.h" +#include +#include +#include +#include + +CreateGroupDialog::CreateGroupDialog(QWidget* parent) + : QDialog(parent) + , m_nameEdit(new QLineEdit(this)) +{ + setWindowTitle("Создать группу"); + setMinimumWidth(350); + setModal(true); + + auto* layout = new QVBoxLayout(this); + + auto* nameLabel = new QLabel("Название группы:", this); + layout->addWidget(nameLabel); + + m_nameEdit->setPlaceholderText("Введите название..."); + m_nameEdit->setMaxLength(128); + layout->addWidget(m_nameEdit); + + layout->addSpacing(12); + + auto* btnLayout = new QHBoxLayout(); + btnLayout->addStretch(); + + auto* cancelBtn = new QPushButton("Отмена", this); + connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); + btnLayout->addWidget(cancelBtn); + + auto* createBtn = new QPushButton("Создать", this); + createBtn->setDefault(true); + connect(createBtn, &QPushButton::clicked, this, [this]() { + if (m_nameEdit->text().trimmed().isEmpty()) { + m_nameEdit->setFocus(); + return; + } + accept(); + }); + btnLayout->addWidget(createBtn); + + layout->addLayout(btnLayout); + + m_nameEdit->setFocus(); +} + +QString CreateGroupDialog::groupName() const { + return m_nameEdit->text().trimmed(); +} diff --git a/tools/chatgui/src/creategroupdialog.h b/tools/chatgui/src/creategroupdialog.h new file mode 100644 index 00000000..f947d05a --- /dev/null +++ b/tools/chatgui/src/creategroupdialog.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class CreateGroupDialog : public QDialog { + Q_OBJECT +public: + explicit CreateGroupDialog(QWidget* parent = nullptr); + + QString groupName() const; + +private: + QLineEdit* m_nameEdit; +}; diff --git a/tools/chatgui/src/debug_ui.h b/tools/chatgui/src/debug_ui.h index 775e9ce1..9ec0679b 100644 --- a/tools/chatgui/src/debug_ui.h +++ b/tools/chatgui/src/debug_ui.h @@ -10,7 +10,7 @@ namespace GuiDebugPriv { inline void setFile(const char *path) { if (s_file && s_file != stderr) fclose(s_file); - s_file = path ? fopen(path, "w") : nullptr; + s_file = path ? fopen(path, "a") : nullptr; if (!s_file) s_file = stderr; } } diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 9ea8b351..3b40b36b 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -5,7 +5,6 @@ #include "mainwindow.h" #include "../db/db_manager.h" #include "../transport/node_config.h" -#include "debug_ui.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); @@ -55,16 +54,14 @@ int main(int argc, char *argv[]) { cfg.save(); } - if (!cfg.debugFile().isEmpty()) - guiDebugFile(cfg.debugFile().toUtf8().constData()); - QString dbPath = cfg.dbPath(); if (dbPath.isEmpty()) dbPath = "chats.db"; if (!QDir::isAbsolutePath(dbPath)) dbPath = QCoreApplication::applicationDirPath() + "/" + dbPath; DbManager db(dbPath); - MainWindow w(nullptr, &db, cfgPath, cfg.debugFile()); + MainWindow w(nullptr, &db, cfgPath, dbPath, cfg.debugFile(), + cfg.debugLevel(), cfg.debugCategories()); w.show(); return app.exec(); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index e1635e57..e794efa7 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -5,6 +5,7 @@ #include "invitedialog.h" #include "joindialog.h" #include "settingsdialog.h" +#include "creategroupdialog.h" #include "invite_link.h" #include "../db/db_manager.h" #include "../transport/utun_node.h" @@ -16,6 +17,14 @@ #include #include +extern "C" { +#include "../transport/chat_core.h" +#include "../../lib/mem.h" +#include "../../lib/debug_config.h" +#include +#include +} + static MainWindow* s_mainWindow = nullptr; static void onMsgReceivedCallback(const char* ch_id, int ch_id_len) { @@ -24,8 +33,16 @@ static void onMsgReceivedCallback(const char* ch_id, int ch_id_len) { s_mainWindow->onMessageReceived(ch_id); } +static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) { + Q_UNUSED(ch_id_len); + Q_UNUSED(ch_id); + if (s_mainWindow) + s_mainWindow->reloadChannels(); +} + MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, - const QString& dbPath, const QString& debugFile) + const QString& dbPath, const QString& debugFile, + const QString& debugLevel, const QString& debugCategories) : QMainWindow(parent) , m_channelList(nullptr) , m_messageList(nullptr) @@ -36,6 +53,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, , m_cfgPath(cfgPath) , m_cfgDbPath(dbPath) , m_cfgDebugFile(debugFile) + , m_cfgDebugLevel(debugLevel) + , m_cfgDebugCategories(debugCategories) { setWindowTitle("Chat"); resize(900, 600); @@ -45,6 +64,7 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, if (m_db && m_db->isOpen()) { s_mainWindow = this; gui_bridge_set_msg_received_cb(onMsgReceivedCallback); + gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback); setupNode(); setupMessaging(); @@ -59,6 +79,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, this, &MainWindow::showInviteDialog); connect(m_channelList, &ChannelList::joinRequested, this, &MainWindow::showJoinDialog); + connect(m_channelList, &ChannelList::createGroupRequested, + this, &MainWindow::onCreateGroupRequested); connect(m_channelList, &ChannelList::settingsRequested, this, &MainWindow::showSettings); @@ -109,6 +131,10 @@ void MainWindow::setupNode() { if (!m_cfgDebugFile.isEmpty()) m_node->setDebugFile(m_cfgDebugFile); + if (!m_cfgDebugLevel.isEmpty()) + m_node->setDebugLevel(m_cfgDebugLevel); + if (!m_cfgDebugCategories.isEmpty()) + m_node->setDebugCategories(m_cfgDebugCategories); if (!m_cfgDbPath.isEmpty()) m_node->setDbPath(m_cfgDbPath); @@ -208,7 +234,117 @@ void MainWindow::showSettings() { } void MainWindow::onMessageReceived(const char* ch_id) { - qDebug("MainWindow: msg received ch=%s", ch_id); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: msg received ch=%s", ch_id); if (m_messageList) m_messageList->refresh(); } + +void MainWindow::reloadChannels() { + if (m_channelList) + m_channelList->loadChannels(); +} + +void MainWindow::onCreateGroupRequested() { + if (!m_node || !m_node->isRunning()) { + QMessageBox::warning(this, "Error", "Node is not running."); + return; + } + if (!gui_bridge_is_uasync_ready()) { + QMessageBox::warning(this, "Error", "uTun thread is not ready yet. Please wait and try again."); + return; + } + + CreateGroupDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) return; + + QString name = dlg.groupName(); + if (name.isEmpty()) return; + + /* генерация X25519 ключей */ + uint8_t x25519_pub[32] = {0}, x25519_priv[32] = {0}; + { + EVP_PKEY* pkey = nullptr; + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, nullptr); + if (ctx) { + EVP_PKEY_keygen_init(ctx); + EVP_PKEY_generate(ctx, &pkey); + EVP_PKEY_CTX_free(ctx); + } + if (pkey) { + size_t pkLen = 32, skLen = 32; + EVP_PKEY_get_raw_public_key(pkey, x25519_pub, &pkLen); + EVP_PKEY_get_raw_private_key(pkey, x25519_priv, &skLen); + EVP_PKEY_free(pkey); + } + } + + /* генерация Ed25519 ключей */ + uint8_t ed25519_pub[32] = {0}, ed25519_priv[32] = {0}; + EVP_PKEY* ed_pkey = nullptr; + { + EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + if (ctx) { + EVP_PKEY_keygen_init(ctx); + EVP_PKEY_generate(ctx, &ed_pkey); + EVP_PKEY_CTX_free(ctx); + } + if (ed_pkey) { + size_t pkLen = 32, skLen = 32; + EVP_PKEY_get_raw_public_key(ed_pkey, ed25519_pub, &pkLen); + EVP_PKEY_get_raw_private_key(ed_pkey, ed25519_priv, &skLen); + } + } + + /* случайный channel_id (uint64 → decimal string) */ + uint64_t chId = 0; + RAND_bytes((uint8_t*)&chId, sizeof(chId)); + QString channelId = QString::number(chId); + + /* owner_node_id из instance */ + uint64_t ownerNodeId = m_node->nodeIdHex().toULongLong(nullptr, 16); + + /* Ed25519 подпись: channel_id || name || owner_node_id(8 LE) || x25519_pub(32) || ed25519_pub(32) */ + uint8_t sig[64] = {0}; + if (ed_pkey) { + QByteArray nameUtf8 = name.toUtf8(); + QByteArray chIdUtf8 = channelId.toUtf8(); + size_t msgLen = chIdUtf8.size() + 1 + nameUtf8.size() + 1 + 8 + 32 + 32; + uint8_t* msg = (uint8_t*)malloc(msgLen); + if (msg) { + uint8_t* p = msg; + memcpy(p, chIdUtf8.constData(), chIdUtf8.size()); p += chIdUtf8.size(); *p++ = '\0'; + memcpy(p, nameUtf8.constData(), nameUtf8.size()); p += nameUtf8.size(); *p++ = '\0'; + memcpy(p, &ownerNodeId, 8); p += 8; + memcpy(p, x25519_pub, 32); p += 32; + memcpy(p, ed25519_pub, 32); p += 32; + + EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); + if (mdctx) { + EVP_DigestSignInit(mdctx, nullptr, nullptr, nullptr, ed_pkey); + size_t siglen = 64; + EVP_DigestSign(mdctx, sig, &siglen, msg, msgLen); + EVP_MD_CTX_free(mdctx); + } + free(msg); + } + EVP_PKEY_free(ed_pkey); + } + + /* отправляем в uasync-поток */ + struct chat_channel_create* req = (struct chat_channel_create*)u_malloc(sizeof(*req)); + memset(req, 0, sizeof(*req)); + snprintf(req->channel_id, sizeof(req->channel_id), "%s", qPrintable(channelId)); + snprintf(req->name, sizeof(req->name), "%s", qPrintable(name)); + req->owner_node_id = ownerNodeId; + req->is_dm = 0; + memcpy(req->x25519_pubkey, x25519_pub, 32); + memcpy(req->x25519_privkey, x25519_priv, 32); + memcpy(req->ed25519_pubkey, ed25519_pub, 32); + memcpy(req->ed25519_privkey, ed25519_priv, 32); + memcpy(req->signature, sig, 64); + + gui_bridge_post_uasync_fn(chat_core_create_channel_trampoline, req); + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: create group request posted ch=%s name=%s", + qPrintable(channelId), qPrintable(name)); +} diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 70f72e0d..7886a749 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -15,7 +15,9 @@ class MainWindow : public QMainWindow { public: explicit MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, const QString& dbPath = QString(), - const QString& debugFile = QString()); + const QString& debugFile = QString(), + const QString& debugLevel = QString(), + const QString& debugCategories = QString()); ~MainWindow() override; protected: @@ -25,10 +27,12 @@ private slots: void onTrayActivated(QSystemTrayIcon::ActivationReason reason); void showInviteDialog(const QString& channelId); void showJoinDialog(); + void onCreateGroupRequested(); void showSettings(); public: void onMessageReceived(const char* ch_id); + void reloadChannels(); private: void setupUi(); @@ -46,4 +50,6 @@ private: QString m_cfgPath; QString m_cfgDbPath; QString m_cfgDebugFile; + QString m_cfgDebugLevel; + QString m_cfgDebugCategories; }; diff --git a/tools/chatgui/transport/chat_core.c b/tools/chatgui/transport/chat_core.c index 641fecfb..9c5b7da0 100644 --- a/tools/chatgui/transport/chat_core.c +++ b/tools/chatgui/transport/chat_core.c @@ -8,6 +8,7 @@ #include "chat_core.h" #include "chat_sync.h" #include "gui_bridge.h" +#include "topo_node_sqlite.h" #include "../../../src/utun_instance.h" #include "../../../src/etcp_router.h" @@ -128,6 +129,9 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { g_cc.inst = inst; g_cc.my_node_id = inst->node_id; + topo_node_sqlite_init(g_cc.db); + topo_groups_set_sqlite_db(inst->topo_groups, g_cc.db); + db_exec( "CREATE TABLE IF NOT EXISTS local_identity (" " id INTEGER PRIMARY KEY CHECK (id = 1)," @@ -140,28 +144,6 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { " updated_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE TABLE IF NOT EXISTS nodes (" - " node_id INTEGER PRIMARY KEY," - " name TEXT," - " x25519_pubkey BLOB NOT NULL," - " ed25519_pubkey BLOB," - " last_seen_at INTEGER," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - - "CREATE TABLE IF NOT EXISTS node_addresses (" - " id INTEGER PRIMARY KEY AUTOINCREMENT," - " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," - " family INTEGER NOT NULL CHECK(family IN (4, 6))," - " protocol INTEGER NOT NULL DEFAULT 1," - " address BLOB NOT NULL," - " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," - " rtt INTEGER," - " is_nat INTEGER DEFAULT 0," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" - "CREATE TABLE IF NOT EXISTS accounts (" " node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id)," " display_name TEXT NOT NULL," @@ -171,21 +153,6 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { " created_at INTEGER DEFAULT (unixepoch())" ");" - "CREATE TABLE IF NOT EXISTS channels (" - " channel_id TEXT PRIMARY KEY," - " name TEXT NOT NULL," - " owner_node_id INTEGER," - " is_dm INTEGER DEFAULT 0," - " x25519_pubkey BLOB NOT NULL," - " x25519_privkey BLOB," - " ed25519_pubkey BLOB NOT NULL," - " ed25519_privkey BLOB," - " signature BLOB NOT NULL," - " last_read_msg_id INTEGER," - " last_pos_msg_id INTEGER," - " created_at INTEGER DEFAULT (unixepoch())" - ");" - "CREATE TABLE IF NOT EXISTS ui_state (" " key TEXT PRIMARY KEY," " value TEXT" @@ -627,7 +594,7 @@ void chat_core_connect_from_invite(struct chat_invite* inv) { uint64_t node_id = inv->node_id; - if (nodeinfo_find_by_id(group, node_id)) { + if (topo_node_find_by_id(group, node_id)) { DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: node 0x%016llx already in BGP, connecting", CC_ID, (unsigned long long)node_id); conn_mgr_connect_node(g_cc.inst->conn_mgr, node_id, 30000, @@ -635,56 +602,47 @@ void chat_core_connect_from_invite(struct chat_invite* inv) { return; } - size_t dyn_sz = (size_t)inv->addr_count * sizeof(struct NODEINFO_IPV4_ADDR); - size_t data_sz = sizeof(struct NODEINFO_Q) - sizeof(struct ll_entry) + dyn_sz + 8; - struct NODEINFO_Q* nq = (struct NODEINFO_Q*)queue_entry_new(data_sz); - if (!nq) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: failed to alloc NODEINFO_Q", CC_ID); + struct ll_entry* qe = queue_entry_new(sizeof(struct TOPO_NODEQ)); + if (!qe) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: failed to alloc TOPO_NODEQ", CC_ID); uint8_t err[12]; int r = -7; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); return; } + struct TOPO_NODEQ* nq = (struct TOPO_NODEQ*)qe; + memset((uint8_t*)nq + sizeof(struct ll_entry), 0, sizeof(*nq) - sizeof(struct ll_entry)); - struct NODEINFO* ni = &nq->node; + struct TOPO_NODE* ni = u_calloc(1, sizeof(struct TOPO_NODE)); + if (!ni) { queue_entry_free(qe); return; } ni->node_id = node_id; + ni->group_id = group->group_id; ni->ver = 0; memcpy(ni->public_key, inv->pubkey, SC_PUBKEY_SIZE); memset(ni->ed25519_public_key, 0, SC_PUBKEY_SIZE); - ni->node_name_len = 0; - ni->local_v4_sockets = 0; - ni->local_v4_addrs = (uint8_t)inv->addr_count; - ni->local_v6_sockets = 0; - ni->local_v6_addrs = 0; - ni->local_v4_subnets = 0; - ni->local_v6_subnets = 0; - ni->tranzit_nodes = 0; - ni->hop_count = 0; - - uint8_t* dyn = (uint8_t*)ni + sizeof(struct NODEINFO); + ni->node_name = u_strdup(""); + + struct TOPO_ADDR4* addrs_head = NULL; const uint8_t* src = inv->addrs_data; for (int i = 0; i < inv->addr_count; i++) { - struct NODEINFO_IPV4_ADDR* addr = - (struct NODEINFO_IPV4_ADDR*)(dyn + i * sizeof(struct NODEINFO_IPV4_ADDR)); - memset(addr, 0, sizeof(*addr)); uint8_t family = *src++; if (family == 4) { - memcpy(addr->addr, src, 4); src += 4; - uint16_t port_be = ((uint16_t)src[0] << 8) | src[1]; src += 2; - addr->port = port_be; - addr->type = ADDR_TYPE_REAL; - addr->socket_id = 0; - addr->protocol = NODE_PROTO_UDP; + struct TOPO_ADDR4* a4 = memory_pool_alloc(group->instance->topo_groups->v4_addr_pool); + if (!a4) continue; + memcpy(a4->addr, src, 4); src += 4; + a4->port = ((uint16_t)src[0] << 8) | src[1]; src += 2; + a4->type = TOPO_ADDR_REAL; a4->socket_id = 0; a4->protocol = TOPO_PROTO_UDP; + a4->next = addrs_head; addrs_head = a4; } else { - src += 18; continue; + src += 18; /* skip v6 */ } } + ni->v4_addrs = addrs_head; - nq->alien = 0; - nq->dirty = 0; - nq->last_ver = 0; - nq->conn_mgr_type = CONN_TYPE_NONE; - nq->conn_mgr_intermediariy_count = 0; + nq->node = ni; topo_node_ref(ni); + nq->hash_node_id = node_id; nq->alien = 0; + nq->dirty = 0; nq->last_ver = 0; + nq->conn_mgr_type = CONN_TYPE_NONE; nq->conn_mgr_intermediariy_count = 0; memset(&nq->connectivity, 0, sizeof(nq->connectivity)); nq->best_socket = NULL; @@ -695,3 +653,72 @@ void chat_core_connect_from_invite(struct chat_invite* inv) { conn_mgr_connect_node(g_cc.inst->conn_mgr, node_id, 30000, connect_result_cb, NULL); } + +/* ─── создание канала ─── */ + +void chat_core_create_channel(struct chat_channel_create* req) { + if (!g_cc.initialized) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: create_channel NOT INITIALIZED ch=%s", + CC_ID, req ? req->channel_id : "(null)"); + return; + } + if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: create_channel req=NULL", CC_ID); return; } + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: create_channel BEGIN ch=%s name=%s", + CC_ID, req->channel_id, req->name); + + char tbl_msg[80]; msg_table_name(req->channel_id, tbl_msg, sizeof(tbl_msg)); + + /* создаём таблицу сообщений */ + char sql[512]; snprintf(sql, sizeof(sql), + "CREATE TABLE IF NOT EXISTS \"%s\" (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " node_id INTEGER NOT NULL," + " content_type TEXT NOT NULL," + " data BLOB NOT NULL," + " timestamp INTEGER NOT NULL," + " datahash INTEGER NOT NULL," + " chain_hash BLOB NOT NULL," + " signature BLOB," + " is_outgoing INTEGER DEFAULT 0," + " is_read INTEGER DEFAULT 0," + " sync_flags INTEGER DEFAULT 0," + " UNIQUE(timestamp, datahash))", tbl_msg); + db_exec(sql); + + snprintf(sql, sizeof(sql), + "CREATE INDEX IF NOT EXISTS \"idx_%s_ts_dh\" ON \"%s\"(timestamp, datahash)", + tbl_msg, tbl_msg); + db_exec(sql); + + /* записываем канал в БД */ + int rc = topo_node_sqlite_channel_put(g_cc.db, + req->channel_id, req->name, req->is_dm, req->owner_node_id, + req->x25519_pubkey, req->x25519_privkey, + req->ed25519_pubkey, req->ed25519_privkey, + req->signature); + + if (rc != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: channel_put FAILED ch=%s rc=%d", + CC_ID, req->channel_id, rc); + } else { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: channel_put OK ch=%s name=%s owner=0x%016llx", + CC_ID, req->channel_id, req->name, (unsigned long long)req->owner_node_id); + + /* уведомляем GUI */ + uint8_t ch_id_len = (uint8_t)strlen(req->channel_id); + uint8_t data[65]; + data[0] = ch_id_len; + memcpy(data + 1, req->channel_id, ch_id_len); + gui_bridge_post(GUI_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: gui_bridge_post sent GUI_EVT_CHANNEL_UPDATED ch=%s", + CC_ID, req->channel_id); + } +} + +void chat_core_create_channel_trampoline(void* arg) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_core: TRAMPOLINE invoked arg=%p", arg); + struct chat_channel_create* req = (struct chat_channel_create*)arg; + chat_core_create_channel(req); + u_free(req); +} diff --git a/tools/chatgui/transport/chat_core.h b/tools/chatgui/transport/chat_core.h index 3354b1f8..749acb1c 100644 --- a/tools/chatgui/transport/chat_core.h +++ b/tools/chatgui/transport/chat_core.h @@ -62,6 +62,23 @@ struct chat_invite { void chat_core_connect_from_invite(struct chat_invite* inv); +/* ── Создание канала (GUI → uasync) ── */ + +struct chat_channel_create { + char channel_id[64]; + char name[128]; + uint64_t owner_node_id; + int is_dm; + uint8_t x25519_pubkey[32]; + uint8_t x25519_privkey[32]; + uint8_t ed25519_pubkey[32]; + uint8_t ed25519_privkey[32]; + uint8_t signature[64]; +}; + +void chat_core_create_channel(struct chat_channel_create* req); +void chat_core_create_channel_trampoline(void* arg); + /* ── Утилиты ── */ void chat_core_set_my_node_id(uint64_t node_id); diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 91999008..c62101c4 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -31,6 +31,9 @@ void gui_bridge_post(int event_type, const uint8_t* data, int data_len); /* GUI → uasync: выполнить функцию в uasync-потоке */ void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg); +/* Проверить готовность uasync (g_ua != NULL) */ +int gui_bridge_is_uasync_ready(void); + /* Callback для результатов подключения (вызывается из GUI-потока) */ typedef void (*gui_connect_result_fn)(uint64_t node_id, int result); void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb); @@ -39,6 +42,10 @@ void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb); typedef void (*gui_msg_received_fn)(const char* ch_id, int ch_id_len); void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb); +/* Callback для обновления канала (вызывается из GUI-потока) */ +typedef void (*gui_channel_updated_fn)(const char* ch_id, int ch_id_len); +void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index 88a1023c..4982ad6a 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -3,12 +3,12 @@ #include #include #include -#include #include extern "C" { #include "../../../lib/u_async.h" #include "../../../lib/mem.h" +#include "../../../lib/debug_config.h" } /* ── Внутренний объект-приёмник в GUI-потоке ── */ @@ -26,6 +26,7 @@ public: static GuiBridgeReceiver* g_receiver = nullptr; static gui_connect_result_fn g_connect_result_cb = nullptr; static gui_msg_received_fn g_msg_received_cb = nullptr; +static gui_channel_updated_fn g_channel_updated_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -40,32 +41,32 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { uint8_t chLen = d[0]; if (dlen >= 1 + chLen) { QString chId = QString::fromUtf8((const char*)d + 1, chLen); - qDebug("gui_bridge: MSG_RECEIVED ch=%s", qPrintable(chId)); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: MSG_RECEIVED ch=%s", qPrintable(chId)); if (g_msg_received_cb) g_msg_received_cb((const char*)d + 1, chLen); } else { - qWarning("gui_bridge: MSG_RECEIVED invalid data length %d", dlen); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: MSG_RECEIVED invalid data length %d", dlen); } } else { - qWarning("gui_bridge: MSG_RECEIVED data too short %d", dlen); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: MSG_RECEIVED data too short %d", dlen); } break; case GUI_EVT_CONNECT_RESULT: if (dlen >= 12) { uint64_t nodeId; int result; memcpy(&nodeId, d, 8); memcpy(&result, d + 8, 4); - qDebug("gui_bridge: CONNECT_RESULT node=0x%016llx result=%d", - (unsigned long long)nodeId, result); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: CONNECT_RESULT node=0x%016llx result=%d", + (unsigned long long)nodeId, result); if (g_connect_result_cb) g_connect_result_cb(nodeId, result); } else { - qWarning("gui_bridge: CONNECT_RESULT data too short %d", dlen); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: CONNECT_RESULT data too short %d", dlen); } break; case GUI_EVT_NEW_PEER: if (dlen >= 8) { uint64_t nodeId; memcpy(&nodeId, d, 8); - qDebug("gui_bridge: NEW_PEER node=0x%016llx", (unsigned long long)nodeId); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: NEW_PEER node=0x%016llx", (unsigned long long)nodeId); } else { - qWarning("gui_bridge: NEW_PEER data too short %d", dlen); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: NEW_PEER data too short %d", dlen); } break; case GUI_EVT_CHANNEL_UPDATED: @@ -73,14 +74,15 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { uint8_t chLen = d[0]; if (dlen >= 1 + chLen) { QString chId = QString::fromUtf8((const char*)d + 1, chLen); - qDebug("gui_bridge: CHANNEL_UPDATED ch=%s", qPrintable(chId)); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge: CHANNEL_UPDATED ch=%s", qPrintable(chId)); + if (g_channel_updated_cb) g_channel_updated_cb((const char*)d + 1, chLen); } } else { - qWarning("gui_bridge: CHANNEL_UPDATED data too short %d", dlen); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: CHANNEL_UPDATED data too short %d", dlen); } break; default: - qWarning("gui_bridge: unknown event type %d", eventType); + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; } } @@ -112,7 +114,17 @@ void gui_bridge_post(int event_type, const uint8_t* data, int data_len) { } void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) { - if (g_ua && fn) uasync_post(g_ua, fn, arg); + if (!g_ua) { + DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge_post_uasync_fn: g_ua=NULL, post dropped"); + return; + } + if (!fn) return; + uasync_post(g_ua, fn, arg); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "gui_bridge_post_uasync_fn: posted fn=%p arg=%p", (void*)fn, arg); +} + +int gui_bridge_is_uasync_ready(void) { + return g_ua != NULL ? 1 : 0; } void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb) { @@ -123,6 +135,10 @@ void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb) { g_msg_received_cb = cb; } +void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb) { + g_channel_updated_cb = cb; +} + } /* extern "C" */ #include "gui_bridge_impl.moc" diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 9c396f60..e049006e 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -125,6 +125,8 @@ bool NodeConfig::load() { } else if (section == "gui") { if (key == "db_path") m_dbPath = val; else if (key == "debug_file") m_debugFile = val; + else if (key == "debug_level") m_debugLevel = val; + else if (key == "debug_categories") m_debugCategories = val; } } f.close(); @@ -169,7 +171,9 @@ bool NodeConfig::save() { out << "[gui]\n"; out << "db_path=" << (m_dbPath.isEmpty() ? "chats.db" : m_dbPath) << "\n"; - out << "debug_file=" << m_debugFile << "\n\n"; + out << "debug_file=" << m_debugFile << "\n"; + out << "debug_level=" << (m_debugLevel.isEmpty() ? "info" : m_debugLevel) << "\n"; + out << "debug_categories=" << m_debugCategories << "\n\n"; f.close(); return true; diff --git a/tools/chatgui/transport/node_config.h b/tools/chatgui/transport/node_config.h index dcfa0642..aef29291 100644 --- a/tools/chatgui/transport/node_config.h +++ b/tools/chatgui/transport/node_config.h @@ -48,6 +48,8 @@ public: /* GUI settings (from [gui] section) */ QString dbPath() const { return m_dbPath; } QString debugFile() const { return m_debugFile; } + QString debugLevel() const { return m_debugLevel; } + QString debugCategories() const { return m_debugCategories; } /* Identity generation (public for fixing invalid keys) */ void generateIdentity(); @@ -64,5 +66,7 @@ private: QString m_controlAddr; QString m_dbPath; QString m_debugFile; + QString m_debugLevel; + QString m_debugCategories; }; #endif // NODE_CONFIG_H diff --git a/tools/chatgui/transport/topo_node_sqlite.c b/tools/chatgui/transport/topo_node_sqlite.c new file mode 100644 index 00000000..26dd8d29 --- /dev/null +++ b/tools/chatgui/transport/topo_node_sqlite.c @@ -0,0 +1,203 @@ +#include "topo_node_sqlite.h" +#include "../../../src/topo_node.h" +#include "../../../lib/debug_config.h" +#include +#include + +#define PEERS_JOIN_SIG_SIZE 64 + +static 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'; +} + +static 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 topo_node_sqlite_init(sqlite3* db) { + if (!db) return -1; + + const char* sql = + "CREATE TABLE IF NOT EXISTS nodes (" + " node_id INTEGER PRIMARY KEY," + " name TEXT," + " x25519_pubkey BLOB NOT NULL," + " ed25519_pubkey BLOB," + " last_seen_at INTEGER," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS node_addresses (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," + " family INTEGER NOT NULL CHECK(family IN (4, 6))," + " protocol INTEGER NOT NULL DEFAULT 1," + " address BLOB NOT NULL," + " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," + " rtt INTEGER," + " is_nat INTEGER DEFAULT 0," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" + + "CREATE TABLE IF NOT EXISTS channels (" + " channel_id TEXT PRIMARY KEY," + " name TEXT NOT NULL," + " owner_node_id INTEGER," + " is_dm INTEGER DEFAULT 0," + " x25519_pubkey BLOB NOT NULL," + " x25519_privkey BLOB," + " ed25519_pubkey BLOB NOT NULL," + " ed25519_privkey BLOB," + " signature BLOB NOT NULL," + " last_read_msg_id INTEGER," + " last_pos_msg_id INTEGER," + " created_at INTEGER DEFAULT (unixepoch())" + ");"; + + char* err = NULL; + int rc = sqlite3_exec(db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_init: %s", err); + sqlite3_free(err); + return -1; + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_node_sqlite tables initialized"); + return 0; +} + +int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_NODEQ* nq) { + if (!db || !nq || !nq->node) return -1; + struct TOPO_NODE* ni = nq->node; + + sqlite3_exec(db, "BEGIN", NULL, NULL, NULL); + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db, + "INSERT OR REPLACE INTO nodes(node_id, name, x25519_pubkey, ed25519_pubkey, last_seen_at)" + " VALUES(?,?,?,?,unixepoch())", -1, &stmt, NULL) == SQLITE_OK) { + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ni->node_id); + sqlite3_bind_text(stmt, 2, ni->node_name ? ni->node_name : "", -1, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 3, ni->public_key, 32, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 4, ni->ed25519_public_key, 32, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + } + + sqlite3_stmt* del_stmt = NULL; + if (sqlite3_prepare_v2(db, "DELETE FROM node_addresses WHERE node_id=?", -1, &del_stmt, NULL) == SQLITE_OK) { + sqlite3_bind_int64(del_stmt, 1, (sqlite3_int64)ni->node_id); + sqlite3_step(del_stmt); sqlite3_finalize(del_stmt); + } + + sqlite3_stmt* addr_stmt = NULL; + if (sqlite3_prepare_v2(db, + "INSERT INTO node_addresses(node_id, family, protocol, address, port, rtt, is_nat)" + " VALUES(?,?,?,?,?,?,?)", -1, &addr_stmt, NULL) == SQLITE_OK) { + struct TOPO_ADDR4* a4 = ni->v4_addrs; + while (a4) { + sqlite3_bind_int64(addr_stmt, 1, (sqlite3_int64)ni->node_id); + sqlite3_bind_int(addr_stmt, 2, 4); + sqlite3_bind_int(addr_stmt, 3, a4->protocol); + sqlite3_bind_blob(addr_stmt, 4, a4->addr, 4, SQLITE_STATIC); + sqlite3_bind_int(addr_stmt, 5, a4->port); + sqlite3_bind_null(addr_stmt, 6); + sqlite3_bind_int(addr_stmt, 7, a4->type == 1 ? 1 : 0); + sqlite3_step(addr_stmt); sqlite3_reset(addr_stmt); + a4 = a4->next; + } + struct TOPO_ADDR6* a6 = ni->v6_addrs; + while (a6) { + sqlite3_bind_int64(addr_stmt, 1, (sqlite3_int64)ni->node_id); + sqlite3_bind_int(addr_stmt, 2, 6); + sqlite3_bind_int(addr_stmt, 3, a6->protocol); + sqlite3_bind_blob(addr_stmt, 4, a6->addr, 16, SQLITE_STATIC); + sqlite3_bind_int(addr_stmt, 5, a6->port); + sqlite3_bind_null(addr_stmt, 6); + sqlite3_bind_int(addr_stmt, 7, a6->type == 1 ? 1 : 0); + sqlite3_step(addr_stmt); sqlite3_reset(addr_stmt); + a6 = a6->next; + } + sqlite3_finalize(addr_stmt); + } + + sqlite3_exec(db, "COMMIT", NULL, NULL, NULL); + return 0; +} + +int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, + const char* name, int is_dm, uint64_t owner_node_id, + const uint8_t* x25519_pub, const uint8_t* x25519_priv, + const uint8_t* ed25519_pub, const uint8_t* ed25519_priv, + const uint8_t* signature) { + if (!db || !channel_id || !name || !x25519_pub || !ed25519_pub || !signature) return -1; + + char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); + char sql[512]; + snprintf(sql, sizeof(sql), + "CREATE TABLE IF NOT EXISTS \"%s\" (" + " node_id INTEGER NOT NULL," + " join_sig BLOB NOT NULL," + " creator_sig BLOB," + " comment TEXT," + " joined_at INTEGER DEFAULT (unixepoch())," + " PRIMARY KEY (node_id))", peers_tbl); + char* err = NULL; + sqlite3_exec(db, sql, NULL, NULL, &err); + if (err) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "topo_node_sqlite_channel_put peers: %s", err); sqlite3_free(err); } + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db, + "INSERT OR REPLACE INTO channels(channel_id, name, owner_node_id, is_dm," + " x25519_pubkey, x25519_privkey, ed25519_pubkey, ed25519_privkey, signature)" + " VALUES(?,?,?,?,?,?,?,?,?)", -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_text(stmt, 1, channel_id, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)owner_node_id); + sqlite3_bind_int(stmt, 4, is_dm); + sqlite3_bind_blob(stmt, 5, x25519_pub, 32, SQLITE_STATIC); + if (x25519_priv) sqlite3_bind_blob(stmt, 6, x25519_priv, 32, SQLITE_STATIC); + else sqlite3_bind_null(stmt, 6); + sqlite3_bind_blob(stmt, 7, ed25519_pub, 32, SQLITE_STATIC); + if (ed25519_priv) sqlite3_bind_blob(stmt, 8, ed25519_priv, 32, SQLITE_STATIC); + else sqlite3_bind_null(stmt, 8); + sqlite3_bind_blob(stmt, 9, signature, 64, SQLITE_STATIC); + int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); + return rc == SQLITE_DONE ? 0 : -1; +} + +int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t node_id) { + if (!db || !channel_id) return -1; + char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); + char sql[256]; + snprintf(sql, sizeof(sql), + "INSERT OR REPLACE INTO \"%s\"(node_id, join_sig) VALUES(?,?)", peers_tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + static const unsigned char zero_sig[PEERS_JOIN_SIG_SIZE] = {0}; + sqlite3_bind_blob(stmt, 2, zero_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); + sqlite3_step(stmt); sqlite3_finalize(stmt); + return 0; +} + +int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id) { + if (!db || !channel_id) return -1; + char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); + char sql[256]; + snprintf(sql, sizeof(sql), "DELETE FROM \"%s\" WHERE node_id=?", peers_tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + sqlite3_step(stmt); sqlite3_finalize(stmt); + return 0; +} diff --git a/tools/chatgui/transport/topo_node_sqlite.h b/tools/chatgui/transport/topo_node_sqlite.h new file mode 100644 index 00000000..27217457 --- /dev/null +++ b/tools/chatgui/transport/topo_node_sqlite.h @@ -0,0 +1,24 @@ +#ifndef TOPO_NODE_SQLITE_H +#define TOPO_NODE_SQLITE_H + +#include +#include +#include + +struct TOPO_NODEQ; + +int topo_node_sqlite_init(sqlite3* db); + +int topo_node_sqlite_node_put(sqlite3* db, struct TOPO_NODEQ* nq); + +int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, + const char* name, int is_dm, uint64_t owner_node_id, + const uint8_t* x25519_pub, const uint8_t* x25519_priv, + const uint8_t* ed25519_pub, const uint8_t* ed25519_priv, + const uint8_t* signature); + +int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t node_id); + +int topo_node_sqlite_member_del(sqlite3* db, const char* channel_id, uint64_t node_id); + +#endif diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 81616f7c..43222a59 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -60,6 +60,14 @@ void UtunNode::setDbPath(const QString& path) { m_dbPath = path; } +void UtunNode::setDebugLevel(const QString& level) { + m_debugLevel = level; +} + +void UtunNode::setDebugCategories(const QString& categories) { + m_debugCategories = categories; +} + void UtunNode::stop() { if (!m_running) return; m_stop = true; @@ -100,11 +108,62 @@ void UtunNode::runLoop() { g_currentNode = this; debug_config_init(); - debug_set_level(DEBUG_LEVEL_ERROR); debug_enable_function_name(0); + /* ── debug level from config ── */ + if (!m_debugLevel.isEmpty()) { + QString lvl = m_debugLevel.toLower(); + debug_level_t dl = DEBUG_LEVEL_INFO; + if (lvl == "none") dl = DEBUG_LEVEL_NONE; + else if (lvl == "error") dl = DEBUG_LEVEL_ERROR; + else if (lvl == "warn") dl = DEBUG_LEVEL_WARN; + else if (lvl == "info") dl = DEBUG_LEVEL_INFO; + else if (lvl == "debug") dl = DEBUG_LEVEL_DEBUG; + else if (lvl == "trace") dl = DEBUG_LEVEL_TRACE; + g_debug_config.level = dl; + g_debug_config.console_level = dl; + g_debug_config.file_level = dl; + } else { + g_debug_config.level = DEBUG_LEVEL_INFO; + g_debug_config.console_level = DEBUG_LEVEL_INFO; + g_debug_config.file_level = DEBUG_LEVEL_INFO; + } + + /* chatgui defaults: suppress spam, verbose for our categories */ + g_debug_config.category_levels[DEBUG_CATEGORY_MEMORY] = DEBUG_LEVEL_DISABLED; + g_debug_config.category_levels[DEBUG_CATEGORY_TIMING] = DEBUG_LEVEL_DISABLED; + g_debug_config.category_levels[DEBUG_CATEGORY_DEBUG] = DEBUG_LEVEL_TRACE; + g_debug_config.category_levels[DEBUG_CATEGORY_BGP] = DEBUG_LEVEL_INFO; + + /* per-category overrides from config: cat=level,cat=level,... */ + if (!m_debugCategories.isEmpty()) { + QByteArray cats = m_debugCategories.toUtf8(); + char* token = strtok(cats.data(), ","); + while (token) { + while (*token == ' ') token++; + char* eq = strchr(token, '='); + if (eq) { + *eq = '\0'; + char* cat_name = token; + char* lvl_name = eq + 1; + while (*lvl_name == ' ') lvl_name++; + debug_category_t cat = get_category_by_name(cat_name); + debug_level_t lvl = debug_level_from_name(lvl_name); + if (cat != DEBUG_CATEGORY_NONE && lvl != DEBUG_LEVEL_NONE) { + g_debug_config.category_levels[cat] = lvl; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "debug category: %s=%s", + debug_get_category_name(cat), + lvl_name); + } + } + token = strtok(NULL, ","); + } + } + + /* file output (shared with GUI via debug_config) */ if (!m_debugFile.isEmpty()) { debug_enable_file_output(m_debugFile.toUtf8().constData(), 0); + debug_enable_console(1); g_debug_config.thread_marker = 2; } @@ -145,6 +204,7 @@ void UtunNode::runLoop() { chat_core_init(m_instance, m_dbPath.toUtf8().constData()); chat_sync_init(m_instance, NULL); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_core + chat_sync initialized"); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "utun_node: entering poll loop"); while (!m_stop) { uasync_poll(ua, 100); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index c2715424..04c62761 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -22,6 +22,8 @@ public: void setDebugFile(const QString& path); void setDbPath(const QString& path); + void setDebugLevel(const QString& level); + void setDebugCategories(const QString& categories); void send(uint64_t dstNodeId, const QByteArray& data); @@ -45,5 +47,7 @@ private: QString m_configPath; QString m_debugFile; QString m_dbPath; + QString m_debugLevel; + QString m_debugCategories; }; #endif // UTUN_NODE_H