Browse Source

etcp: переупорядочить close + защита state==2 во всех API-точках; topo: правильная очистка group nodes и registry; chat/member: новый модуль member_sync; chatgui: обновления транспорта и UI

topo_upd
evgeny 2 months ago
parent
commit
7f901c01e1
  1. 6
      lib/u_async.c
  2. 1
      lib/u_async.h
  3. 8
      src/Makefile.am
  4. 5
      src/chat/chat_core.c
  5. 3
      src/chat/chat_core.h
  6. 4
      src/chat/chat_event.c
  7. 4
      src/chat/chat_event.h
  8. 362
      src/chat/chat_member.c
  9. 70
      src/chat/chat_member.h
  10. 9
      src/chat/chat_profile.c
  11. 139
      src/chat/chat_status.c
  12. 67
      src/chat/chat_sync.c
  13. 3
      src/chat/db_sync.c
  14. 81
      src/chat/member_sync.c
  15. 4
      src/chat/member_sync.h
  16. 2
      src/chat/merkle_sync.c
  17. 12
      src/media_delivery/media_delivery.c
  18. 21
      src/routing_layer/topo_group.c
  19. 38
      src/transport_layer/etcp.c
  20. 2
      src/transport_layer/etcp_api.c
  21. 2
      src/transport_layer/etcp_connections.c
  22. 20
      src/utun_instance.c
  23. 3
      tools/chatgui-android/libutun_lite/instance_lite.c
  24. 44
      tools/chatgui/db/db_manager.cpp
  25. 11
      tools/chatgui/db/db_manager.h
  26. 257
      tools/chatgui/src/accountlist.cpp
  27. 40
      tools/chatgui/src/accountlist.h
  28. 47
      tools/chatgui/src/channeldelegate.cpp
  29. 20
      tools/chatgui/src/channellist.cpp
  30. 50
      tools/chatgui/src/mainwindow.cpp
  31. 4
      tools/chatgui/src/mainwindow.h
  32. 247
      tools/chatgui/src/memberlistmodel.cpp
  33. 26
      tools/chatgui/src/memberlistmodel.h
  34. 101
      tools/chatgui/src/memberpropsdialog.cpp
  35. 8
      tools/chatgui/src/memberpropsdialog.h
  36. 34
      tools/chatgui/transport/gui_bridge.h
  37. 46
      tools/chatgui/transport/gui_bridge_impl.cpp
  38. 16
      tools/chatgui/transport/utun_node.cpp

6
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;

1
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);

8
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 \

5
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);

3
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) */

4
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);
}

4
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);

362
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)");
}

70
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 <stdint.h>
#include <stddef.h>
#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 */

9
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);
}

139
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 <string.h>
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);
}

67
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);

3
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;
}
}

81
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,

4
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_<ch_id>). */
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).
*/

2
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);
}
}

12
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;
}
}

21
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);

38
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);

2
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);

2
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));

20
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;

3
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));

44
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<ChannelMember> DbManager::getChannelMembers(const QString& chId, int offset, int limit) const {
QList<ChannelMember> 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<quint64, QString> DbManager::getLocalNicknames(const QString& chId) const {
QMap<quint64, QString> m;
if (!m_db) return m;

11
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<ChannelMember> getChannelMembers(const QString& chId, int offset, int limit) const;
void cleanOrphanedPeers(const QString& chId) const;
QString getDisplayName(quint64 nodeId) const;
QMap<quint64, QString> getLocalNicknames(const QString& chId) const;

257
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 <QVBoxLayout>
#include <QSplitter>
#include <QLabel>
@ -11,15 +12,23 @@
#include <QFont>
#include <QSet>
#include <algorithm>
#include <cstring>
#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<struct ETCP_LINK*> 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("<span style='color:%1;'>●</span> %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);
}

40
tools/chatgui/src/accountlist.h

@ -7,10 +7,10 @@
#include <QMenu>
#include <QVector>
#include <QElapsedTimer>
#include <stdint.h>
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<DetailAddress> m_detailAddrs;
bool m_snapConnPresent = false;
bool m_snapConnUp = false;
bool m_snapConnInit = false;
int m_snapOwnTcpActive = 0;
QVector<MemberLinkSnap> m_snapLinks;
QVector<MemberSockSnap> m_snapSocks;
};

47
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;

20
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);

50
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*/,

4
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();

247
tools/chatgui/src/memberlistmodel.cpp

@ -1,11 +1,26 @@
#include "memberlistmodel.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QPainter>
#include <QFont>
#include <QTimer>
#include <QColor>
#include <cstring>
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<MemberListModel*>(this)->m_pendingFrom = from;
const_cast<MemberListModel*>(this)->m_pendingCount = BATCH;
QTimer::singleShot(0, const_cast<MemberListModel*>(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<int> 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<int> 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;
}
}
}

26
tools/chatgui/src/memberlistmodel.h

@ -7,8 +7,8 @@
#include <QString>
#include <QIcon>
#include <QPixmap>
class DbManager;
#include <QTimer>
#include <stdint.h>
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<int, MemberListItem> m_cache;
mutable int m_pendingFrom = -1;
mutable int m_pendingCount = 0;
QTimer* m_rttTimer;
};
#endif

101
tools/chatgui/src/memberpropsdialog.cpp

@ -1,26 +1,33 @@
#include "memberpropsdialog.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QMessageBox>
#include <cstring>
#include <openssl/evp.h>
#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();
}

8
tools/chatgui/src/memberpropsdialog.h

@ -4,6 +4,7 @@
#include <QCheckBox>
#include <QLineEdit>
#include <QString>
#include <stdint.h>
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;

34
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);

46
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) ── */

16
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);

Loading…
Cancel
Save