From 7f901c01e1fc4f2b043e43c41e0c2e65c68111a6 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 9 Aug 2026 12:39:53 +0300 Subject: [PATCH] =?UTF-8?q?etcp:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=83=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=8F=D0=B4=D0=BE=D1=87=D0=B8=D1=82=D1=8C=20close?= =?UTF-8?q?=20+=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20state=3D=3D2=20?= =?UTF-8?q?=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=D1=85=20API-=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=D1=85;=20topo:=20=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BE=D1=87=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20group=20nodes=20=D0=B8=20registry;=20chat/?= =?UTF-8?q?member:=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D1=83=D0=BB=D1=8C=20member=5Fsync;=20chatgui:=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=81=D0=BF=D0=BE=D1=80=D1=82=D0=B0=20=D0=B8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/u_async.c | 6 +- lib/u_async.h | 1 + src/Makefile.am | 8 +- src/chat/chat_core.c | 5 + src/chat/chat_core.h | 3 + src/chat/chat_event.c | 4 +- src/chat/chat_event.h | 4 + src/chat/chat_member.c | 362 ++++++++++++++++++ src/chat/chat_member.h | 70 ++++ src/chat/chat_profile.c | 9 +- src/chat/chat_status.c | 139 +++++++ src/chat/chat_sync.c | 67 +++- src/chat/db_sync.c | 3 +- src/chat/member_sync.c | 81 ++++ src/chat/member_sync.h | 4 + src/chat/merkle_sync.c | 2 +- src/media_delivery/media_delivery.c | 12 +- src/routing_layer/topo_group.c | 21 +- src/transport_layer/etcp.c | 38 +- src/transport_layer/etcp_api.c | 2 +- src/transport_layer/etcp_connections.c | 2 + src/utun_instance.c | 20 +- .../libutun_lite/instance_lite.c | 3 +- tools/chatgui/db/db_manager.cpp | 44 --- tools/chatgui/db/db_manager.h | 11 - tools/chatgui/src/accountlist.cpp | 257 +++++++------ tools/chatgui/src/accountlist.h | 40 +- tools/chatgui/src/channeldelegate.cpp | 47 +-- tools/chatgui/src/channellist.cpp | 20 +- tools/chatgui/src/mainwindow.cpp | 50 ++- tools/chatgui/src/mainwindow.h | 4 +- tools/chatgui/src/memberlistmodel.cpp | 247 +++++------- tools/chatgui/src/memberlistmodel.h | 26 +- tools/chatgui/src/memberpropsdialog.cpp | 101 ++--- tools/chatgui/src/memberpropsdialog.h | 8 +- tools/chatgui/transport/gui_bridge.h | 34 +- tools/chatgui/transport/gui_bridge_impl.cpp | 46 ++- tools/chatgui/transport/utun_node.cpp | 16 +- 38 files changed, 1276 insertions(+), 541 deletions(-) create mode 100644 src/chat/chat_member.c create mode 100644 src/chat/chat_member.h diff --git a/lib/u_async.c b/lib/u_async.c index 79630fba..ad966594 100644 --- a/lib/u_async.c +++ b/lib/u_async.c @@ -440,7 +440,7 @@ static uint64_t timeval_to_ms(const struct timeval* tv) { // Process immediate_queue (deferred callbacks via uasync_call_soon) -static void process_immediate_queue(struct UASYNC* ua) { +void process_immediate_queue(struct UASYNC* ua) { while (ua->immediate_queue_head) { struct timeout_node* node = ua->immediate_queue_head; ua->immediate_queue_head = node->next; @@ -456,6 +456,10 @@ static void process_immediate_queue(struct UASYNC* ua) { } } +void uasync_drain_immediate(struct UASYNC* ua) { + if (ua) process_immediate_queue(ua); +} + // Process expired timeouts with safe cancellation static void process_timeouts(struct UASYNC* ua) { if (!ua) return; diff --git a/lib/u_async.h b/lib/u_async.h index da9336d4..de289104 100644 --- a/lib/u_async.h +++ b/lib/u_async.h @@ -125,6 +125,7 @@ err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable); // Single iteration of event loop with timeout (timebase units) void uasync_poll(struct UASYNC* ua, int timeout_tb); +void uasync_drain_immediate(struct UASYNC* ua); // Mainloop (бесконечный цикл, __noreturn) void uasync_mainloop(struct UASYNC* ua); diff --git a/src/Makefile.am b/src/Makefile.am index 8110dad8..e2264834 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -70,8 +70,8 @@ utun_CORE_SOURCES = \ chat/chat_event.c \ chat/chat_setting.c \ chat/chat_core.c \ - chat/chat_msg.c \ - chat/chat_whisper.c \ + chat/chat_member.c \ + chat/chat_msg.c \ chat/chat_whisper.c \ chat/chat_channel.c \ chat/chat_profile.c \ chat/chat_status.c \ @@ -150,8 +150,8 @@ libutun_a_SOURCES = \ chat/chat_event.c \ chat/chat_setting.c \ chat/chat_core.c \ - chat/chat_msg.c \ - chat/chat_channel.c \ + chat/chat_member.c \ + chat/chat_msg.c \ chat/chat_channel.c \ chat/chat_profile.c \ chat/chat_status.c \ chat/chat_sync.c \ diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index e7a90ca6..a80b7fea 100644 --- a/src/chat/chat_core.c +++ b/src/chat/chat_core.c @@ -13,6 +13,7 @@ #include "chat_core_priv.h" #include "chat_event.h" #include "chat_setting.h" +#include "chat_member.h" #include "member_sync.h" #include "../utun_instance.h" @@ -138,6 +139,9 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { g_cc.initialized = 1; + /* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */ + chat_member_init(); + /* create media_files table */ media_index_init(g_cc.db); @@ -664,6 +668,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) { uinfo ? uinfo : "", NULL, 0, json, sig, stg); 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; } + member_sync_broadcast_one(g_cc.inst, t->ch_id, t->node_id); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s", CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json); diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 801851ef..77f83f36 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -122,6 +122,9 @@ void chat_core_collect_conn_list_trampoline(void* arg); /* Сбор полных метрик одного соединения (arg = u_malloc uint64_t peer_node_id) */ void chat_core_collect_conn_metrics_trampoline(void* arg); +/* Сбор live-снапшота для панели деталей участника (arg = u_malloc uint64_t node_id) */ +void chat_core_collect_member_detail_trampoline(void* arg); + /* ── Headless CLI API ── */ /** Вариант для сборки без UTUN_INSTANCE (использует sqlite3* и node_id) */ diff --git a/src/chat/chat_event.c b/src/chat/chat_event.c index b8ef904f..e92d678a 100644 --- a/src/chat/chat_event.c +++ b/src/chat/chat_event.c @@ -35,8 +35,10 @@ void chat_event_post(int type, const uint8_t* data, int len) { [16]="NODEINFO_UPDATED", [17]="LINK_STATUS_CHANGED", [18]="INVITE_RECEIVED", [19]="LOCAL_SOCKETS", [20]="CONN_LIST", [21]="CONN_METRICS", + [22]="MEMBER_DETAIL", [23]="MEMBER_LIST", + [24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED", }; - const char* n = (type >= 1 && type <= 21) ? names[type] : "?"; + const char* n = (type >= 1 && type <= 25) ? names[type] : "?"; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "chat_event: %s(%d) data=%d bytes", n, type, len); if (data && len > 0) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_DEBUG, " ", data, len); } diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h index a87e7a1d..479f9289 100644 --- a/src/chat/chat_event.h +++ b/src/chat/chat_event.h @@ -40,6 +40,10 @@ extern "C" { #define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */ #define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */ #define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */ +#define CHAT_EVT_MEMBER_DETAIL 22 /* [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live-снапшот для панели деталей участника */ +#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */ +#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */ +#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len); diff --git a/src/chat/chat_member.c b/src/chat/chat_member.c new file mode 100644 index 00000000..26a5e0bd --- /dev/null +++ b/src/chat/chat_member.c @@ -0,0 +1,362 @@ +/* + * chat_member.c — единый код запроса мемберов для GUI + * + * Используется: chatgui (desktop), headless CLI, Android. + * Все три потребителя получают данные через chat_core_get_member_list(). + */ + +#include "chat_core_priv.h" +#include "chat_member.h" +#include "chat_event.h" +#include "member_sync.h" + +#include "../utun_instance.h" +#include "../../lib/mem.h" +#include "../../lib/ll_queue.h" +#include "../../lib/json_flat.h" +#include "../../lib/platform_compat.h" + +/* ─── парсинг adm_tags в flags ─── */ + +static uint8_t parse_adm_tags_flags(const char* adm_tags) { + if (!adm_tags || !adm_tags[0]) return 0; + uint8_t flags = 0; + + /* supernode */ + char buf[256]; + if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0 + && strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_SUPERNODE; + + /* admin: значение — накопительная строка ed...ed..., последний символ e/d */ + if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) { + const char* last = buf; + for (const char* p = buf; *p; p++) + if (*p == 'e' || *p == 'd') last = p; + if (*last == 'e') flags |= CHAT_MEMBER_FLAG_ADMIN; + } + + /* moder — аналогично admin */ + if (json_flat_get(adm_tags, "moder", buf, sizeof(buf)) == 0) { + const char* last = buf; + for (const char* p = buf; *p; p++) + if (*p == 'e' || *p == 'd') last = p; + if (*last == 'e') flags |= CHAT_MEMBER_FLAG_MODER; + } + return flags; +} + +/* ─── сериализация (struct → wire) ─── */ + +void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst) { + uint64_t nid = src->node_id; + uint16_t rtt = src->rtt; + memcpy(dst, &nid, 8); + dst[8] = src->online; + dst[9] = src->is_self; + dst[10] = src->flags; + memcpy(dst + 11, &rtt, 2); + dst[13] = src->nat_type; + dst[14] = src->storage; + memcpy(dst + 15, src->name, 64); +} + +int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst) { + if (!src || !dst) return -1; + memcpy(&dst->node_id, src, 8); + dst->online = src[8]; + dst->is_self = src[9]; + dst->flags = src[10]; + memcpy(&dst->rtt, src + 11, 2); + dst->nat_type = src[13]; + dst->storage = src[14]; + memcpy(dst->name, src + 15, 64); + dst->name[63] = '\0'; + return 0; +} + +/* ─── запрос списка мемберов ─── */ + +int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) { + if (!out || !count) return -1; + *out = NULL; *count = 0; + if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — not initialized"); + return -1; + } + + char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); + uint64_t my_id = g_cc.my_node_id; + + /* 1. запрашиваем всех мемберов */ + sqlite3_stmt* st = NULL; + char sql[512]; + snprintf(sql, sizeof(sql), + "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,'')," + " COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')," + " COALESCE(p.storage,0)" + " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" + " ORDER BY n.online DESC, p.node_id ASC", peers_tbl); + + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — query failed ch=%s err=%s", + ch_id, sqlite3_errmsg(g_cc.db)); + return -1; + } + + struct chat_member_display* members = NULL; + int cap = 0, cnt = 0; + + while (sqlite3_step(st) == SQLITE_ROW) { + if (cnt >= cap) { + cap = cap ? cap * 2 : 32; + struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members)); + if (!tmp) { u_free(members); sqlite3_finalize(st); return -1; } + members = tmp; + } + struct chat_member_display* m = &members[cnt]; + m->node_id = (uint64_t)sqlite3_column_int64(st, 0); + m->online = (uint8_t)sqlite3_column_int(st, 1); + const char* node_name = (const char*)sqlite3_column_text(st, 2); + const char* local_nick = (const char*)sqlite3_column_text(st, 3); + const char* adm_tags = (const char*)sqlite3_column_text(st, 4); + m->storage = (uint8_t)sqlite3_column_int(st, 5); + m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0); + m->flags = parse_adm_tags_flags(adm_tags); + m->rtt = 0xFFFF; + m->nat_type = 0xFF; + + /* display name: local_nick если задан, иначе node name, иначе short hex */ + const char* display = (local_nick && local_nick[0]) ? local_nick : + (node_name && node_name[0]) ? node_name : NULL; + if (display) { + strncpy(m->name, display, sizeof(m->name) - 1); + m->name[sizeof(m->name) - 1] = '\0'; + } else { + snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF)); + } + cnt++; + } + sqlite3_finalize(st); + + if (cnt == 0) { + *out = NULL; *count = 0; + return 0; + } + + /* 2. RTT и NAT type из node_addresses (batch) */ + char* id_list = u_malloc((size_t)cnt * 21 + 4); + if (!id_list) { u_free(members); return -1; } + id_list[0] = '\0'; + for (int i = 0; i < cnt; i++) { + if (i > 0) strcat(id_list, ","); + char nbuf[24]; snprintf(nbuf, sizeof(nbuf), "%llu", (unsigned long long)members[i].node_id); + strcat(id_list, nbuf); + } + + snprintf(sql, sizeof(sql), + "SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses" + " WHERE node_id IN (%s) AND rtt > 0 GROUP BY node_id", id_list); + u_free(id_list); + + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) { + while (sqlite3_step(st) == SQLITE_ROW) { + uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); + int rtt_val = sqlite3_column_int(st, 1); + int at = sqlite3_column_int(st, 2); + for (int i = 0; i < cnt; i++) { + if (members[i].node_id == nid) { + members[i].rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val); + members[i].nat_type = (uint8_t)at; + break; + } + } + } + sqlite3_finalize(st); + } + + /* 3. проверяем connected (активное ETCP-соединение) */ + for (int i = 0; i < cnt; i++) { + if (!members[i].online) continue; + if (g_cc.inst->connections + && queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&members[i].node_id)) + members[i].online = 2; /* 2 = connected (online + active conn) */ + } + + /* 4. сериализуем в wire-формат */ + size_t wire_sz = (size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE; + uint8_t* wire = u_malloc(wire_sz); + if (!wire) { u_free(members); return -1; } + for (int i = 0; i < cnt; i++) + chat_core_serialize_member(&members[i], wire + i * CHAT_MEMBER_DISPLAY_SIZE); + u_free(members); + + *out = wire; + *count = cnt; + return 0; +} + +/* ─── один мембер ─── */ + +int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out) { + if (!out) return -1; + if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) return -1; + + char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); + uint64_t my_id = g_cc.my_node_id; + + sqlite3_stmt* st = NULL; + char sql[384]; + snprintf(sql, sizeof(sql), + "SELECT COALESCE(n.online,0), COALESCE(n.name,'')," + " COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')," + " COALESCE(p.storage,0)" + " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" + " WHERE p.node_id=?", peers_tbl); + + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; } + + struct chat_member_display m; + m.node_id = node_id; + m.online = (uint8_t)sqlite3_column_int(st, 0); + const char* node_name = (const char*)sqlite3_column_text(st, 1); + const char* local_nick = (const char*)sqlite3_column_text(st, 2); + const char* adm_tags = (const char*)sqlite3_column_text(st, 3); + m.storage = (uint8_t)sqlite3_column_int(st, 4); + m.is_self = (uint8_t)(node_id == my_id ? 1 : 0); + m.flags = parse_adm_tags_flags(adm_tags); + m.rtt = 0xFFFF; + m.nat_type = 0xFF; + + const char* display = (local_nick && local_nick[0]) ? local_nick : + (node_name && node_name[0]) ? node_name : NULL; + if (display) { + strncpy(m.name, display, sizeof(m.name) - 1); + m.name[sizeof(m.name) - 1] = '\0'; + } else { + snprintf(m.name, sizeof(m.name), "0x%04X", (unsigned)(node_id & 0xFFFF)); + } + sqlite3_finalize(st); + + /* RTT / NAT */ + snprintf(sql, sizeof(sql), + "SELECT MIN(rtt), MIN(addr_type) FROM node_addresses" + " WHERE node_id=? AND rtt > 0"); + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) { + sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (sqlite3_step(st) == SQLITE_ROW) { + int rtt_val = sqlite3_column_int(st, 0); + m.rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val); + m.nat_type = (uint8_t)sqlite3_column_int(st, 1); + } + sqlite3_finalize(st); + } + + /* connected */ + if (m.online && g_cc.inst->connections + && queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&node_id)) + m.online = 2; + + chat_core_serialize_member(&m, out); + return 0; +} + +/* ─── трамплины для gui_bridge ─── */ + +void chat_core_request_member_list_trampoline(void* arg) { + if (!arg || !g_cc.initialized) { u_free(arg); return; } + char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0'; + u_free(arg); + + uint8_t* members = NULL; + int count = 0; + if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return; + + size_t ch_len = strlen(ch_id); + size_t evt_sz = 1 + ch_len + 2 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE; + uint8_t* evt = u_malloc(evt_sz); + if (!evt) { u_free(members); return; } + + uint16_t u16cnt = (uint16_t)count; + evt[0] = (uint8_t)ch_len; + memcpy(evt + 1, ch_id, ch_len); + memcpy(evt + 1 + ch_len, &u16cnt, 2); + memcpy(evt + 1 + ch_len + 2, members, (size_t)count * CHAT_MEMBER_DISPLAY_SIZE); + chat_event_post(CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz); + + u_free(evt); + u_free(members); +} + +void chat_core_request_member_rtt_trampoline(void* arg) { + if (!arg || !g_cc.initialized) { u_free(arg); return; } + char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0'; + u_free(arg); + + uint8_t* members = NULL; + int count = 0; + if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return; + + size_t ch_len = strlen(ch_id); + size_t evt_sz = 1 + ch_len + 1 + CHAT_MEMBER_DISPLAY_SIZE; + uint8_t* evt = u_malloc(evt_sz); + if (!evt) { u_free(members); return; } + + /* шлём мемберов по одному (RTT мог измениться у каждого) */ + uint16_t rtt_nodata = 0xFFFF; + for (int i = 0; i < count; i++) { + uint16_t rtt_val; + memcpy(&rtt_val, members + i * CHAT_MEMBER_DISPLAY_SIZE + 11, 2); + if (rtt_val == rtt_nodata) continue; + evt[0] = (uint8_t)ch_len; + memcpy(evt + 1, ch_id, ch_len); + evt[1 + ch_len] = 1; + memcpy(evt + 1 + ch_len + 1, members + i * CHAT_MEMBER_DISPLAY_SIZE, CHAT_MEMBER_DISPLAY_SIZE); + chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz); + } + + u_free(evt); + u_free(members); +} + +/* ─── node_props_changed callback (member_sync → chat_event) ─── */ + +static void on_adm_tags_changed(uint64_t node_id, const char* adm_tags, void* arg) { + (void)adm_tags; (void)arg; + if (!g_cc.initialized || !g_cc.inst || !g_cc.db) return; + + struct chat_core_ctx* cc = &g_cc; + for (int i = 0; i < cc->si_count; i++) { + const char* ch_id = cc->si_ch_id[i]; + if (!ch_id) continue; + + char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "SELECT 1 FROM \"%s\" WHERE node_id=?", peers_tbl); + + sqlite3_stmt* st = NULL; + 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) { + sqlite3_finalize(st); + size_t cl = strlen(ch_id); + uint8_t evt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; + evt[0] = (uint8_t)cl; + memcpy(evt + 1, ch_id, cl); + evt[1 + cl] = 1; + if (chat_core_get_single_member(ch_id, node_id, + evt + 1 + cl + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, + 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE); + return; + } + sqlite3_finalize(st); + } + } +} + +void chat_member_init(void) { + member_sync_add_props_cbk(on_adm_tags_changed, NULL); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (adm_tags → MEMBER_UPDATED)"); +} diff --git a/src/chat/chat_member.h b/src/chat/chat_member.h new file mode 100644 index 00000000..c0a43bef --- /dev/null +++ b/src/chat/chat_member.h @@ -0,0 +1,70 @@ +/* + * chat_member.h — единая структура мембера для отображения в GUI + * + * Используется chatgui (desktop), headless CLI, Android. + * Все три потребителя получают данные через chat_core_get_member_list(), + * а не прямыми SQL-запросами. + * + * Формат на проводе (ручная сериализация, фиксированный размер 79 байт): + * [0-7] node_id uint64_t LE + * [8] online uint8_t + * [9] is_self uint8_t + * [10] flags uint8_t 0x01=supernode 0x02=admin 0x04=moder + * [11-12] rtt uint16_t LE (ms, 0xFFFF=нет данных) + * [13] nat_type uint8_t ADDR_TYPE_* + * [14] storage uint8_t + * [15-78] name char[64] local_nick если задан, иначе node name. \0-padded + */ + +#ifndef CHAT_MEMBER_H +#define CHAT_MEMBER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CHAT_MEMBER_FLAG_SUPERNODE 0x01 +#define CHAT_MEMBER_FLAG_ADMIN 0x02 +#define CHAT_MEMBER_FLAG_MODER 0x04 + +#define CHAT_MEMBER_DISPLAY_SIZE 79 + +/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */ +int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count); + +/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */ +int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out); + +/* Сериализовать из внутренней структуры в wire-формат */ +struct chat_member_display { + uint64_t node_id; + uint8_t online; + uint8_t is_self; + uint8_t flags; + uint16_t rtt; + uint8_t nat_type; + uint8_t storage; + char name[64]; +}; + +void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst); +int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst); + +/* Трамплин для gui_bridge: GUI запрашивает полный список мемберов канала. + * arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_LIST. */ +void chat_core_request_member_list_trampoline(void* arg); + +/* Трамплин для gui_bridge: GUI запрашивает RTT мемберов канала. + * arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */ +void chat_core_request_member_rtt_trampoline(void* arg); + +/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */ +void chat_member_init(void); + +#ifdef __cplusplus +} +#endif +#endif /* CHAT_MEMBER_H */ diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 6b8e2e8e..1538d53e 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -6,6 +6,7 @@ #include "chat_core_priv.h" #include "chat_event.h" +#include "chat_member.h" #include "../../lib/json_flat.h" #include "../routing_layer/topo_node_sqlite.h" #include "member_sync.h" @@ -73,9 +74,11 @@ void chat_core_update_my_name(const char* name) { int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, new_sig, join_ts, NULL, 0, juser, NULL, 0, NULL, NULL, 0); if (r2 != 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r2); - uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch); - evt[0] = cl; memcpy(evt + 1, ch, cl); - chat_event_post(CHAT_EVT_MEMBERS_CHANGED, evt, 1 + cl); + else member_sync_broadcast_one(g_cc.inst, ch, myid); + uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; uint8_t cl = (uint8_t)strlen(ch); + mevt[0] = cl; memcpy(mevt + 1, ch, cl); mevt[1 + cl] = 1; + if (chat_core_get_single_member(ch, myid, mevt + 1 + cl + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + cl + 1 + CHAT_MEMBER_DISPLAY_SIZE); } sqlite3_finalize(ps); } diff --git a/src/chat/chat_status.c b/src/chat/chat_status.c index acaec267..3e9616c2 100644 --- a/src/chat/chat_status.c +++ b/src/chat/chat_status.c @@ -14,7 +14,9 @@ #include "../../lib/platform_compat.h" #include "../transport_layer/etcp.h" #include "../transport_layer/etcp_connections.h" +#include "../transport_layer/stcp_link.h" #include "../transport_layer/pkt_normalizer.h" +#include static const char* nat_type_str(uint8_t t) { switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; } @@ -288,3 +290,140 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) { u_free(arg); collect_conn_metrics(peer_id); } + +/* ── Live-снапшот для панели деталей участника ── + * packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:24B)*[sock_count:1](sock:48B)* + * flags: bit0=conn_present bit1=conn_up(links_up) bit2=conn_initialized + * link: [is_tcp:1][link_state:1][link_status:1][tcp_ready:1][family:1][addr_len:1][addr:16][port:2] + * sock: [sock_id:4][port:2][link_count:4][ifname:38] + * Собирается в worker-потоке — читает живые структуры UTUN_INSTANCE безопасно. */ + +#define MEMBER_DETAIL_MAX_LINKS 16 +#define MEMBER_DETAIL_MAX_SOCKS 16 +#define MEMBER_DETAIL_LINK_SIZE 24 +#define MEMBER_DETAIL_SOCK_SIZE 48 + +/* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */ +static void sock_ifname(const char* name, char* out, size_t out_sz) { + out[0] = '\0'; + if (!name || !name[0]) return; + const char* last = NULL; + for (const char* q = name; (q = strstr(q, "_v")) != NULL; q++) last = q; + if (!last) return; + int idx = (int)(last - name); + if (idx <= 3) return; + int len = idx - 3; + if (len >= (int)out_sz) len = (int)out_sz - 1; + memcpy(out, name + 3, len); + out[len] = '\0'; +} + +static void collect_member_detail(uint64_t node_id) { + if (!g_cc.initialized || !g_cc.inst) return; + struct UTUN_INSTANCE* inst = g_cc.inst; + + uint8_t flags = 0; + struct ETCP_CONN* conn = instance_find_conn(inst, node_id); + if (conn) { + flags |= 1; + if (conn->links_up) flags |= 2; + if (conn->initialized) flags |= 4; + } + + uint16_t own_tcp_active = 0; + if (node_id == inst->node_id) { + if (inst->connections) { + for (struct ll_entry* e = inst->connections->head; e; e = e->next) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; + if (ce && ce->conn && ce->conn->initialized) { + for (struct ETCP_LINK* lk = ce->conn->links; lk; lk = lk->next) + if (lk->is_tcp) { own_tcp_active++; break; } + } + } + } + if (inst->tcp_connections) { + for (struct ll_entry* e = inst->tcp_connections->head; e; e = e->next) { + struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; + if (te && te->etcp_conn && te->etcp_conn->initialized) own_tcp_active++; + } + } + } + + uint8_t link_count = 0; + for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < MEMBER_DETAIL_MAX_LINKS; lk = lk->next) + link_count++; + + uint8_t sock_count = 0; + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < MEMBER_DETAIL_MAX_SOCKS; s = s->next) + sock_count++; + for (struct TCP_SOCKET* ts = inst->tcp_sockets; ts && sock_count < MEMBER_DETAIL_MAX_SOCKS; ts = ts->next) + sock_count++; + + size_t buf_sz = 12 + (size_t)link_count * MEMBER_DETAIL_LINK_SIZE + + 1 + (size_t)sock_count * MEMBER_DETAIL_SOCK_SIZE; + uint8_t* buf = (uint8_t*)u_malloc(buf_sz); + if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "collect_member_detail: buf alloc failed"); return; } + + uint8_t* p = buf; + memcpy(p, &node_id, 8); p += 8; + *p++ = flags; + memcpy(p, &own_tcp_active, 2); p += 2; + *p++ = link_count; + + for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count; lk = lk->next, link_count--) { + *p++ = lk->is_tcp ? 1 : 0; + *p++ = lk->link_state; + *p++ = lk->link_status; + *p++ = (lk->is_tcp && lk->tcp_link && stcp_link_is_ready(lk->tcp_link)) ? 1 : 0; + uint8_t family = 0, addr_len = 0; uint8_t addr[16] = {0}; uint16_t port = 0; + const struct sockaddr_storage* sa = &lk->remote_addr; + if (sa->ss_family == AF_INET) { + struct sockaddr_in* sin = (struct sockaddr_in*)sa; + family = 4; addr_len = 4; memcpy(addr, &sin->sin_addr, 4); port = ntohs(sin->sin_port); + } else if (sa->ss_family == AF_INET6) { + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; + family = 6; addr_len = 16; memcpy(addr, &sin6->sin6_addr, 16); port = ntohs(sin6->sin6_port); + } + *p++ = family; + *p++ = addr_len; + memcpy(p, addr, 16); p += 16; + memcpy(p, &port, 2); p += 2; + } + + *p++ = sock_count; + for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count; s = s->next, sock_count--) { + uint32_t sid = s->sock_id; + uint16_t sp = 0; + const struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr; + if (sa->ss_family == AF_INET) sp = ntohs(((const struct sockaddr_in*)sa)->sin_port); + else if (sa->ss_family == AF_INET6) sp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port); + uint32_t lc = s->links_queue ? (uint32_t)queue_entry_count(s->links_queue) : 0; + char ifname[38] = {0}; sock_ifname(s->name, ifname, sizeof(ifname)); + memcpy(p, &sid, 4); p += 4; + memcpy(p, &sp, 2); p += 2; + memcpy(p, &lc, 4); p += 4; + memcpy(p, ifname, 38); p += 38; + } + for (struct TCP_SOCKET* ts = inst->tcp_sockets; ts && sock_count; ts = ts->next, sock_count--) { + uint32_t sid = ts->sock_id; + uint16_t sp = 0; + const struct sockaddr_storage* sa = ts->interface_addr.ss_family ? &ts->interface_addr : &ts->local_addr; + if (sa->ss_family == AF_INET) sp = ntohs(((const struct sockaddr_in*)sa)->sin_port); + else if (sa->ss_family == AF_INET6) sp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port); + char ifname[38] = {0}; sock_ifname(ts->name, ifname, sizeof(ifname)); + memcpy(p, &sid, 4); p += 4; + memcpy(p, &sp, 2); p += 2; + uint32_t lc = 0; memcpy(p, &lc, 4); p += 4; + memcpy(p, ifname, 38); p += 38; + } + + chat_event_post(CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf)); + u_free(buf); +} + +void chat_core_collect_member_detail_trampoline(void* arg) { + uint64_t node_id; + memcpy(&node_id, arg, sizeof(node_id)); + u_free(arg); + collect_member_detail(node_id); +} diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index dbf06469..43be2255 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -1,6 +1,7 @@ #include "chat_sync.h" #include "chat_core.h" #include "chat_core_priv.h" +#include "chat_member.h" #include "chat_setting.h" #include "chat_event.h" #include "../routing_layer/topo_node_sqlite.h" @@ -361,12 +362,15 @@ static void cs_on_remote_status_changed(uint64_t peer, int online) { for (int i = 0; i < g_cs->channel_count; i++) { for (int j = 0; j < g_cs->channels[i].peer_count; j++) { if (g_cs->channels[i].peer_ids[j] == peer) { - size_t cl = strlen(g_cs->channels[i].channel_id); - if (cl > 63) cl = 63; - uint8_t evt[65]; evt[0] = (uint8_t)cl; - memcpy(evt + 1, g_cs->channels[i].channel_id, cl); - chat_event_post(CHAT_EVT_MEMBERS_CHANGED, evt, 1 + (int)cl); - cs_post_channel_online(g_cs, g_cs->channels[i].channel_id); + const char* ch_id = g_cs->channels[i].channel_id; + size_t cl = strlen(ch_id); + uint8_t evt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; + evt[0] = (uint8_t)cl; + memcpy(evt + 1, ch_id, cl); + evt[1 + cl] = 1; + if (chat_core_get_single_member(ch_id, peer, evt + 1 + cl + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE); + cs_post_channel_online(g_cs, ch_id); break; } } @@ -383,12 +387,15 @@ static void cs_on_peer_status_changed(uint64_t peer, int online) { for (int j = 0; j < g_cs->channels[i].peer_count; j++) { if (g_cs->channels[i].peer_ids[j] == peer) { found_in_channel = 1; - size_t cl = strlen(g_cs->channels[i].channel_id); - if (cl > 63) cl = 63; - uint8_t evt[65]; evt[0] = (uint8_t)cl; - memcpy(evt + 1, g_cs->channels[i].channel_id, cl); - chat_event_post(CHAT_EVT_MEMBERS_CHANGED, evt, 1 + (int)cl); - cs_post_channel_online(g_cs, g_cs->channels[i].channel_id); + const char* ch_id = g_cs->channels[i].channel_id; + size_t cl = strlen(ch_id); + uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; + mevt[0] = (uint8_t)cl; + memcpy(mevt + 1, ch_id, cl); + mevt[1 + cl] = 1; + if (chat_core_get_single_member(ch_id, peer, mevt + 1 + cl + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE); + cs_post_channel_online(g_cs, ch_id); /* 3. Рассылка дельты всем synced-соседям */ uint8_t st = (uint8_t)online; @@ -524,7 +531,8 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) { switch (status) { case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break; - case ETCP_CONN_STATUS_DOWN: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break; + case ETCP_CONN_STATUS_DOWN: + case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break; default: break; } } @@ -1280,8 +1288,10 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer, /* add to channel cache — always refresh to include new peer */ cs_refresh_channels(cs); - { uint8_t evt[65]; uint8_t cl=(uint8_t)strlen(ch_id); evt[0]=cl; memcpy(evt+1,ch_id,cl); - chat_event_post(CHAT_EVT_MEMBERS_CHANGED, evt, 1+cl); } + { uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; uint8_t cl = (uint8_t)strlen(ch_id); + mevt[0] = cl; memcpy(mevt + 1, ch_id, cl); mevt[1 + cl] = 1; + if (chat_core_get_single_member(ch_id, node_id, mevt + 1 + cl + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + cl + 1 + CHAT_MEMBER_DISPLAY_SIZE); } DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN calling cs_post_channel_online ch=%s", CS_ID, ch_id); cs_post_channel_online(cs, ch_id); @@ -1440,7 +1450,22 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer, evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len); chat_event_post(CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); - chat_event_post(CHAT_EVT_MEMBERS_CHANGED, evt, 1 + ch_id_len); + /* full member list for the newly joined channel */ + { uint8_t* members = NULL; int mc = 0; + if (chat_core_get_member_list(ch_id, &members, &mc) == 0 && members) { + size_t esz = 1 + ch_id_len + 2 + (size_t)mc * CHAT_MEMBER_DISPLAY_SIZE; + uint8_t* mevt = u_malloc(esz); + if (mevt) { + uint16_t u16 = (uint16_t)mc; + mevt[0] = ch_id_len; memcpy(mevt + 1, ch_id, ch_id_len); + memcpy(mevt + 1 + ch_id_len, &u16, 2); + memcpy(mevt + 1 + ch_id_len + 2, members, (size_t)mc * CHAT_MEMBER_DISPLAY_SIZE); + chat_event_post(CHAT_EVT_MEMBER_LIST, mevt, (int)esz); + u_free(mevt); + } + u_free(members); + } + } DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: WELCOME calling cs_post_channel_online ch=%s", CS_ID, ch_id); cs_post_channel_online(cs, ch_id); @@ -1536,7 +1561,10 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer, cs_refresh_channels(cs); - { uint8_t mevt[65]; uint8_t ml = (uint8_t)strlen(ch_id); mevt[0] = ml; memcpy(mevt + 1, ch_id, ml); chat_event_post(CHAT_EVT_MEMBERS_CHANGED, mevt, 1 + ml); } + { uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE]; uint8_t ml = (uint8_t)strlen(ch_id); + mevt[0] = ml; memcpy(mevt + 1, ch_id, ml); mevt[1 + ml] = 1; + if (chat_core_get_single_member(ch_id, node_id, mevt + 1 + ml + 1) == 0) + chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + ml + 1 + CHAT_MEMBER_DISPLAY_SIZE); } /* propagate to others (except sender and the subject node) */ cs_propagate(cs, ch_id, peer, pl, len); @@ -1556,7 +1584,10 @@ static void cs_handle_peer_remove(struct chat_sync* cs, uint64_t peer, topo_node_sqlite_member_del(db, ch_id, node_id); cs_refresh_channels(cs); - { uint8_t mevt[65]; uint8_t ml = (uint8_t)strlen(ch_id); mevt[0] = ml; memcpy(mevt + 1, ch_id, ml); chat_event_post(CHAT_EVT_MEMBERS_CHANGED, mevt, 1 + ml); } + { uint8_t mevt[1 + 64 + 8]; uint8_t ml = (uint8_t)strlen(ch_id); + mevt[0] = ml; memcpy(mevt + 1, ch_id, ml); + memcpy(mevt + 1 + ml, &node_id, 8); + chat_event_post(CHAT_EVT_MEMBER_REMOVED, mevt, 1 + ml + 8); } cs_propagate(cs, ch_id, peer, pl, len); diff --git a/src/chat/db_sync.c b/src/chat/db_sync.c index b807b60b..ec57a6c4 100644 --- a/src/chat/db_sync.c +++ b/src/chat/db_sync.c @@ -1258,7 +1258,8 @@ static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) static void db_sync_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) { switch (status) { case ETCP_CONN_STATUS_UP: db_sync_on_conn_up(conn, arg); break; - case ETCP_CONN_STATUS_DOWN: db_sync_on_conn_down(conn, arg); break; + case ETCP_CONN_STATUS_DOWN: + case ETCP_CONN_STATUS_DELETE: db_sync_on_conn_down(conn, arg); break; default: break; } } diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 7a0086f8..f3ca53fa 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -511,6 +511,87 @@ void member_sync_cancel(struct UTUN_INSTANCE* inst, uint64_t peer, const char* c merkle_sync_cancel(inst, peer, ch_id); } +void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id) { + if (!inst || !ch_id) return; + sqlite3* db = _db(inst); if (!db) return; + char peers_tbl[128]; _peers_table(ch_id, peers_tbl, sizeof(peers_tbl)); + + sqlite3_stmt* stmt = NULL; + char sql[512]; snprintf(sql, sizeof(sql), + "SELECT node_id, x25519_pubkey, ed25519_pubkey," + " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig" + " FROM \"%s\" WHERE node_id=?", peers_tbl); + if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast_one — query failed ch=%s nid=0x%016llx", MS_ID, ch_id, (unsigned long long)member_id); + return; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)member_id); + if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return; } + + uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); + const uint8_t* x25 = (const uint8_t*)sqlite3_column_blob(stmt, 1); + const uint8_t* ed = (const uint8_t*)sqlite3_column_blob(stmt, 2); + const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(stmt, 3); + uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4); + const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); + uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); + const char* nm = (const char*)sqlite3_column_text(stmt, 7); + const char* atags = (const char*)sqlite3_column_text(stmt, 8); + const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 9); + if (!x25 || !ed) { sqlite3_finalize(stmt); return; } + uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; + uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; + + sqlite3_stmt* as = NULL; + sqlite3_prepare_v2(db, + "SELECT family, socket_id, protocol, address, port FROM node_addresses WHERE node_id=?" + " ORDER BY family, address, port", -1, &as, NULL); + uint8_t addrs[2048]; int addr_off = 0; int addr_count = 0; + if (as) { + sqlite3_bind_int64(as, 1, (sqlite3_int64)nid); + while (sqlite3_step(as) == SQLITE_ROW && addr_off < (int)sizeof(addrs) - 9) { + int fam = sqlite3_column_int(as, 0); + int sid = sqlite3_column_int(as, 1); + int proto = sqlite3_column_int(as, 2); + int ip_sz = fam == 4 ? 4 : 16; + if (addr_off + 3 + ip_sz + 2 > (int)sizeof(addrs)) break; + addrs[addr_off++] = (uint8_t)fam; + addrs[addr_off++] = (uint8_t)sid; + addrs[addr_off++] = (uint8_t)proto; + memcpy(addrs + addr_off, sqlite3_column_blob(as, 3), (size_t)ip_sz); + addr_off += ip_sz; + uint16_t p = (uint16_t)sqlite3_column_int(as, 4); + addrs[addr_off++] = (uint8_t)(p >> 8); + addrs[addr_off++] = (uint8_t)(p & 0xFF); + addr_count++; + } + sqlite3_finalize(as); + } + + uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; + uint8_t buf[8192]; size_t off = 0; + uint16_t wcnt = 1; memcpy(buf, &wcnt, 2); off += 2; + memcpy(buf + off, &nid, 8); off += 8; + memcpy(buf + off, x25, 32); off += 32; + memcpy(buf + off, ed, 32); off += 32; + buf[off++] = flags; + if (flags & PEERS_FLAG_HAS_JOIN) { memcpy(buf + off, sig, 64); off += 64; memcpy(buf + off, &jts, 8); off += 8; } + if (usig && uts) { memcpy(buf + off, usig, 64); off += 64; memcpy(buf + off, &uts, 8); off += 8; } + else { memset(buf + off, 0, 72); off += 72; } + buf[off++] = nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; } + buf[off++] = atl; if (atl) { memcpy(buf + off, atags, atl); off += atl; } + if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); } + else { memset(buf + off, 0, 64); } + off += 64; + buf[off++] = (uint8_t)addr_count; + memcpy(buf + off, addrs, (size_t)addr_off); off += (size_t)addr_off; + sqlite3_finalize(stmt); + + merkle_sync_broadcast(inst, ch_id, inst->node_id, buf, off); + DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast_one ch=%s nid=0x%016llx size=%zu", + MS_ID, ch_id, (unsigned long long)member_id, off); +} + int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id, const uint8_t* x25519, const uint8_t* ed25519, diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index bbc8660e..a2509386 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -111,6 +111,10 @@ int member_sync_del(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t mem /* Количество мемберов в канале (SELECT COUNT из peers_). */ int member_sync_count(struct UTUN_INSTANCE* inst, const char* ch_id); +/* Сериализовать одного мембера и отправить всем подключённым пирам через merkle_sync_broadcast. + Вызывается после локального изменения данных (adm_tags, имя, адреса). */ +void member_sync_broadcast_one(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id); + /* * Установить онлайн-статус узла (nodes.online = 0/1). */ diff --git a/src/chat/merkle_sync.c b/src/chat/merkle_sync.c index d610b7db..0e01cb60 100644 --- a/src/chat/merkle_sync.c +++ b/src/chat/merkle_sync.c @@ -647,7 +647,7 @@ void merkle_sync_broadcast(struct UTUN_INSTANCE* inst, const char* ns, DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: broadcast ns=%s from=%016llx len=%zu", MS_ID, ns, (unsigned long long)from_peer, len); for (struct ms_session* s = ms->sessions; s; s = s->next) { - if (s->sess_state != SESS_SYNCED || s->peer == from_peer || strcmp(s->ns, ns) != 0) continue; + if (s->peer == from_peer || strcmp(s->ns, ns) != 0) continue; _send_broadcast_data(ms, s->peer, ns, data, len); } } diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c index f7fc3c44..1e0b6eaf 100644 --- a/src/media_delivery/media_delivery.c +++ b/src/media_delivery/media_delivery.c @@ -1164,18 +1164,20 @@ static void md_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) { if (!conn) return; uint64_t node_id = conn->peer_node_id; - if (status == ETCP_CONN_STATUS_DOWN || status == ETCP_CONN_STATUS_DELETE) { - /* remove from served_nodes */ + switch (status) { + case ETCP_CONN_STATUS_DOWN: + case ETCP_CONN_STATUS_DELETE: if (md->served_nodes) { struct ll_entry* e = queue_find_data_by_index(md->served_nodes, &node_id); if (e) { queue_remove_data(md->served_nodes, e); queue_entry_free(e); } } - /* mark super_peer as disconnected */ - struct media_super_peer* sp = md_super_peer_find(md, node_id); + { struct media_super_peer* sp = md_super_peer_find(md, node_id); if (sp) { sp->connected = 0; sp->hello_done = 0; sp->inflight_count = 0; if (sp->timeout_timer) { uasync_cancel_timeout(md->inst->ua, sp->timeout_timer); sp->timeout_timer = NULL; } - } + } } + break; + default: break; } } diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index 47d3f964..af91874b 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -258,8 +258,23 @@ static void topo_group_destroy(struct TOPO_GROUP* group) { if (group->conn_mgr) { conn_mgr_destroy(group->conn_mgr); group->conn_mgr = NULL; } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 4 conn_mgr done"); - if (group->nodes) queue_free(group->nodes); - if (group->local_node) { topo_nodeq_free_group_fields(group->instance->topo_groups, group->local_node); u_free(group->local_node); } + + if (group->nodes) { + struct ll_entry* e; int nc = 0; + while ((e = queue_data_get(group->nodes)) != NULL) { + struct TOPO_GROUP_NODE* nq = (struct TOPO_GROUP_NODE*)e; + if (nq->paths) { queue_free(nq->paths); nq->paths = NULL; } + topo_nodeq_free_group_fields(group->instance->topo_groups, nq); + queue_entry_free(e); nc++; + } + queue_free(group->nodes); + if (nc) DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 4b nodes cleaned (%d)", nc); + } + if (group->local_node) { + if (group->local_node->paths) { queue_free(group->local_node->paths); group->local_node->paths = NULL; } + topo_nodeq_free_group_fields(group->instance->topo_groups, group->local_node); + u_free(group->local_node); + } DEBUG_INFO(DEBUG_CATEGORY_SYS, "[GRP_DESTROY] 5 done"); } @@ -357,7 +372,7 @@ void topo_groups_destroy(struct UTUN_INSTANCE* instance) { while ((re = queue_data_get(g->node_registry)) != NULL) { struct TOPO_NODE* node; memcpy(&node, re->data + 8, sizeof(node)); - topo_node_registry_unref(g, node->node_id); + topo_node_destroy(g, node); queue_entry_free(re); ncount++; } queue_free(g->node_registry); diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index 9de93c20..c1883b48 100644 --- a/src/transport_layer/etcp.c +++ b/src/transport_layer/etcp.c @@ -404,7 +404,10 @@ void etcp_connection_close(struct ETCP_CONN* etcp) { // === PHASE 1: detach from external world === + etcp->state = 2; // deleted — blocks ref_take and etcp_send immediately if (etcp->links_up != 0) etcp->links_up = 0; + + etcp_fire_conn_status(etcp, ETCP_CONN_STATUS_DELETE); etcp_cbk_fire(etcp, ETCP_CBK_EVENT_DOWN); // Cancel active timers @@ -432,10 +435,6 @@ void etcp_connection_close(struct ETCP_CONN* etcp) { etcp->conn_queue = NULL; } - etcp->state = 2; // deleted - - etcp_fire_conn_status(etcp, ETCP_CONN_STATUS_DELETE); - // === PHASE 2: deferred resource cleanup (only if no outstanding refs) === if (etcp->ref_count == 0) uasync_call_soon(etcp->instance->ua, etcp, etcp_connection_free_deferred); @@ -463,6 +462,10 @@ void etcp_conn_ref_free(struct ETCP_CONN* conn) { // Reset connection void etcp_conn_reset(struct ETCP_CONN* etcp) { + if (!etcp || etcp->state == 2) { + if (etcp) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] conn_reset on deleted conn", etcp->log_name); + return; + } // Reset IDs DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Resetting ETCP instance [%s]", etcp->log_name); etcp->next_tx_id = 1; @@ -589,6 +592,7 @@ void etcp_conn_reset(struct ETCP_CONN* etcp) { } void etcp_links_reset(struct ETCP_CONN* etcp) {// Если сбой в обмене или ребутнулась одна из сторон -> необходимо заново переинициализировать соединение + if (!etcp || etcp->state == 2) return; // Сбрасываем initialized во всех линках struct ETCP_LINK* link = etcp->links; while (link) { @@ -598,6 +602,8 @@ void etcp_links_reset(struct ETCP_CONN* etcp) {// Если сбой в обме } void etcp_conn_reinit(struct ETCP_CONN* etcp, const char* reason) {// Если сбой в обмене или ребутнулась одна из сторон -> необходимо заново переинициализировать соединение + if (!etcp) return; + if (etcp->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] conn_reinit on deleted conn", etcp->log_name); return; } DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] REINIT: %s (init=%d links=%d reinit=%u tx=%d)", etcp->log_name, reason, etcp->initialized, etcp->links_up, etcp->reinit_count, etcp->tx_state); @@ -632,6 +638,7 @@ void etcp_conn_reinit(struct ETCP_CONN* etcp, const char* reason) {// Если // внутренняя функция. Вызывается один раз когда первый линк готов. void etcp_conn_ready(struct ETCP_CONN* conn) { if (!conn) return; + if (conn->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] conn_ready on deleted conn", conn->log_name); return; } if (conn->initialized) return; // already ready conn->initialized = 1; @@ -718,6 +725,11 @@ int etcp_int_send(struct ETCP_CONN* etcp, const void* data, uint16_t len) { return -1; } + if (etcp->state == 2) { + DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] int_send on deleted conn", etcp->log_name); + return -1; + } + if (!etcp->input_queue) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] input_queue is NULL for etcp=%p", etcp->log_name, etcp); return -1; @@ -801,7 +813,7 @@ static void input_queue_try_resume(struct ETCP_CONN* etcp) {// при ACK // Called from link-level etcp_link_update_inflight_lim() after inflight_lim_bytes changes. // Recalculates connection-level optimal_inflight and resumes input_queue if room opened up. void etcp_conn_on_inflight_lim_changed(struct ETCP_CONN* etcp) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; uint32_t sum = 0; for (struct ETCP_LINK* tl = etcp->links; tl; tl = tl->next) sum += tl->inflight_lim_bytes; etcp->optimal_inflight = sum; @@ -991,6 +1003,7 @@ static void send_ack_req_cb(struct ll_queue* q, void* arg) {// etcp->ack_q data */ void etcp_on_link_down(struct ETCP_CONN* etcp, struct ETCP_LINK* down_link) { DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); + if (!etcp || etcp->state == 2) return; int up=0; struct ETCP_LINK* link = etcp->links; while (link) { @@ -1070,6 +1083,8 @@ static void etcp_conn_process_send_queue(struct ETCP_CONN* etcp) {// вызыв // вызывается линком когда освобождается или очередью если появляются данные на передачу struct ETCP_DGRAM* etcp_request_pkt(struct ETCP_CONN* etcp) { DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); + if (!etcp) return NULL; + if (etcp->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] request_pkt on deleted conn", etcp->log_name); return NULL; } struct ETCP_LINK* link = NULL; // если есть активный burst — используем этот линк напрямую for (struct ETCP_LINK* l = etcp->links; l; l = l->next) { @@ -1385,6 +1400,7 @@ void etcp_output_try_assembly(struct ETCP_CONN* etcp) { void etcp_ack_recv(struct ETCP_CONN* etcp, uint32_t seq, uint16_t ts, uint16_t dts) { DEBUG_TRACE(DEBUG_CATEGORY_ETCP, ""); if (!etcp) return; + if (etcp->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] ack_recv on deleted conn", etcp->log_name); return; } // DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] processing ACK for seq=%u, ts=%u, dts=%u", etcp->log_name, seq, ts, dts); @@ -1820,7 +1836,7 @@ void etcp_conn_input(struct ETCP_DGRAM* pkt) { } void etcp_update_mtu(struct ETCP_CONN* etcp) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; int new_mtu = PACKET_DATA_MAX_MTU; int has_links = 0; struct ETCP_LINK* link = etcp->links; @@ -1852,7 +1868,7 @@ void etcp_metrics_init(struct etcp_metrics* m) { } void etcp_metrics_add_rtt(struct ETCP_CONN* etcp, uint16_t rtt_tb) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; struct etcp_metrics* m = &etcp->metrics; int i; for (i = 0; i < (int)ETCP_METRICS_RTT_BOUNDS_COUNT; i++) { @@ -1864,7 +1880,7 @@ void etcp_metrics_add_rtt(struct ETCP_CONN* etcp, uint16_t rtt_tb) { } void etcp_metrics_add_sent(struct ETCP_CONN* etcp, uint32_t len) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; struct etcp_metrics* m = &etcp->metrics; m->work_sent++; m->work_bytes_sent += len; @@ -1873,7 +1889,7 @@ void etcp_metrics_add_sent(struct ETCP_CONN* etcp, uint32_t len) { } void etcp_metrics_add_rcvd(struct ETCP_CONN* etcp, uint32_t len) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; struct etcp_metrics* m = &etcp->metrics; m->work_rcvd++; m->work_bytes_rcvd += len; @@ -1882,7 +1898,7 @@ void etcp_metrics_add_rcvd(struct ETCP_CONN* etcp, uint32_t len) { } void etcp_metrics_add_loss(struct ETCP_CONN* etcp, uint32_t count) { - if (!etcp) return; + if (!etcp || etcp->state == 2) return; struct etcp_metrics* m = &etcp->metrics; m->work_lost += count; m->total_lost += count; @@ -2001,7 +2017,7 @@ static void metrics_snapshot_timer_cb(void* arg) { } void etcp_metrics_start_timer(struct ETCP_CONN* etcp) { - if (!etcp || etcp->metrics.timer) return; + if (!etcp || etcp->state == 2 || etcp->metrics.timer) return; etcp_metrics_init(&etcp->metrics); etcp->metrics.timer = uasync_set_timeout(etcp->instance->ua, ETCP_METRICS_INTERVAL_TB, etcp, metrics_snapshot_timer_cb, "etcp_metrics"); DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "[%s] Metrics timer started (interval=%ums)", etcp->log_name, ETCP_METRICS_INTERVAL_TB / 10); diff --git a/src/transport_layer/etcp_api.c b/src/transport_layer/etcp_api.c index 53de4dcd..10467812 100644 --- a/src/transport_layer/etcp_api.c +++ b/src/transport_layer/etcp_api.c @@ -149,7 +149,7 @@ void etcp_socket_cbk_fire(struct ETCP_SOCKET* sock, int event) { while (cbe) { struct etcp_socket_cbk_entry* n = cbe->next; if (cbe->event_mask & event) cbe->fn(sock, event, cbe->arg); cbe = n; } } void etcp_set_routing_exchange_state(struct ETCP_CONN* conn, uint8_t new_state) { - if (!conn) return; + if (!conn || conn->state == 2) return; conn->routing_exchange_active = new_state; if (new_state >= 3 && conn->bgp_ready_cbk) conn->bgp_ready_cbk(conn); diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 528cb466..1ac222ba 100644 --- a/src/transport_layer/etcp_connections.c +++ b/src/transport_layer/etcp_connections.c @@ -909,6 +909,8 @@ void tcp_socket_remove(struct TCP_SOCKET* sock) { struct ETCP_LINK* etcp_link_new(struct ETCP_CONN* etcp, struct ETCP_SOCKET* conn, struct sockaddr_storage* remote_addr, uint8_t is_server) { DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, ""); + if (!etcp) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "null etcp"); return NULL; } + if (etcp->state == 2) { DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "[%s] link_new on deleted conn", etcp->log_name); return NULL; } int is_tcp = (conn == NULL); struct ETCP_LINK* link = u_calloc(1, sizeof(struct ETCP_LINK)); diff --git a/src/utun_instance.c b/src/utun_instance.c index 89a4e1d5..05d3afe6 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -513,14 +513,7 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { } queue_free(instance->tcp_connections); instance->tcp_connections = NULL; while (instance->tcp_sockets) tcp_socket_remove(instance->tcp_sockets); - /* Drain deferred callbacks so stcp_link_close_impl runs before ETCP conn close */ - { - int deferred_loops = 0; - while (instance->ua && instance->ua->immediate_queue_head) { - uasync_poll(instance->ua, 0); - if (++deferred_loops > 1000) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[DESTROY] J: deferred loop stuck"); break; } - } - } + uasync_drain_immediate(instance->ua); DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] J done — TCP"); /* Phase K: ETCP connections */ @@ -538,14 +531,9 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) { queue_free(instance->connections); instance->connections = NULL; DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] K done — connections"); - /* Phase L: drain deferred + pings */ - { - int deferred_loops = 0; - while (instance->ua && instance->ua->immediate_queue_head) { - uasync_poll(instance->ua, 0); - if (++deferred_loops > 1000) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[DESTROY] L: deferred callback loop stuck"); break; } - } - } + uasync_drain_immediate(instance->ua); + + /* Phase L: pings */ { struct PING_CONTEXT* p = instance->pending_pings; int pc = 0; diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 2dc672a2..0622de99 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -433,7 +433,8 @@ int instance_lite_start(const char* config_text) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO); - debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_INFO); + debug_set_category_level(DEBUG_CATEGORY_CHAT_SYNC, DEBUG_LEVEL_TRACE); + debug_set_category_level(DEBUG_CATEGORY_MEMBER_SYNC, DEBUG_LEVEL_TRACE); debug_enable_function_name(0); cfg_get_val(config_text, "db_path", g_db_path, sizeof(g_db_path)); diff --git a/tools/chatgui/db/db_manager.cpp b/tools/chatgui/db/db_manager.cpp index 57d3ef36..d1b1d56e 100644 --- a/tools/chatgui/db/db_manager.cpp +++ b/tools/chatgui/db/db_manager.cpp @@ -258,50 +258,6 @@ MessageRow DbManager::getMessageById(const QString& chId, qint64 msgId) const { /* ── peers (UI) ── */ -int DbManager::getChannelMemberCount(const QString& chId) const { - QString sql = QStringLiteral("SELECT COUNT(*) FROM \"%1\"").arg(peersTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) return 0; - int c = 0; - if (sqlite3_step(stmt) == SQLITE_ROW) c = sqlite3_column_int(stmt, 0); - sqlite3_finalize(stmt); - return c; -} - -QList DbManager::getChannelMembers(const QString& chId, int offset, int limit) const { - QList list; - QString sql = QStringLiteral( - "SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''), COALESCE(p.local_nick,''), COALESCE(p.adm_tags,'')" - " FROM \"%1\" p LEFT JOIN nodes n ON p.node_id=n.node_id" - " ORDER BY n.online DESC, p.node_id ASC LIMIT ? OFFSET ?" - ).arg(peersTableName(chId)); - sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData()); - if (!stmt) return list; - sqlite3_bind_int(stmt, 1, limit); - sqlite3_bind_int(stmt, 2, offset); - while (sqlite3_step(stmt) == SQLITE_ROW) { - ChannelMember m; - m.nodeId = (quint64)sqlite3_column_int64(stmt, 0); - m.online = sqlite3_column_int(stmt, 1) != 0; - m.name = colText(stmt, 2); - m.localNick = colText(stmt, 3); - m.admTags = colText(stmt, 4); - m.flags = 0; - const QByteArray at = m.admTags.toUtf8(); - const char* tags = at.constData(); - if (tags && tags[0]) { - if (strstr(tags, "\"supernode\":\"yes\"")) m.flags |= 0x01; - const char* ap = strstr(tags, "\"admin\":\""); - if (ap) { ap += 8; const char* last = ap; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') ap = last; last++; } if (*ap == 'e') m.flags |= 0x02; } - const char* mp = strstr(tags, "\"moder\":\""); - if (mp) { mp += 8; const char* last = mp; while (*last && *last != '"') { if (*last == 'e' || *last == 'd') mp = last; last++; } if (*mp == 'e') m.flags |= 0x04; } - } - list.append(m); - } - sqlite3_finalize(stmt); - return list; -} - QMap DbManager::getLocalNicknames(const QString& chId) const { QMap m; if (!m_db) return m; diff --git a/tools/chatgui/db/db_manager.h b/tools/chatgui/db/db_manager.h index c63ff73b..50fcc0b3 100644 --- a/tools/chatgui/db/db_manager.h +++ b/tools/chatgui/db/db_manager.h @@ -49,15 +49,6 @@ struct AccountRow { bool isContact = true; }; -struct ChannelMember { - quint64 nodeId = 0; - bool online = false; - QString name; // from nodes.name, empty if not set - QString localNick; // from peers.local_nick override, empty if not set - QString admTags; // raw JSON {supernode, admin, moder, ver, ...} - uint8_t flags = 0; // bitmask: MEMBER_FLAG_SUPERNODE/ADMIN/MODER -}; - struct NodeAddr { quint64 nodeId = 0; int family = 0; @@ -103,8 +94,6 @@ public: MessageRow getMessageById(const QString& chId, qint64 msgId) const; /* ── Участники канала (UI) ── */ - int getChannelMemberCount(const QString& chId) const; - QList getChannelMembers(const QString& chId, int offset, int limit) const; void cleanOrphanedPeers(const QString& chId) const; QString getDisplayName(quint64 nodeId) const; QMap getLocalNicknames(const QString& chId) const; diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 3061627a..f80c07ce 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -4,6 +4,7 @@ #include "memberpropsdialog.h" #include "renamedialog.h" #include "../db/db_manager.h" +#include "../transport/gui_bridge.h" #include #include #include @@ -11,15 +12,23 @@ #include #include #include +#include #include "../../../lib/platform_compat.h" extern "C" { - #include "utun_instance.h" - #include "etcp.h" - #include "etcp_connections.h" #include "../lib/socket_compat.h" - #include "../lib/ll_queue.h" - #include "stcp_link.h" + #include "../lib/debug_config.h" + #include "chat/chat_core.h" + #include "../../lib/mem.h" +} + +static AccountList* g_detailTarget = nullptr; + +static void memberDetailCb(const uint8_t* data, int len) { + if (!g_detailTarget) return; + if (len < 13) return; + uint64_t nodeId; memcpy(&nodeId, data, 8); + g_detailTarget->onMemberDetailData(nodeId, data, len); } QString AccountList::addrStr(const void* addr, int family) { @@ -51,7 +60,7 @@ QString AccountList::relativeTimeStr(qint64 timestamp) { AccountList::AccountList(DbManager* db, QWidget *parent) : QWidget(parent) , m_listView(new QListView(this)) - , m_model(new MemberListModel(db, this)) + , m_model(new MemberListModel(this)) , m_db(db) , m_detailText(nullptr) { @@ -93,10 +102,6 @@ AccountList::AccountList(DbManager* db, QWidget *parent) connect(m_listView, &QListView::clicked, this, &AccountList::onMemberClicked); connect(m_listView, &QListView::doubleClicked, this, &AccountList::onMemberDoubleClicked); - m_refreshTimer = new QTimer(this); - m_refreshTimer->start(2000); - connect(m_refreshTimer, &QTimer::timeout, m_model, &MemberListModel::refreshRtt); - connect(m_listView, &QWidget::customContextMenuRequested, this, &AccountList::onMemberContextMenu); @@ -107,8 +112,15 @@ AccountList::AccountList(DbManager* db, QWidget *parent) m_linkTimer = new QTimer(this); m_linkTimer->setInterval(1000); connect(m_linkTimer, &QTimer::timeout, this, [this]{ - if (m_detailNodeId != 0) updateDetailDisplay(); + if (m_detailNodeId != 0) requestMemberDetail(); }); + + g_detailTarget = this; + gui_bridge_set_member_detail_cb(memberDetailCb); +} + +AccountList::~AccountList() { + if (g_detailTarget == this) g_detailTarget = nullptr; } void AccountList::setChannel(const QString& channelId) { @@ -139,6 +151,12 @@ void AccountList::showMemberDetail(quint64 nodeId) { m_detailLastSeen = 0; m_detailCreated = 0; m_detailAddrs.clear(); + m_snapConnPresent = false; + m_snapConnUp = false; + m_snapConnInit = false; + m_snapOwnTcpActive = 0; + m_snapLinks.clear(); + m_snapSocks.clear(); sqlite3* d = m_db->m_db; sqlite3_stmt* st = nullptr; @@ -193,31 +211,6 @@ void AccountList::showMemberDetail(quint64 nodeId) { sqlite3_finalize(na); } - /* resolve interface names from live sockets and sort */ - if (m_inst) { - for (auto& a : m_detailAddrs) { - if (a.protocol == 1) { - for (struct ETCP_SOCKET* s = m_inst->etcp_sockets; s; s = s->next) { - if ((int)s->sock_id == a.socket_id) { - QString name = QString::fromLatin1(s->name); - int idx = name.lastIndexOf("_v"); - if (idx > 3) a.ifname = name.mid(3, idx - 3); - break; - } - } - } else if (a.protocol == 2) { - for (struct TCP_SOCKET* ts = m_inst->tcp_sockets; ts; ts = ts->next) { - if ((int)ts->sock_id == a.socket_id) { - QString name = QString::fromLatin1(ts->name); - int idx = name.lastIndexOf("_v"); - if (idx > 3) a.ifname = name.mid(3, idx - 3); - break; - } - } - } - } - } - std::sort(m_detailAddrs.begin(), m_detailAddrs.end(), [](const DetailAddress& a, const DetailAddress& b) { if (a.ifname != b.ifname) return a.ifname < b.ifname; @@ -229,6 +222,78 @@ void AccountList::showMemberDetail(quint64 nodeId) { m_detailTimer->start(); m_linkTimer->start(); updateDetailDisplay(); + requestMemberDetail(); +} + +void AccountList::requestMemberDetail() { + if (m_detailNodeId == 0) return; + void* arg = u_malloc(sizeof(uint64_t)); + if (!arg) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "AccountList: req alloc failed"); return; } + uint64_t nid = m_detailNodeId; + memcpy(arg, &nid, sizeof(nid)); + gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, arg); +} + +void AccountList::onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len) { + if (nodeId != m_detailNodeId) return; /* stale response from earlier selection */ + if (len < 13) return; + const uint8_t* p = data; + /* skip node_id(8) */ + p += 8; + uint8_t flags = *p++; + m_snapConnPresent = (flags & 1) != 0; + m_snapConnUp = (flags & 2) != 0; + m_snapConnInit = (flags & 4) != 0; + uint16_t ownTcpActive; + memcpy(&ownTcpActive, p, 2); p += 2; + m_snapOwnTcpActive = ownTcpActive; + + uint8_t linkCount = *p++; + m_snapLinks.clear(); + for (int i = 0; i < linkCount; i++) { + if (p + MEMBER_DETAIL_LINK_SIZE > data + len) break; + MemberLinkSnap lk; + lk.isTcp = p[0] != 0; + lk.linkState = p[1]; + lk.linkStatus = p[2]; + lk.tcpReady = p[3] != 0; + lk.family = p[4]; + int addrLen = p[5]; + lk.addr = QByteArray((const char*)p + 6, qMin(addrLen, 16)); + memcpy(&lk.port, p + 22, 2); + p += MEMBER_DETAIL_LINK_SIZE; + m_snapLinks.append(lk); + } + + if (p >= data + len) { updateDetailDisplay(); return; } + uint8_t sockCount = *p++; + m_snapSocks.clear(); + for (int i = 0; i < sockCount; i++) { + if (p + MEMBER_DETAIL_SOCK_SIZE > data + len) break; + MemberSockSnap s; + memcpy(&s.sockId, p, 4); + memcpy(&s.port, p + 4, 2); + memcpy(&s.linkCount, p + 6, 4); + s.ifname = QString::fromUtf8((const char*)p + 10).trimmed(); + p += MEMBER_DETAIL_SOCK_SIZE; + m_snapSocks.append(s); + } + + /* resolve interface names from snapshot sockets (by socket_id) */ + for (auto& a : m_detailAddrs) { + for (const auto& s : m_snapSocks) { + if (s.sockId == a.socket_id) { a.ifname = s.ifname; break; } + } + } + std::sort(m_detailAddrs.begin(), m_detailAddrs.end(), + [](const DetailAddress& a, const DetailAddress& b) { + if (a.ifname != b.ifname) return a.ifname < b.ifname; + if (a.addr != b.addr) return a.addr < b.addr; + if (a.protocol != b.protocol) return a.protocol < b.protocol; + return a.port < b.port; + }); + + updateDetailDisplay(); } void AccountList::updateDetailDisplay() { @@ -246,83 +311,36 @@ void AccountList::updateDetailDisplay() { if (m_detailCreated > 0) lines.append(QString("📅 %1").arg(relativeTimeStr(m_detailCreated))); - struct ETCP_CONN* conn = nullptr; - if (m_inst) conn = instance_find_conn(m_inst, m_detailNodeId); - bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false; - QSet matchedLinks; - - /* Count active TCP connections for own node (shared across all TCP sockets) */ - int ownTcpActiveLinks = 0; - if (isSelf && m_inst) { - if (m_inst->connections) { - for (struct ll_entry* e = m_inst->connections->head; e; e = e->next) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data; - if (ce && ce->conn && ce->conn->initialized) { - for (struct ETCP_LINK* lk = ce->conn->links; lk; lk = lk->next) { - if (lk->is_tcp) { ownTcpActiveLinks++; break; } - } - } - } - } - if (m_inst->tcp_connections) { - for (struct ll_entry* e = m_inst->tcp_connections->head; e; e = e->next) { - struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data; - if (te && te->etcp_conn && te->etcp_conn->initialized) ownTcpActiveLinks++; - } - } - } for (const auto& a : m_detailAddrs) { QString proto; switch (a.protocol) { case 1: proto = "UDP"; break; case 2: proto = "TCP"; break; default: proto = "UDP/TCP"; break; } QString linkColor = "#9E9E9E"; - if (conn) { - for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) { - const struct sockaddr_storage* sa = &lk->remote_addr; - int lfm = 0; QString lip; uint16_t lpo = 0; - char ipbuf[INET6_ADDRSTRLEN]; - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - inet_ntop(AF_INET, &sin->sin_addr, ipbuf, sizeof(ipbuf)); - lfm = 4; lpo = ntohs(sin->sin_port); lip = QString::fromLocal8Bit(ipbuf); - } else if (sa->ss_family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - inet_ntop(AF_INET6, &sin6->sin6_addr, ipbuf, sizeof(ipbuf)); - lfm = 6; lpo = ntohs(sin6->sin6_port); lip = QString::fromLocal8Bit(ipbuf); - } - if (lfm == a.family && lpo == (uint16_t)a.port && lip == addrStr(a.addr.constData(), a.family)) { - linkColor = (lk->link_state == 3 && lk->link_status == 1) ? "#4CAF50" : "#E53935"; - matchedLinks.insert(lk); + if (m_snapConnPresent) { + for (const auto& lk : m_snapLinks) { + if (lk.family == a.family && lk.port == a.port + && lk.addr == a.addr) { + linkColor = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935"; break; } } if (linkColor == "#9E9E9E" && a.protocol == 2) { - for (struct ETCP_LINK* tlk = conn->links; tlk; tlk = tlk->next) { - if (tlk->is_tcp && tlk->tcp_link) { - linkColor = stcp_link_is_ready(tlk->tcp_link) ? "#4CAF50" : "#E53935"; - break; - } + for (const auto& lk : m_snapLinks) { + if (lk.isTcp && lk.tcpReady) { linkColor = "#4CAF50"; break; } } } } QString prefix; - if (isSelf && m_inst) { + if (isSelf) { int linkCount = 0; if (a.protocol == 1) { - for (struct ETCP_SOCKET* sock = m_inst->etcp_sockets; sock; sock = sock->next) { - uint16_t sp = 0; - const struct sockaddr_storage* addr = sock->interface_addr.ss_family ? &sock->interface_addr : &sock->local_addr; - if (addr->ss_family == AF_INET) sp = ntohs(((const struct sockaddr_in*)addr)->sin_port); - else if (addr->ss_family == AF_INET6) sp = ntohs(((const struct sockaddr_in6*)addr)->sin6_port); - if (sp == (uint16_t)a.port) { - linkCount = sock->links_queue ? queue_entry_count(sock->links_queue) : 0; - break; - } + for (const auto& s : m_snapSocks) { + if (s.port == a.port) { linkCount = s.linkCount; break; } } } else if (a.protocol == 2) { - linkCount = ownTcpActiveLinks; + linkCount = m_snapOwnTcpActive; } if (linkCount > 0) prefix = QString("[%1] ").arg(linkCount); } @@ -338,25 +356,20 @@ void AccountList::updateDetailDisplay() { } } - /* Show ETCP links that are not in DB (non-self) */ - if (conn) { - for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) { - if (matchedLinks.contains(lk)) continue; - const struct sockaddr_storage* sa = &lk->remote_addr; - char ipbuf[INET6_ADDRSTRLEN]; QString lip; uint16_t lpo = 0; - if (sa->ss_family == AF_INET) { - struct sockaddr_in* sin = (struct sockaddr_in*)sa; - inet_ntop(AF_INET, &sin->sin_addr, ipbuf, sizeof(ipbuf)); - lpo = ntohs(sin->sin_port); lip = QString::fromLocal8Bit(ipbuf); - } else if (sa->ss_family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa; - inet_ntop(AF_INET6, &sin6->sin6_addr, ipbuf, sizeof(ipbuf)); - lpo = ntohs(sin6->sin6_port); lip = QString::fromLocal8Bit(ipbuf); - } else { continue; } - QString proto = lk->is_tcp ? "TCP" : "UDP"; - QString lc = (lk->link_state == 3 && lk->link_status == 1) ? "#4CAF50" : "#E53935"; + /* Show snapshot links that are not in DB (non-self) */ + if (m_snapConnPresent && m_snapLinks.size() > 0) { + for (int li = 0; li < m_snapLinks.size(); li++) { + const auto& lk = m_snapLinks[li]; + bool matched = false; + for (const auto& a : m_detailAddrs) { + if (lk.family == a.family && lk.port == a.port + && lk.addr == a.addr) { matched = true; break; } + } + if (matched) continue; + QString proto = lk.isTcp ? "TCP" : "UDP"; + QString lc = (lk.linkState == 3 && lk.linkStatus == 1) ? "#4CAF50" : "#E53935"; lines.append(QString("● %2:%3:%4 [link]") - .arg(lc, proto, lip).arg(lpo)); + .arg(lc, proto, addrStr(lk.addr.constData(), lk.family)).arg(lk.port)); } } @@ -364,7 +377,7 @@ void AccountList::updateDetailDisplay() { } void AccountList::onDetailTimerTick() { - if (m_detailNodeId != 0) updateDetailDisplay(); + if (m_detailNodeId != 0) requestMemberDetail(); } void AccountList::onMemberDoubleClicked(const QModelIndex& index) { @@ -372,7 +385,8 @@ void AccountList::onMemberDoubleClicked(const QModelIndex& index) { quint64 nodeId = m_model->nodeIdAt(index.row()); if (!nodeId) return; QString name = m_model->data(index, Qt::DisplayRole).toString(); - MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this); + uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF); + MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this); if (dlg.exec() == QDialog::Accepted) { m_model->refresh(); emit memberRenamed(); @@ -404,9 +418,22 @@ void AccountList::onMemberContextMenu(const QPoint& pos) { } if (chosen != editAction) return; - MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, this); + uint8_t flags = (uint8_t)(m_model->data(index, Qt::UserRole + 3).toInt() & 0xFF); + MemberPropsDialog dlg(m_db, m_channelId, nodeId, name, flags, this); if (dlg.exec() == QDialog::Accepted) { m_model->refresh(); emit memberRenamed(); } } + +void AccountList::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) { + m_model->onMemberList(chId, count, data, dataLen); +} + +void AccountList::onMemberUpdated(const QString& chId, const uint8_t* data) { + m_model->onMemberUpdated(chId, data); +} + +void AccountList::onMemberRemoved(const QString& chId, uint64_t nodeId) { + m_model->onMemberRemoved(chId, nodeId); +} diff --git a/tools/chatgui/src/accountlist.h b/tools/chatgui/src/accountlist.h index 95334edd..57a10c79 100644 --- a/tools/chatgui/src/accountlist.h +++ b/tools/chatgui/src/accountlist.h @@ -7,10 +7,10 @@ #include #include #include +#include class MemberListModel; class DbManager; -struct UTUN_INSTANCE; struct DetailAddress { int family; @@ -21,14 +21,39 @@ struct DetailAddress { QString ifname; }; +struct MemberLinkSnap { + bool isTcp; + int linkState; + int linkStatus; + bool tcpReady; + int family; + QByteArray addr; + int port; +}; + +struct MemberSockSnap { + int sockId; + int port; + int linkCount; + QString ifname; +}; + class AccountList : public QWidget { Q_OBJECT public: explicit AccountList(DbManager* db = nullptr, QWidget *parent = nullptr); + ~AccountList(); void setChannel(const QString& channelId); void refresh(); - void setUtunInstance(struct UTUN_INSTANCE* inst) { m_inst = inst; } + void onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len); + + /* Bridge callbacks (called from GUI thread) */ + void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen); + void onMemberUpdated(const QString& chId, const uint8_t* data); + void onMemberRemoved(const QString& chId, uint64_t nodeId); + + MemberListModel* model() const { return m_model; } signals: void memberRenamed(); @@ -42,6 +67,7 @@ private slots: private: void showMemberDetail(quint64 nodeId); void updateDetailDisplay(); + void requestMemberDetail(); static QString relativeTimeStr(qint64 timestamp); static QString addrStr(const void* addr, int family); @@ -49,17 +75,21 @@ private: MemberListModel *m_model; DbManager *m_db; QTextEdit *m_detailText; - QTimer *m_refreshTimer; QTimer *m_detailTimer; QTimer *m_linkTimer; QString m_channelId; - struct UTUN_INSTANCE* m_inst = nullptr; - quint64 m_detailNodeId = 0; QString m_detailName; bool m_detailOnline = false; qint64 m_detailLastSeen = 0; qint64 m_detailCreated = 0; QVector m_detailAddrs; + + bool m_snapConnPresent = false; + bool m_snapConnUp = false; + bool m_snapConnInit = false; + int m_snapOwnTcpActive = 0; + QVector m_snapLinks; + QVector m_snapSocks; }; diff --git a/tools/chatgui/src/channeldelegate.cpp b/tools/chatgui/src/channeldelegate.cpp index d2426ddd..57121a78 100644 --- a/tools/chatgui/src/channeldelegate.cpp +++ b/tools/chatgui/src/channeldelegate.cpp @@ -8,21 +8,21 @@ ChannelDelegate::ChannelDelegate(QObject *parent) QRect ChannelDelegate::inviteButtonRect(const QRect &itemRect) { const int margin = 6; return QRect(itemRect.right() - margin - InviteBtnSize, - itemRect.bottom() - margin - InviteBtnSize, + itemRect.top() + margin, InviteBtnSize, InviteBtnSize); } QRect ChannelDelegate::inviteByLinkButtonRect(const QRect &itemRect) { const int margin = 6; return QRect(itemRect.right() - margin - InviteBtnSize * 2 - InviteBtnGap, - itemRect.bottom() - margin - InviteBtnSize, + itemRect.top() + margin, InviteBtnSize, InviteBtnSize); } QRect ChannelDelegate::settingsButtonRect(const QRect &itemRect) { const int margin = 6; return QRect(itemRect.right() - margin - InviteBtnSize * 3 - InviteBtnGap * 2, - itemRect.bottom() - margin - InviteBtnSize, + itemRect.top() + margin, InviteBtnSize, InviteBtnSize); } @@ -99,6 +99,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio QRect nameRect = textRect; nameRect.setHeight(QFontMetrics(nameFont).height()); + nameRect.setRight(nameRect.right() - InviteBtnSize * 3 - InviteBtnGap * 2 - 4); painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignTop, painter->fontMetrics().elidedText(name, Qt::ElideRight, nameRect.width())); @@ -113,7 +114,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio if (option.state & QStyle::State_Selected) nickColor = nickColor.lighter(140); QRect msgRect(textRect.left(), nameRect.bottom() + 4, - textRect.right() - InviteBtnSize - 6 - textRect.left(), + textRect.width(), option.rect.bottom() - nameRect.bottom() - margin); QFontMetrics msgFm(msgFont); @@ -163,21 +164,15 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio painter->drawText(badgeRect, Qt::AlignCenter, unreadText); } - /* ── invite plus button (bottom-right) ── */ + /* ── invite plus button (top-right) ── */ QRect plusRect = inviteButtonRect(option.rect); bool plusHovered = index.data(ChannelInviteBtnHoveredRole).toInt() != 0; painter->setRenderHint(QPainter::Antialiasing); - if (plusHovered) { - painter->setBrush(QColor(0x33, 0x90, 0xEC)); - painter->setPen(Qt::NoPen); - painter->drawEllipse(plusRect); - painter->setPen(QPen(Qt::white, 2)); - } else { - painter->setBrush(QColor(0x33, 0x90, 0xEC, 110)); - painter->setPen(Qt::NoPen); - painter->drawEllipse(plusRect); - painter->setPen(QPen(QColor(0x33, 0x90, 0xEC), 2)); - } + QColor plusFill = plusHovered ? QColor(0x4A, 0xA4, 0xF0) : QColor(0x33, 0x90, 0xEC); + painter->setBrush(plusFill); + painter->setPen(Qt::NoPen); + painter->drawEllipse(plusRect); + painter->setPen(QPen(Qt::white, 2)); painter->drawLine(plusRect.center().x(), plusRect.top() + 5, plusRect.center().x(), plusRect.bottom() - 5); painter->drawLine(plusRect.left() + 5, plusRect.center().y(), @@ -187,18 +182,12 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio QRect invRect = inviteByLinkButtonRect(option.rect); bool invHovered = index.data(ChannelInviteByLinkBtnHoveredRole).toInt() != 0; painter->setRenderHint(QPainter::Antialiasing); - if (invHovered) { - painter->setBrush(QColor(0xE6, 0x7E, 0x22)); - painter->setPen(Qt::NoPen); - painter->drawRoundedRect(invRect, 3, 3); - painter->setPen(QPen(Qt::white, 1.5)); - } else { - painter->setBrush(QColor(0xE6, 0x7E, 0x22, 110)); - painter->setPen(Qt::NoPen); - painter->drawRoundedRect(invRect, 3, 3); - painter->setPen(QPen(QColor(0xE6, 0x7E, 0x22), 1.5)); - } - /* arrow pointing into a box: →| or a simple arrow-left: ← */ + QColor invFill = invHovered ? QColor(0xF0, 0x90, 0x30) : QColor(0xE6, 0x7E, 0x22); + painter->setBrush(invFill); + painter->setPen(Qt::NoPen); + painter->drawRoundedRect(invRect, 3, 3); + painter->setPen(QPen(Qt::white, 1.5)); + /* arrow: ← */ int cx = invRect.center().x(), cy = invRect.center().y(); int dx = 3; painter->drawLine(cx + dx, cy - 3, cx - dx, cy); @@ -209,7 +198,7 @@ void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio /* ── settings gear (left of invite-by-link button) ── */ QRect gearRect = settingsButtonRect(option.rect); bool gearHovered = index.data(ChannelSettingsBtnHoveredRole).toInt() != 0; - QColor gearColor = gearHovered ? QColor("#3390EC") : QColor("#888"); + QColor gearColor = gearHovered ? QColor("#3390EC") : QColor("#555"); painter->setPen(QPen(gearColor, 2)); painter->setBrush(Qt::NoBrush); int gr = 5; diff --git a/tools/chatgui/src/channellist.cpp b/tools/chatgui/src/channellist.cpp index 4924ebdf..cfd705af 100644 --- a/tools/chatgui/src/channellist.cpp +++ b/tools/chatgui/src/channellist.cpp @@ -149,8 +149,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) gearBtn->setFixedSize(28, 28); gearBtn->setToolTip(QString::fromUtf8("Настройки")); gearBtn->setStyleSheet( - "QPushButton { border: none; font-size: 18px; background: transparent; color: gray; }" - "QPushButton:hover { color: palette(text); }"); + "QPushButton { border: none; font-size: 18px; background: transparent; color: #666; }" + "QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }"); fl->addWidget(gearBtn); connect(gearBtn, &QPushButton::clicked, this, [this] { emit settingsRequested(); }); @@ -159,8 +159,8 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) monitorBtn->setFixedSize(28, 28); monitorBtn->setToolTip(QString::fromUtf8("Connection Monitor")); monitorBtn->setStyleSheet( - "QPushButton { border: none; font-size: 16px; background: transparent; color: gray; }" - "QPushButton:hover { color: palette(text); }"); + "QPushButton { border: none; font-size: 16px; background: transparent; color: #666; }" + "QPushButton:hover { color: #3390EC; background: rgba(51,144,236,20); border-radius: 14px; }"); fl->addWidget(monitorBtn); connect(monitorBtn, &QPushButton::clicked, this, [this] { emit connMonitorRequested(); }); @@ -170,9 +170,9 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) createBtn->setFixedSize(36, 28); createBtn->setToolTip(QString::fromUtf8("Создать группу")); 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; }"); + "QPushButton { border: none; border-radius: 6px; font-size: 11px; font-weight: bold;" + " color: white; background: #27AE60; }" + "QPushButton:hover { background: #219A52; }"); connect(createBtn, &QPushButton::clicked, this, [this] { emit createGroupRequested(); }); fl->addWidget(createBtn); @@ -180,9 +180,9 @@ ChannelList::ChannelList(DbManager* db, QWidget *parent) plusBtn->setFixedSize(28, 28); plusBtn->setToolTip(QString::fromUtf8("Присоединиться")); plusBtn->setStyleSheet( - "QPushButton { border: 1.5px solid #3390EC; border-radius: 14px;" - " font-size: 18px; font-weight: bold; color: #3390EC; background: transparent; }" - "QPushButton:hover { background: #3390EC; color: white; }"); + "QPushButton { border: none; border-radius: 14px; font-size: 18px; font-weight: bold;" + " color: white; background: #3390EC; }" + "QPushButton:hover { background: #2B7EC8; }"); connect(plusBtn, &QPushButton::clicked, this, [this] { emit joinRequested(); }); fl->addWidget(plusBtn); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index bdea7632..059d5202 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -60,10 +60,30 @@ static void onChannelUpdatedCallback(const char* ch_id, int ch_id_len) { s_mainWindow->reloadChannels(); } -static void onMembersChangedCallback(const char* ch_id, int ch_id_len) { - Q_UNUSED(ch_id_len); +static void onMemberListCallback(const uint8_t* data, int len) { + if (len < 3) return; + uint8_t cl = data[0]; + if (len < 1 + (int)cl + 2) return; + uint16_t count; memcpy(&count, data + 1 + cl, 2); + if (s_mainWindow) + s_mainWindow->onMemberList(QString::fromUtf8((const char*)data + 1, cl), count, data + 1 + cl + 2, len - 1 - cl - 2); +} + +static void onMemberUpdatedCallback(const uint8_t* data, int len) { + if (len < 1 + 64 + 1 + 79) return; + uint8_t cl = data[0]; + if (len < 1 + (int)cl + 1 + 79) return; if (s_mainWindow) - s_mainWindow->onMembersChanged(ch_id); + s_mainWindow->onMemberUpdated(QString::fromUtf8((const char*)data + 1, cl), data + 1 + cl + 1); +} + +static void onMemberRemovedCallback(const uint8_t* data, int len) { + if (len < 1 + 64 + 8) return; + uint8_t cl = data[0]; + if (len < 1 + (int)cl + 8) return; + uint64_t nid; memcpy(&nid, data + 1 + cl, 8); + if (s_mainWindow) + s_mainWindow->onMemberRemoved(QString::fromUtf8((const char*)data + 1, cl), nid); } static void onAutoConnectStatusCallback(uint8_t status, uint16_t total_tried, @@ -109,7 +129,9 @@ MainWindow::~MainWindow() { m_autoPlayTimer->stop(); gui_bridge_set_msg_received_cb(nullptr); gui_bridge_set_channel_updated_cb(nullptr); - gui_bridge_set_members_changed_cb(nullptr); + gui_bridge_set_member_list_cb(nullptr); + gui_bridge_set_member_updated_cb(nullptr); + gui_bridge_set_member_removed_cb(nullptr); gui_bridge_set_my_node_id_cb(nullptr); gui_bridge_set_auto_connect_status_cb(nullptr); gui_bridge_set_channel_peers_online_cb(nullptr); @@ -157,11 +179,9 @@ void MainWindow::setupNode() { }); connect(m_node, &UtunNode::started, this, [this]() { qDebug("UtunNode started"); - if (m_accountList && m_node) m_accountList->setUtunInstance(m_node->instance()); }); connect(m_node, &UtunNode::stopped, this, [this]() { qDebug("UtunNode stopped"); - if (m_node) m_node->finalize(); qApp->quit(); }); @@ -260,7 +280,9 @@ void MainWindow::setupBridgeCallbacks() { s_mainWindow = this; gui_bridge_set_msg_received_cb(onMsgReceivedCallback); gui_bridge_set_channel_updated_cb(onChannelUpdatedCallback); - gui_bridge_set_members_changed_cb(onMembersChangedCallback); + gui_bridge_set_member_list_cb(onMemberListCallback); + gui_bridge_set_member_updated_cb(onMemberUpdatedCallback); + gui_bridge_set_member_removed_cb(onMemberRemovedCallback); gui_bridge_set_my_node_id_cb([](uint64_t nid) { qDebug("MainWindow: MY_NODE_ID setMyNodeId(%016llx)", (unsigned long long)nid); if (s_mainWindow && s_mainWindow->m_db) s_mainWindow->m_db->setMyNodeId((quint64)nid); @@ -463,10 +485,16 @@ void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t au } } -void MainWindow::onMembersChanged(const char* ch_id) { - QString cur = QSettings().value("last_channel_id").toString(); - if (cur == QString::fromUtf8(ch_id)) - m_accountList->refresh(); +void MainWindow::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) { + m_accountList->onMemberList(chId, count, data, dataLen); +} + +void MainWindow::onMemberUpdated(const QString& chId, const uint8_t* data) { + m_accountList->onMemberUpdated(chId, data); +} + +void MainWindow::onMemberRemoved(const QString& chId, uint64_t nodeId) { + m_accountList->onMemberRemoved(chId, nodeId); } void MainWindow::onAutoConnectStatus(uint8_t status, uint16_t /*total_tried*/, diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index 47d671bf..2bb801aa 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -48,7 +48,9 @@ private slots: public: void onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author); - void onMembersChanged(const char* ch_id); + void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen); + void onMemberUpdated(const QString& chId, const uint8_t* data); + void onMemberRemoved(const QString& chId, uint64_t nodeId); void onAutoConnectStatus(uint8_t status, uint16_t total, uint16_t totalNodes, uint16_t connected); void onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t online); void reloadChannels(); diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp index 718cda93..2ed9cf2c 100644 --- a/tools/chatgui/src/memberlistmodel.cpp +++ b/tools/chatgui/src/memberlistmodel.cpp @@ -1,11 +1,26 @@ #include "memberlistmodel.h" -#include "../db/db_manager.h" +#include "../transport/gui_bridge.h" #include #include -#include +#include +#include -MemberListModel::MemberListModel(DbManager* db, QObject* parent) - : QAbstractListModel(parent), m_db(db) {} +extern "C" { +#include "../../../src/chat/chat_core.h" +#include "../../../src/chat/chat_member.h" +#include "../../../lib/mem.h" +} + +#define CHAT_MEMBER_DISPLAY_SIZE 79 + +MemberListModel::MemberListModel(QObject* parent) + : QAbstractListModel(parent) + , m_rttTimer(new QTimer(this)) +{ + m_rttTimer->setInterval(2000); + connect(m_rttTimer, &QTimer::timeout, this, &MemberListModel::refreshRtt); + m_rttTimer->start(); +} int MemberListModel::rowCount(const QModelIndex& parent) const { (void)parent; @@ -18,7 +33,7 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { auto it = m_cache.find(index.row()); if (it != m_cache.end()) { - if (role == Qt::DisplayRole) return it->localNick.isEmpty() ? it->name : it->localNick; + if (role == Qt::DisplayRole) return it->name; if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar)); if (role == Qt::UserRole + 1) { QString sub; @@ -38,168 +53,118 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const { if (role == Qt::UserRole + 3) return it->flags; return {}; } - - if (role == Qt::DisplayRole) { - scheduleBatch(index.row()); - return QStringLiteral("\u2026"); - } return {}; } void MemberListModel::setChannel(const QString& channelId) { beginResetModel(); m_channelId = channelId; - m_db->cleanOrphanedPeers(channelId); - m_totalCount = m_db ? m_db->getChannelMemberCount(channelId) : 0; + m_totalCount = 0; m_cache.clear(); - m_pendingFrom = -1; endResetModel(); - if (m_totalCount > 0) scheduleBatch(0); + + if (!channelId.isEmpty()) { + char* ch = u_strdup(channelId.toUtf8().constData()); + gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, ch); + } +} + +void MemberListModel::refresh() { + if (!m_channelId.isEmpty()) setChannel(m_channelId); } -void MemberListModel::refresh() { if (!m_channelId.isEmpty()) setChannel(m_channelId); } +void MemberListModel::refreshRtt() { + if (m_channelId.isEmpty()) return; + char* ch = u_strdup(m_channelId.toUtf8().constData()); + gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, ch); +} quint64 MemberListModel::nodeIdAt(int row) const { auto it = m_cache.find(row); return it != m_cache.end() ? it->nodeId : 0; } -void MemberListModel::scheduleBatch(int centerRow) const { - if (m_pendingFrom >= 0) return; - int from = centerRow - BATCH / 2; - if (from < 0) from = 0; - int maxFrom = m_totalCount - BATCH; - if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0; - const_cast(this)->m_pendingFrom = from; - const_cast(this)->m_pendingCount = BATCH; - QTimer::singleShot(0, const_cast(this), &MemberListModel::loadPendingBatch); +static void deserialize_member(const uint8_t* src, MemberListItem& item) { + memcpy(&item.nodeId, src, 8); + uint8_t online = src[8]; + item.isSelf = src[9] != 0; + item.flags = src[10]; + uint16_t rtt; memcpy(&rtt, src + 11, 2); + item.rtt = (rtt == 0xFFFF) ? -1 : (int)rtt; + int nat = (int)src[13]; + item.natType = (nat == 0xFF) ? -1 : nat; + item.online = (online != 0); + + char name[64]; + memcpy(name, src + 15, 64); name[63] = '\0'; + item.name = QString::fromUtf8(name); + + QColor c = item.online ? QColor("#4CAF50") : QColor("#9E9E9E"); + QPixmap pm(24, 24); pm.fill(Qt::transparent); + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing); + p.setBrush(c); + p.setPen(Qt::NoPen); + p.drawEllipse(2, 2, 20, 20); + p.setPen(Qt::white); + QFont f; f.setBold(true); f.setPixelSize(12); + p.setFont(f); + p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, item.name.left(1)); + p.end(); + item.avatar = pm; } -void MemberListModel::loadPendingBatch() { - if (m_pendingFrom < 0) return; - int from = m_pendingFrom, count = m_pendingCount; - m_pendingFrom = -1; - loadBatch(from, count); -} +void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) { + (void)dataLen; + if (chId != m_channelId) return; -void MemberListModel::loadBatch(int from, int count) { - if (!m_db || m_channelId.isEmpty()) return; - int maxFrom = m_totalCount - count; - if (from > maxFrom) from = maxFrom > 0 ? maxFrom : 0; - - auto members = m_db->getChannelMembers(m_channelId, from, count); - for (int i = 0; i < members.size(); i++) { - int row = from + i; - MemberListItem& item = m_cache[row]; - item.nodeId = members[i].nodeId; - item.isSelf = (item.nodeId == m_db->myNodeId()); - item.online = members[i].online; - item.flags = members[i].flags; - QString shortId = QString("%1").arg(members[i].nodeId, 8, 16, QChar('0')).right(4).toUpper(); - item.name = members[i].name.isEmpty() ? shortId : members[i].name; - item.localNick = members[i].localNick; - QColor c = members[i].online ? QColor("#4CAF50") : QColor("#9E9E9E"); - item.avatar = makeAvatar(c, item.name[0]); - } + beginResetModel(); + m_totalCount = count; + m_cache.clear(); - // batch-загрузка RTT и NAT типа из node_addresses - QStringList idList; - for (int i = 0; i < members.size(); i++) - idList.append(QString::number(members[i].nodeId)); - QString ids = idList.isEmpty() ? QString("0") : idList.join(","); - - QByteArray sql = QString( - "SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses" - " WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id" - ).arg(ids).toUtf8(); - - sqlite3_stmt* na = nullptr; - if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) == SQLITE_OK) { - while (sqlite3_step(na) == SQLITE_ROW) { - quint64 nid = (quint64)sqlite3_column_int64(na, 0); - int rttVal = sqlite3_column_int(na, 1); - int at = sqlite3_column_int(na, 2); - for (int i = 0; i < members.size(); i++) { - if (members[i].nodeId == nid) { - int row = from + i; - auto it = m_cache.find(row); - if (it != m_cache.end()) { - it->rtt = rttVal; - it->natType = at; - } - break; - } - } - } - sqlite3_finalize(na); + for (int i = 0; i < count; i++) { + MemberListItem item; + deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item); + m_cache[i] = item; } + endResetModel(); +} - evictDistant(from + BATCH / 2); +void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) { + if (chId != m_channelId) return; - int last = from + (int)members.size() - 1; - if (last >= from) emit dataChanged(index(from), index(last)); -} + MemberListItem item; + deserialize_member(data, item); -void MemberListModel::refreshRtt() { - if (m_cache.isEmpty() || !m_db || !m_db->m_db) return; - - QStringList idList; - for (auto it = m_cache.begin(); it != m_cache.end(); ++it) - idList.append(QString::number(it->nodeId)); - if (idList.isEmpty()) return; - QString ids = idList.join(","); - - QByteArray sql = QString( - "SELECT node_id, MIN(rtt), MIN(addr_type) FROM node_addresses" - " WHERE node_id IN (%1) AND rtt > 0 GROUP BY node_id" - ).arg(ids).toUtf8(); - - sqlite3_stmt* na = nullptr; - if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &na, nullptr) != SQLITE_OK) return; - - QList changed; - while (sqlite3_step(na) == SQLITE_ROW) { - quint64 nid = (quint64)sqlite3_column_int64(na, 0); - int rttVal = sqlite3_column_int(na, 1); - int at = sqlite3_column_int(na, 2); - for (auto it = m_cache.begin(); it != m_cache.end(); ++it) { - if (it->nodeId == nid) { - if (it->rtt != rttVal || it->natType != at) { - it->rtt = rttVal; - it->natType = at; - changed.append(it.key()); - } - break; - } - } + /* find existing row by nodeId */ + int row = -1; + for (auto it = m_cache.begin(); it != m_cache.end(); ++it) { + if (it->nodeId == item.nodeId) { row = it.key(); break; } } - sqlite3_finalize(na); - - for (int row : changed) emit dataChanged(index(row), index(row), {Qt::UserRole + 1}); -} -void MemberListModel::evictDistant(int centerRow) { - if (m_cache.size() <= MAX_CACHE) return; - int half = MAX_CACHE / 2; - QList toRemove; - for (auto it = m_cache.begin(); it != m_cache.end(); ++it) - if (qAbs(it.key() - centerRow) > half) - toRemove.append(it.key()); - for (int k : toRemove) m_cache.remove(k); + if (row >= 0) { + m_cache[row] = item; + emit dataChanged(index(row), index(row)); + } else { + /* new member — add at end */ + row = m_totalCount++; + m_cache[row] = item; + beginInsertRows(QModelIndex(), row, row); + endInsertRows(); + } } -QPixmap MemberListModel::makeAvatar(QColor color, QChar letter) const { - QPixmap pm(24, 24); - pm.fill(Qt::transparent); - QPainter p(&pm); - p.setRenderHint(QPainter::Antialiasing); - p.setBrush(color); - p.setPen(Qt::NoPen); - p.drawEllipse(2, 2, 20, 20); - p.setPen(Qt::white); - QFont f; f.setBold(true); f.setPixelSize(12); - p.setFont(f); - p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, QString(letter)); - p.end(); - return pm; +void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) { + if (chId != m_channelId) return; + + for (auto it = m_cache.begin(); it != m_cache.end(); ++it) { + if (it->nodeId == nodeId) { + int row = it.key(); + beginRemoveRows(QModelIndex(), row, row); + m_cache.remove(row); + m_totalCount--; + endRemoveRows(); + return; + } + } } diff --git a/tools/chatgui/src/memberlistmodel.h b/tools/chatgui/src/memberlistmodel.h index 92248a49..6682e211 100644 --- a/tools/chatgui/src/memberlistmodel.h +++ b/tools/chatgui/src/memberlistmodel.h @@ -7,8 +7,8 @@ #include #include #include - -class DbManager; +#include +#include struct MemberListItem { quint64 nodeId = 0; @@ -16,7 +16,6 @@ struct MemberListItem { bool isSelf = false; QPixmap avatar; QString name; - QString localNick; int rtt = -1; int natType = -1; uint8_t flags = 0; @@ -25,10 +24,7 @@ struct MemberListItem { class MemberListModel : public QAbstractListModel { Q_OBJECT public: - static const int BATCH = 40; - static const int MAX_CACHE = 120; - - explicit MemberListModel(DbManager* db, QObject* parent = nullptr); + explicit MemberListModel(QObject* parent = nullptr); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -38,21 +34,19 @@ public: void refreshRtt(); quint64 nodeIdAt(int row) const; -private slots: - void loadPendingBatch(); + /* Called from bridge callbacks (GUI thread) */ + void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen); + void onMemberUpdated(const QString& chId, const uint8_t* data); + void onMemberRemoved(const QString& chId, uint64_t nodeId); private: - void scheduleBatch(int centerRow) const; void loadBatch(int from, int count); - void evictDistant(int centerRow); QPixmap makeAvatar(QColor color, QChar letter) const; - DbManager* m_db; - QString m_channelId; - int m_totalCount = 0; + QString m_channelId; + int m_totalCount = 0; mutable QMap m_cache; - mutable int m_pendingFrom = -1; - mutable int m_pendingCount = 0; + QTimer* m_rttTimer; }; #endif diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp index f51f7ea3..6e7442af 100644 --- a/tools/chatgui/src/memberpropsdialog.cpp +++ b/tools/chatgui/src/memberpropsdialog.cpp @@ -1,26 +1,33 @@ #include "memberpropsdialog.h" #include "../db/db_manager.h" +#include "../transport/gui_bridge.h" #include #include #include #include #include +#include -#include -#include "../../../lib/platform_compat.h" +extern "C" { +#include "../../../src/chat/chat_core.h" +#include "../../../src/chat/chat_member.h" +#include "../../../lib/mem.h" +} MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, - quint64 nodeId, const QString& memberName, - QWidget* parent) + uint64_t nodeId, const QString& memberName, + uint8_t flags, QWidget* parent) : QDialog(parent) , m_db(db) , m_channelId(channelId) , m_nodeId(nodeId) + , m_flags(flags) { setWindowTitle("Member Properties"); setMinimumWidth(340); setModal(true); + /* channel private key (for enabling admin/super checkboxes) */ auto chs = m_db->getChannels(); for (const auto& ch : chs) { if (ch.channelId == channelId) { @@ -29,9 +36,12 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, } } bool haveKey = (m_edPrivkey.size() == 32); - bool isOwn = (m_nodeId == m_db->myNodeId()); + bool isOwn = (m_nodeId == (uint64_t)m_db->myNodeId()); + + bool isAdmin = (m_flags & CHAT_MEMBER_FLAG_ADMIN) != 0; + bool isSuper = (m_flags & CHAT_MEMBER_FLAG_SUPERNODE) != 0; - bool isAdmin = false, isSuper = false, isStorage = false; + /* load current nick and version from DB (local_nick is local-only, ver is in adm_tags) */ QString curNick; m_currentVer = 0; if (m_db->m_db) { @@ -45,24 +55,13 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, if (sqlite3_step(st) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(st, 0); if (tags) { - const char* vp = strstr(tags, "ver="); - if (vp) m_currentVer = atoi(vp + 4); - if (strstr(tags, "admin=yes")) isAdmin = true; - if (strstr(tags, "supernode=yes")) isSuper = true; - if (strstr(tags, "storage=yes")) isStorage = true; + const char* vp = strstr(tags, "\"ver\":\""); + if (vp) { vp += 7; m_currentVer = atoi(vp); } } curNick = DbManager::colText(st, 1); } sqlite3_finalize(st); } - QByteArray alt = QString( - "ALTER TABLE \"%1\" ADD COLUMN adm_tags TEXT" - ).arg(tbl).toUtf8(); - sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr); - alt = QString( - "ALTER TABLE \"%1\" ADD COLUMN adm_tags_sig BLOB" - ).arg(tbl).toUtf8(); - sqlite3_exec(m_db->m_db, alt.constData(), nullptr, nullptr, nullptr); } auto* layout = new QVBoxLayout(this); @@ -93,7 +92,6 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, layout->addWidget(m_superCb); m_storageCb = new QCheckBox("Storage (priority attachment keeper)", this); - m_storageCb->setChecked(isStorage); m_storageCb->setEnabled(haveKey); layout->addWidget(m_storageCb); @@ -134,10 +132,10 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, void MemberPropsDialog::onSave() { if (!m_db->m_db) return; - QString tbl = m_db->peersTableName(m_channelId); - /* local_nick — always save (unsigned, local only) */ + /* local_nick — always save local (unsigned, local only) */ QString nick = m_nickEdit->text().trimmed(); + QString tbl = m_db->peersTableName(m_channelId); QByteArray nickSql = QString( "UPDATE \"%1\" SET local_nick=? WHERE node_id=?" ).arg(tbl).toUtf8(); @@ -150,49 +148,26 @@ void MemberPropsDialog::onSave() { sqlite3_finalize(ns); } - /* signed fields — only if key available */ + /* signed fields — через chat_member_tags_commit_trampoline */ if (m_edPrivkey.size() != 32) { accept(); return; } int ver = m_currentVer + 1; - QString tags = QString("ver=%1").arg(ver); - if (m_adminCb->isChecked()) tags += ",admin=yes"; - if (m_superCb->isChecked()) tags += ",supernode=yes"; - if (m_storageCb->isChecked()) tags += ",storage=yes"; - - QByteArray msg = tags.toUtf8(); - uint64_t le = htole64(m_nodeId); - msg.append((const char*)&le, 8); - - uint8_t sig[64] = {}; - EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, - (const uint8_t*)m_edPrivkey.constData(), 32); - if (pkey) { - EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); - if (mdctx) { - size_t siglen = 64; - if (EVP_DigestSignInit(mdctx, nullptr, nullptr, nullptr, pkey) == 1) - EVP_DigestSign(mdctx, sig, &siglen, (const uint8_t*)msg.constData(), msg.size()); - EVP_MD_CTX_free(mdctx); - } - EVP_PKEY_free(pkey); - } - - QByteArray sql = QString( - "UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=?, storage=? WHERE node_id=?" - ).arg(tbl).toUtf8(); - sqlite3_stmt* st = nullptr; - if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) { - sqlite3_bind_text(st, 1, tags.isEmpty() ? nullptr : tags.toUtf8().constData(), - -1, tags.isEmpty() ? SQLITE_STATIC : SQLITE_TRANSIENT); - sqlite3_bind_blob(st, 2, sig, 64, SQLITE_TRANSIENT); - int nt = m_superCb->isChecked() ? 4 : 1; - sqlite3_bind_int(st, 3, nt); - int stg = m_storageCb->isChecked() ? 1 : 0; - sqlite3_bind_int(st, 4, stg); - sqlite3_bind_int64(st, 5, (sqlite3_int64)m_nodeId); - sqlite3_step(st); - sqlite3_finalize(st); - } - + QString json; + json += QString("{\"ver\":\"%1\"").arg(ver); + json += QString(",\"admin\":\"%1\"").arg(m_adminCb->isChecked() ? "yes" : "no"); + json += QString(",\"supernode\":\"%1\"").arg(m_superCb->isChecked() ? "yes" : "no"); + json += QString(",\"storage\":\"%1\"").arg(m_storageCb->isChecked() ? "yes" : "no"); + json += "}"; + + struct chat_member_tags_req* req = (struct chat_member_tags_req*) + u_calloc(1, sizeof(struct chat_member_tags_req)); + if (!req) { accept(); return; } + + QByteArray ch_id_utf8 = m_channelId.toUtf8(); + strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1); + req->node_id = m_nodeId; + strncpy(req->json, json.toUtf8().constData(), sizeof(req->json) - 1); + + gui_bridge_post_uasync_fn(chat_member_tags_commit_trampoline, req); accept(); } diff --git a/tools/chatgui/src/memberpropsdialog.h b/tools/chatgui/src/memberpropsdialog.h index b3dce3fe..0efd79ad 100644 --- a/tools/chatgui/src/memberpropsdialog.h +++ b/tools/chatgui/src/memberpropsdialog.h @@ -4,6 +4,7 @@ #include #include #include +#include class DbManager; @@ -11,8 +12,8 @@ class MemberPropsDialog : public QDialog { Q_OBJECT public: explicit MemberPropsDialog(DbManager* db, const QString& channelId, - quint64 nodeId, const QString& memberName, - QWidget* parent = nullptr); + uint64_t nodeId, const QString& memberName, + uint8_t flags, QWidget* parent = nullptr); private slots: void onSave(); @@ -20,8 +21,9 @@ private slots: private: DbManager* m_db; QString m_channelId; - quint64 m_nodeId; + uint64_t m_nodeId; QByteArray m_edPrivkey; + uint8_t m_flags; int m_currentVer; QLineEdit* m_nickEdit; diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index 68aa9715..6f8cb65f 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -30,6 +30,16 @@ struct TOPO_GROUP_NODE; #define GUI_EVT_NODEINFO_UPDATE 16 /* data: node_id:8 conn_presence:1 conn_up:1 best_rtt_packed:2 */ #define GUI_EVT_CONN_LIST 20 /* data: [count:2][entry:46B]* — binary conn list */ #define GUI_EVT_CONN_METRICS 21 /* data: text dump of selected connection metrics */ +#define GUI_EVT_MEMBER_DETAIL 22 /* data: [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live snapshot for member detail panel */ +#define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:79B]* — полный список мемберов канала */ +#define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:79B] — обновление одного мембера */ +#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */ + +/* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */ +#define MEMBER_DETAIL_LINK_SIZE 24 +#define MEMBER_DETAIL_SOCK_SIZE 48 +#define MEMBER_DETAIL_MAX_LINKS 16 +#define MEMBER_DETAIL_MAX_SOCKS 16 /* ── API ── */ @@ -60,10 +70,6 @@ void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb); 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); -/* Callback для изменения списка участников (вызывается из GUI-потока) */ -typedef void (*gui_members_changed_fn)(const char* ch_id, int ch_id_len); -void gui_bridge_set_members_changed_cb(gui_members_changed_fn cb); - /* Callback для получения своего node_id после инициализации uTun */ typedef void (*gui_my_node_id_fn)(uint64_t node_id); void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb); @@ -110,6 +116,26 @@ void gui_bridge_set_conn_list_cb(gui_conn_list_fn cb); typedef void (*gui_conn_metrics_fn)(const uint8_t* data, int len); void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb); +/* Callback для live-снапшота панели деталей участника (вызывается из GUI-потока) + data: [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — см. chat_status.c collect_member_detail */ +typedef void (*gui_member_detail_fn)(const uint8_t* data, int len); +void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb); + +/* Callback для получения полного списка мемберов канала (вызывается из GUI-потока) + data: [ch_id_len:1][ch_id:var][count:2][member:79B]* */ +typedef void (*gui_member_list_fn)(const uint8_t* data, int len); +void gui_bridge_set_member_list_cb(gui_member_list_fn cb); + +/* Callback для обновления одного мембера (вызывается из GUI-потока) + data: [ch_id_len:1][ch_id:var][count:1][member:79B] */ +typedef void (*gui_member_updated_fn)(const uint8_t* data, int len); +void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb); + +/* Callback для удаления мембера из канала (вызывается из GUI-потока) + data: [ch_id_len:1][ch_id:var][node_id:8] */ +typedef void (*gui_member_removed_fn)(const uint8_t* data, int len); +void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb); + /* uTun nodeinfo callback — registered via utun_add_nodeinfo_cbk, called from uasync thread */ void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg); diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index ebe3dd52..7be47d2a 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -29,7 +29,6 @@ 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 gui_members_changed_fn g_members_changed_cb = nullptr; static gui_my_node_id_fn g_my_node_id_cb = nullptr; static gui_auto_connect_status_fn g_auto_connect_status_cb = nullptr; static gui_channel_peers_online_fn g_channel_peers_online_cb = nullptr; @@ -41,6 +40,10 @@ static gui_nodeinfo_update_fn g_nodeinfo_update_cb = nullptr; static gui_invite_received_fn g_invite_received_cb = nullptr; static gui_conn_list_fn g_conn_list_cb = nullptr; static gui_conn_metrics_fn g_conn_metrics_cb = nullptr; +static gui_member_detail_fn g_member_detail_cb = nullptr; +static gui_member_list_fn g_member_list_cb = nullptr; +static gui_member_updated_fn g_member_updated_cb = nullptr; +static gui_member_removed_fn g_member_removed_cb = nullptr; static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ @@ -98,15 +101,6 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: CHANNEL_UPDATED data too short %d", dlen); } break; - case GUI_EVT_MEMBERS_CHANGED: - if (dlen >= 2) { - uint8_t chLen = d[0]; - if (dlen >= 1 + chLen) { - QString chId = QString::fromUtf8((const char*)d + 1, chLen); - if (g_members_changed_cb) g_members_changed_cb((const char*)d + 1, chLen); - } - } - break; case GUI_EVT_MY_NODE_ID: if (dlen >= 8) { uint64_t nid; memcpy(&nid, d, 8); @@ -188,6 +182,18 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { case GUI_EVT_CONN_METRICS: if (g_conn_metrics_cb) g_conn_metrics_cb(d, dlen); break; + case GUI_EVT_MEMBER_DETAIL: + if (g_member_detail_cb) g_member_detail_cb(d, dlen); + break; + case GUI_EVT_MEMBER_LIST: + if (g_member_list_cb) g_member_list_cb(d, dlen); + break; + case GUI_EVT_MEMBER_UPDATED: + if (g_member_updated_cb) g_member_updated_cb(d, dlen); + break; + case GUI_EVT_MEMBER_REMOVED: + if (g_member_removed_cb) g_member_removed_cb(d, dlen); + break; default: DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge: unknown event type %d", eventType); break; @@ -245,10 +251,6 @@ void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb) { g_channel_updated_cb = cb; } -void gui_bridge_set_members_changed_cb(gui_members_changed_fn cb) { - g_members_changed_cb = cb; -} - void gui_bridge_set_my_node_id_cb(gui_my_node_id_fn cb) { g_my_node_id_cb = cb; } @@ -293,6 +295,22 @@ void gui_bridge_set_conn_metrics_cb(gui_conn_metrics_fn cb) { g_conn_metrics_cb = cb; } +void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb) { + g_member_detail_cb = cb; +} + +void gui_bridge_set_member_list_cb(gui_member_list_fn cb) { + g_member_list_cb = cb; +} + +void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb) { + g_member_updated_cb = cb; +} + +void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) { + g_member_removed_cb = cb; +} + } /* extern "C" */ /* ── uTun nodeinfo callback (called from uasync thread) ── */ diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 5e955634..b1bd0a83 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -101,15 +101,9 @@ void UtunNode::stop() { } void UtunNode::finalize() { - if (!m_instance) return; - DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup start"); - etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, nullptr); - chat_sync_destroy(m_instance); - chat_core_destroy(m_instance); - utun_instance_destroy(m_instance); + if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; } m_instance = nullptr; - uasync_destroy(m_ua, 0); - m_ua = nullptr; + m_running = false; DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "finalize: cleanup done"); } @@ -409,6 +403,12 @@ void UtunNode::runLoop() { DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: poll exit m_stop=%d", (int)m_stop); + if (m_instance) { + DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "runLoop: destroying instance in worker thread"); + utun_instance_destroy(m_instance); + m_instance = nullptr; + } + signal(SIGINT, SIG_DFL); signal(SIGTERM, SIG_DFL);