Use channel member snapshots for desktop participant details
This commit is contained in:
1 parent
c832a5d7a5
commit
539f09004c
18 files changed
+586
-444
No files matched your search
@@ -255,8 +255,7 @@ struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
|
||||
// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg)
|
||||
void chat_core_collect_conn_metrics_trampoline(void* arg);
|
||||
|
||||
// Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg)
|
||||
void chat_core_collect_member_detail_trampoline(void* arg);
|
||||
/* Диагностика карточки участника: запрос/codec находятся в chat_member.h. */
|
||||
|
||||
// ── Headless CLI API ──
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
|
||||
[20]="CONN_LIST", [21]="CONN_METRICS",
|
||||
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
|
||||
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
|
||||
[26]="BGP_INFO", [27]="CONNECTING_NODES",
|
||||
[27]="CONNECTING_NODES",
|
||||
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
|
||||
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
|
||||
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
|
||||
|
||||
@@ -44,11 +44,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_DETAIL 22 /* Адресованный ответ диагностики; wire описан в chat_member.h. */
|
||||
#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */
|
||||
#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */
|
||||
#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
|
||||
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
|
||||
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
|
||||
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
|
||||
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -97,6 +99,54 @@ void chat_core_toggle_member_radio_mute_trampoline(void* arg);
|
||||
void chat_member_init(struct UTUN_INSTANCE* inst);
|
||||
void chat_member_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Дополнительная диагностика карточки; online/RTT берутся только из chat_member_display.
|
||||
* Запрос выделяет GUI через u_calloc и передаёт в uasync; trampoline освобождает его.
|
||||
* Ответ CHAT_EVT_MEMBER_DETAIL всегда повторяет request_id/group_id/node_id, включая ошибки. */
|
||||
struct chat_member_detail_req {
|
||||
struct UTUN_INSTANCE* inst; /* ядро, в uasync которого выполняется сбор */
|
||||
uint64_t group_id; /* конкретная CHAT-группа */
|
||||
uint64_t node_id; /* выбранный участник */
|
||||
uint64_t request_id; /* ненулевой ID запроса, не повторяется при смене выбора */
|
||||
};
|
||||
|
||||
/* Wire LE: request:8, group:8, node:8, result:1, transport_flags:1, own_tcp:2,
|
||||
* hop_count:1, link_count:1, sock_count:1; далее hops:8*, links:64*, socks:48*.
|
||||
* transport_flags: bit0=conn_present, bit1=links_up, bit2=initialized; не доступность в группе.
|
||||
* hops — список выбранного пути от назначения к первому посреднику; у offline/self пустой.
|
||||
* Ошибка содержит только заголовок.
|
||||
* link: tcp:1,state:1,status:1,ready:1,server:1,local_family:1,local_addr:16,local_port:2,
|
||||
* remote_family:1,remote_addr:16,remote_port:2,socket_name:20,reality:1.
|
||||
* sock: id:4,port:2,links_up:2,links_down:2,ifname:38. Строки завершаются NUL. */
|
||||
#define CHAT_MEMBER_DETAIL_HEADER_SIZE 31
|
||||
#define CHAT_MEMBER_DETAIL_MAX_HOPS 16
|
||||
#define CHAT_MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define CHAT_MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define CHAT_MEMBER_DETAIL_LINK_SIZE 64
|
||||
#define CHAT_MEMBER_DETAIL_SOCK_SIZE 48
|
||||
#define CHAT_MEMBER_DETAIL_MAX_SIZE (CHAT_MEMBER_DETAIL_HEADER_SIZE + 8 * CHAT_MEMBER_DETAIL_MAX_HOPS + \
|
||||
CHAT_MEMBER_DETAIL_LINK_SIZE * CHAT_MEMBER_DETAIL_MAX_LINKS + CHAT_MEMBER_DETAIL_SOCK_SIZE * CHAT_MEMBER_DETAIL_MAX_SOCKS)
|
||||
|
||||
enum chat_member_detail_result {
|
||||
CHAT_MEMBER_DETAIL_OK = 0,
|
||||
CHAT_MEMBER_DETAIL_UNAVAILABLE = 1, /* чат/группа остановлены */
|
||||
CHAT_MEMBER_DETAIL_NOT_MEMBER = 2, /* запись участника отсутствует */
|
||||
CHAT_MEMBER_DETAIL_INVALID = 3, /* неверные аргументы запроса */
|
||||
CHAT_MEMBER_DETAIL_ERROR = 4 /* ошибка чтения данных участника */
|
||||
};
|
||||
|
||||
/* Проверенный view заимствует массивы из входного пакета; живёт не дольше data. */
|
||||
struct chat_member_detail_view {
|
||||
uint64_t request_id, group_id, node_id; /* адресат ответа */
|
||||
uint8_t result, transport_flags; /* результат и состояние физического транспорта */
|
||||
uint16_t own_tcp_active; /* TCP-соединения self */
|
||||
uint8_t hop_count, link_count, sock_count; /* длины массивов */
|
||||
const uint8_t *hops, *links, *socks; /* неизменяемые wire-массивы */
|
||||
};
|
||||
|
||||
/* Проверить весь пакет до применения: 0=успех, -1=ошибка с логом; out при ошибке не меняется. */
|
||||
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out);
|
||||
void chat_core_collect_member_detail_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+96
-86
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "chat_core_priv.h"
|
||||
#include "chat_event.h"
|
||||
#include "chat_member.h"
|
||||
#include "../member_files/protocol.h"
|
||||
|
||||
#include "../utun_instance.h"
|
||||
#include "../ntp_time.h"
|
||||
@@ -17,6 +19,7 @@
|
||||
#include "../transport_layer/stcp_link.h"
|
||||
#include "../transport_layer/pkt_normalizer.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include <string.h>
|
||||
|
||||
@@ -361,20 +364,49 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) {
|
||||
u_free(arg);
|
||||
}
|
||||
|
||||
/* ── Live-снапшот для панели деталей участника ──
|
||||
* packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:63B)*[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][is_server:1]
|
||||
* [local_family:1][local_addr:16][local_port:2]
|
||||
* [remote_family:1][remote_addr:16][remote_port:2]
|
||||
* [sock_name:20]
|
||||
* sock: [sock_id:4][port:2][links_up:2][links_down:2][ifname:38]
|
||||
* Собирается в worker-потоке — читает живые структуры UTUN_INSTANCE безопасно. */
|
||||
/* Диагностика выбранного участника собирается целиком в uasync; wire описан в chat_member.h. */
|
||||
#if CHAT_MEMBER_DETAIL_MAX_HOPS != MAX_HOPS
|
||||
#error "member detail hop limit must match routing"
|
||||
#endif
|
||||
|
||||
#define MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define MEMBER_DETAIL_LINK_SIZE 64
|
||||
#define MEMBER_DETAIL_SOCK_SIZE 48
|
||||
/* Проверяем размеры, адресата и все строки до изменения состояния GUI. */
|
||||
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out) {
|
||||
if (!data || !out || len < CHAT_MEMBER_DETAIL_HEADER_SIZE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: missing header bytes=%zu", len); return -1;
|
||||
}
|
||||
struct chat_member_detail_view v = {0};
|
||||
v.request_id = mf_get64(data); v.group_id = mf_get64(data + 8); v.node_id = mf_get64(data + 16);
|
||||
v.result = data[24]; v.transport_flags = data[25];
|
||||
v.own_tcp_active = (uint16_t)data[26] | ((uint16_t)data[27] << 8);
|
||||
v.hop_count = data[28]; v.link_count = data[29]; v.sock_count = data[30];
|
||||
size_t expected = CHAT_MEMBER_DETAIL_HEADER_SIZE + (size_t)v.hop_count * 8 +
|
||||
(size_t)v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE + (size_t)v.sock_count * CHAT_MEMBER_DETAIL_SOCK_SIZE;
|
||||
if (!v.request_id || !v.group_id || !v.node_id || v.result > CHAT_MEMBER_DETAIL_ERROR || v.transport_flags > 7 ||
|
||||
v.hop_count > CHAT_MEMBER_DETAIL_MAX_HOPS || v.link_count > CHAT_MEMBER_DETAIL_MAX_LINKS ||
|
||||
v.sock_count > CHAT_MEMBER_DETAIL_MAX_SOCKS || expected != len ||
|
||||
(v.result && (v.transport_flags || v.own_tcp_active || v.hop_count || v.link_count || v.sock_count))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid packet request=%llu group=%016llx node=%016llx bytes=%zu expected=%zu",
|
||||
(unsigned long long)v.request_id, (unsigned long long)v.group_id, (unsigned long long)v.node_id, len, expected);
|
||||
return -1;
|
||||
}
|
||||
v.hops = data + CHAT_MEMBER_DETAIL_HEADER_SIZE; v.links = v.hops + v.hop_count * 8;
|
||||
v.socks = v.links + v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE;
|
||||
for (int i = 0; i < v.hop_count; i++) if (!mf_get64(v.hops + i * 8)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: zero hop index=%d", i); return -1;
|
||||
}
|
||||
for (int i = 0; i < v.link_count; i++) {
|
||||
const uint8_t* link = v.links + i * CHAT_MEMBER_DETAIL_LINK_SIZE;
|
||||
if (!memchr(link + 43, 0, 20) || link[0] > 1 || link[3] > 1 || link[4] > 1 || link[63] > 1 ||
|
||||
(link[5] && link[5] != 4 && link[5] != 6) || (link[24] && link[24] != 4 && link[24] != 6)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid link index=%d", i); return -1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < v.sock_count; i++) if (!memchr(v.socks + i * CHAT_MEMBER_DETAIL_SOCK_SIZE + 10, 0, 38)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: unterminated interface index=%d", i); return -1;
|
||||
}
|
||||
*out = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */
|
||||
static void sock_ifname(const char* name, char* out, size_t out_sz) {
|
||||
@@ -391,8 +423,35 @@ static void sock_ifname(const char* name, char* out, size_t out_sz) {
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id) {
|
||||
if (!CC(inst) || !CC(inst)->initialized || !inst) return;
|
||||
static void collect_member_detail(const struct chat_member_detail_req* req) {
|
||||
struct UTUN_INSTANCE* inst = req->inst;
|
||||
uint64_t node_id = req->node_id;
|
||||
uint8_t buf[CHAT_MEMBER_DETAIL_MAX_SIZE] = {0};
|
||||
mf_put64(buf, req->request_id); mf_put64(buf + 8, req->group_id); mf_put64(buf + 16, node_id);
|
||||
struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, req->group_id) : NULL;
|
||||
if (!req->request_id || !req->group_id || !node_id) buf[24] = CHAT_MEMBER_DETAIL_INVALID;
|
||||
else if (!inst || !CC(inst) || !CC(inst)->initialized || !group || group->group_type != TOPO_GROUP_TYPE_CHAT)
|
||||
buf[24] = CHAT_MEMBER_DETAIL_UNAVAILABLE;
|
||||
if (!buf[24]) {
|
||||
char table[80], sql[160]; peers_table_name(group->channel_id, table, sizeof(table));
|
||||
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", table);
|
||||
sqlite3_stmt* st = NULL;
|
||||
int rc = sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
if (rc == SQLITE_DONE) buf[24] = CHAT_MEMBER_DETAIL_NOT_MEMBER;
|
||||
else if (rc != SQLITE_ROW) {
|
||||
buf[24] = CHAT_MEMBER_DETAIL_ERROR;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: query request=%llu rc=%d: %s",
|
||||
(unsigned long long)req->request_id, rc, sqlite3_errmsg(CC(inst)->db));
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
if (buf[24]) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member detail: rejected request=%llu group=%016llx node=%016llx result=%u",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id, buf[24]);
|
||||
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, CHAT_MEMBER_DETAIL_HEADER_SIZE); return;
|
||||
}
|
||||
|
||||
uint8_t flags = 0;
|
||||
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
|
||||
@@ -401,17 +460,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
if (conn->links_up) flags |= 2;
|
||||
if (conn->initialized) flags |= 4;
|
||||
}
|
||||
/* bit 3 (0x08): узел присутствует в BGP/topo_group — live-онлайн (self всегда онлайн) */
|
||||
{
|
||||
int bgp_present = (node_id == inst->node_id) ? 1 : 0;
|
||||
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
|
||||
for (struct ll_entry* ge = gl ? gl->head : NULL; !bgp_present && ge; ge = ge->next) {
|
||||
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
|
||||
if (group && topo_node_find_by_id(group, node_id)) bgp_present = 1;
|
||||
}
|
||||
if (bgp_present) flags |= 8;
|
||||
}
|
||||
|
||||
uint16_t own_tcp_active = 0;
|
||||
if (node_id == inst->node_id) {
|
||||
if (inst->connections) {
|
||||
@@ -432,23 +480,20 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
}
|
||||
|
||||
uint8_t link_count = 0;
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < MEMBER_DETAIL_MAX_LINKS; lk = lk->next)
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < CHAT_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)
|
||||
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < CHAT_MEMBER_DETAIL_MAX_SOCKS; s = s->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;
|
||||
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(group, node_id);
|
||||
uint8_t hop_count = 0;
|
||||
uint64_t* hops = topo_node_best_hop_list(node, &hop_count, NULL);
|
||||
buf[25] = flags; buf[26] = (uint8_t)own_tcp_active; buf[27] = (uint8_t)(own_tcp_active >> 8);
|
||||
buf[28] = hop_count; buf[29] = link_count; buf[30] = sock_count;
|
||||
uint8_t* p = buf + CHAT_MEMBER_DETAIL_HEADER_SIZE;
|
||||
for (int i = 0; i < hop_count; i++, p += 8) mf_put64(p, hops[i]);
|
||||
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count; lk = lk->next, link_count--) {
|
||||
*p++ = lk->is_tcp ? 1 : 0;
|
||||
@@ -473,7 +518,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
lf = 6; memcpy(la, &((const struct sockaddr_in6*)lsa)->sin6_addr, 16); lp = ntohs(((const struct sockaddr_in6*)lsa)->sin6_port);
|
||||
}
|
||||
}
|
||||
*p++ = lf; memcpy(p, la, 16); p += 16; memcpy(p, &lp, 2); p += 2;
|
||||
*p++ = lf; memcpy(p, la, 16); p += 16; *p++ = (uint8_t)lp; *p++ = (uint8_t)(lp >> 8);
|
||||
}
|
||||
|
||||
{ uint8_t rf = 0; uint8_t ra[16] = {0}; uint16_t rp = 0;
|
||||
@@ -483,7 +528,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
} else if (sa->ss_family == AF_INET6) {
|
||||
rf = 6; memcpy(ra, &((const struct sockaddr_in6*)sa)->sin6_addr, 16); rp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port);
|
||||
}
|
||||
*p++ = rf; memcpy(p, ra, 16); p += 16; memcpy(p, &rp, 2); p += 2;
|
||||
*p++ = rf; memcpy(p, ra, 16); p += 16; *p++ = (uint8_t)rp; *p++ = (uint8_t)(rp >> 8);
|
||||
}
|
||||
|
||||
{ char sn[20] = {0};
|
||||
@@ -493,7 +538,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
*p++ = lk->reality_set ? 1 : 0;
|
||||
}
|
||||
|
||||
*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;
|
||||
@@ -518,56 +562,22 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
}
|
||||
}
|
||||
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, &lu, 2); p += 2;
|
||||
memcpy(p, &ld, 2); p += 2;
|
||||
for (int i = 0; i < 4; i++) *p++ = (uint8_t)(sid >> (8 * i));
|
||||
*p++ = (uint8_t)sp; *p++ = (uint8_t)(sp >> 8);
|
||||
*p++ = (uint8_t)lu; *p++ = (uint8_t)(lu >> 8);
|
||||
*p++ = (uint8_t)ld; *p++ = (uint8_t)(ld >> 8);
|
||||
memcpy(p, ifname, 38); p += 38;
|
||||
}
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "member detail: request=%llu group=%016llx node=%016llx hops=%u links=%u socks=%u bytes=%zu",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id,
|
||||
buf[28], buf[29], buf[30], (size_t)(p - buf));
|
||||
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
|
||||
u_free(buf);
|
||||
|
||||
/* summary text: direct RTT or BGP */
|
||||
if (conn && conn->links_up) {
|
||||
uint16_t rtt = conn->links ? conn->links->rtt_last : 0;
|
||||
if (rtt > 0) {
|
||||
char tbuf[32];
|
||||
int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10);
|
||||
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
|
||||
}
|
||||
} else if (conn && !conn->links_up && node_id != inst->node_id) {
|
||||
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
|
||||
for (struct ll_entry* ge = gl ? gl->head : NULL; ge; ge = ge->next) {
|
||||
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
|
||||
if (!group) continue;
|
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id);
|
||||
if (!nq) continue;
|
||||
uint8_t hc = 0;
|
||||
uint64_t* hops = topo_node_best_hop_list(nq, &hc, NULL);
|
||||
if (hc > 0 && hops) {
|
||||
char tbuf[512] = {0};
|
||||
uint16_t cr = topo_get_chain_rtt(nq);
|
||||
int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc);
|
||||
for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) {
|
||||
char hn[32] = {0};
|
||||
get_node_name(inst, hops[hi], hn, sizeof(hn));
|
||||
uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF);
|
||||
off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off,
|
||||
" [%04X]%s", hid, hn[0] ? " " : "");
|
||||
if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn);
|
||||
}
|
||||
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void chat_core_collect_member_detail_trampoline(void* arg) {
|
||||
struct chat_node_arg* a = (struct chat_node_arg*)arg;
|
||||
if (!a) return;
|
||||
uint64_t node_id = a->node_id;
|
||||
collect_member_detail(a->inst, node_id);
|
||||
u_free(arg);
|
||||
struct chat_member_detail_req* req = arg;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: null request"); return; }
|
||||
collect_member_detail(req);
|
||||
u_free(req);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "chat/chat_core.h"
|
||||
#include "chat/chat_member.h"
|
||||
#include "chat/chat_event.h"
|
||||
#include "member_files/protocol.h"
|
||||
#include "chat/db_sync.h"
|
||||
#include "chat/member_sync.h"
|
||||
#include "media_async/media_async.h"
|
||||
@@ -62,6 +63,30 @@ static struct TOPO_GROUP* chat_group(struct UTUN_INSTANCE* inst) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int interrupt_detail_query(void* arg) { (void)arg; return 1; }
|
||||
|
||||
static uint8_t detail_reply[CHAT_MEMBER_DETAIL_MAX_SIZE];
|
||||
static int detail_reply_size, detail_unscoped_events;
|
||||
|
||||
static void capture_member_detail(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
|
||||
(void)inst;
|
||||
if (type == 26) detail_unscoped_events++;
|
||||
if (type != CHAT_EVT_MEMBER_DETAIL) return;
|
||||
assert(len >= CHAT_MEMBER_DETAIL_HEADER_SIZE && len <= (int)sizeof(detail_reply));
|
||||
memcpy(detail_reply, data, (size_t)len); detail_reply_size = len;
|
||||
}
|
||||
|
||||
static struct chat_member_detail_view member_diagnostics(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, uint64_t id) {
|
||||
struct chat_member_detail_req* req = u_calloc(1, sizeof(*req)); assert(req);
|
||||
req->inst = inst; req->group_id = group; req->node_id = node; req->request_id = id;
|
||||
detail_reply_size = 0;
|
||||
chat_core_collect_member_detail_trampoline(req);
|
||||
struct chat_member_detail_view view = {0};
|
||||
assert(!chat_core_parse_member_detail(detail_reply, (size_t)detail_reply_size, &view));
|
||||
assert(view.request_id == id && view.group_id == group && view.node_id == node);
|
||||
return view;
|
||||
}
|
||||
|
||||
/* Снимок привязан к группе; RTT адреса и факт наличия пути не заменяют живой маршрут. */
|
||||
static void check_member_snapshots(struct UTUN_INSTANCE* inst, char channels[2][64]) {
|
||||
struct TOPO_GROUP* groups[2]; struct TOPO_GROUP_NODE* nodes[2];
|
||||
@@ -98,11 +123,40 @@ static void check_member_snapshots(struct UTUN_INSTANCE* inst, char channels[2][
|
||||
assert(snapshot.conn_up == nodes[i]->conn_up && snapshot.conn_presence == nodes[i]->conn_presence);
|
||||
assert(!!(snapshot.flags & CHAT_MEMBER_FLAG_PHONE_BUSY) == i);
|
||||
}
|
||||
chat_event_handler_fn saved_handler = inst->chat_event_handler;
|
||||
inst->chat_event_handler = capture_member_detail;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
struct chat_member_detail_view view = member_diagnostics(inst, groups[i]->group_id, 7, 100 + i);
|
||||
assert(view.result == CHAT_MEMBER_DETAIL_OK && view.hop_count == i + 1);
|
||||
assert(mf_get64(view.hops) == 7 && mf_get64(view.hops + i * 8) == (uint64_t)(i ? 8 : 7));
|
||||
}
|
||||
assert(member_diagnostics(inst, groups[0]->group_id, 9, 103).result == CHAT_MEMBER_DETAIL_NOT_MEMBER);
|
||||
assert(member_diagnostics(inst, 1, 7, 104).result == CHAT_MEMBER_DETAIL_UNAVAILABLE);
|
||||
sqlite3_progress_handler(inst->topo_sqlite_db, 1, interrupt_detail_query, NULL);
|
||||
struct chat_member_detail_view failed = member_diagnostics(inst, groups[0]->group_id, 7, 108);
|
||||
sqlite3_progress_handler(inst->topo_sqlite_db, 0, NULL, NULL);
|
||||
assert(failed.result == CHAT_MEMBER_DETAIL_ERROR);
|
||||
member_diagnostics(inst, groups[1]->group_id, 7, 105);
|
||||
struct chat_member_detail_view untouched = { .request_id = 999 };
|
||||
assert(chat_core_parse_member_detail(detail_reply, 0, &untouched) < 0 && untouched.request_id == 999);
|
||||
assert(chat_core_parse_member_detail(detail_reply, (size_t)detail_reply_size - 1, &untouched) < 0);
|
||||
uint8_t saved_count = detail_reply[28]; detail_reply[28] = CHAT_MEMBER_DETAIL_MAX_HOPS + 1;
|
||||
assert(chat_core_parse_member_detail(detail_reply, (size_t)detail_reply_size, &untouched) < 0);
|
||||
detail_reply[28] = saved_count;
|
||||
struct chat_member_detail_view valid;
|
||||
assert(!chat_core_parse_member_detail(detail_reply, (size_t)detail_reply_size, &valid));
|
||||
if (valid.sock_count) {
|
||||
memset((uint8_t*)valid.socks + 10, 'x', 38);
|
||||
assert(chat_core_parse_member_detail(detail_reply, (size_t)detail_reply_size, &untouched) < 0);
|
||||
}
|
||||
assert(untouched.request_id == 999 && !detail_unscoped_events);
|
||||
direct.links_up = 0;
|
||||
uint8_t wire[CHAT_MEMBER_DISPLAY_SIZE]; struct chat_member_display snapshot;
|
||||
assert(!chat_core_get_single_member(inst, channels[0], 7, wire)); chat_core_deserialize_member(wire, &snapshot);
|
||||
assert(!snapshot.online && snapshot.rtt == UINT16_MAX && !(snapshot.flags & CHAT_MEMBER_FLAG_PHONE_KNOWN));
|
||||
assert(!chat_core_member_online(inst, channels[0], 7) && chat_core_member_online(inst, channels[1], 7));
|
||||
assert(member_diagnostics(inst, groups[0]->group_id, 7, 106).hop_count == 0);
|
||||
assert(member_diagnostics(inst, groups[1]->group_id, 7, 107).hop_count == 2);
|
||||
assert(!topo_group_add_path(nodes[0], &relay, relay_hops, 2, 20));
|
||||
assert(!chat_core_get_single_member(inst, channels[0], 7, wire)); chat_core_deserialize_member(wire, &snapshot);
|
||||
assert(snapshot.online && snapshot.rtt == 90 && snapshot.intermediaries == 1);
|
||||
@@ -112,6 +166,7 @@ static void check_member_snapshots(struct UTUN_INSTANCE* inst, char channels[2][
|
||||
relay.rtt_last = 65530;
|
||||
assert(!chat_core_get_single_member(inst, channels[0], 7, wire)); chat_core_deserialize_member(wire, &snapshot);
|
||||
assert(snapshot.rtt == UINT16_MAX); /* насыщение, без переполнения и подстановки node_RTT */
|
||||
inst->chat_event_handler = saved_handler;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
topo_nodeq_clear_paths(nodes[i]); queue_remove_data(groups[i]->nodes, &nodes[i]->ll); queue_entry_free(&nodes[i]->ll);
|
||||
assert(!topo_node_sqlite_member_del(inst->topo_sqlite_db, channels[i], 7));
|
||||
|
||||
@@ -208,59 +208,6 @@ fun MemberWire.toChatMember(previous: ChatMember = ChatMember(nodeId, name)): Ch
|
||||
fun cleanSocketName(raw: String): String =
|
||||
raw.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), " $1")
|
||||
|
||||
/* ── MEMBER_DETAIL (event 22) link format: 64 bytes per link ── */
|
||||
private const val MEMBER_DETAIL_LINK_SIZE = 64
|
||||
|
||||
private fun addrFromBytes(family: Int, bytes: ByteArray): String {
|
||||
if (family == 4 && bytes.size >= 4)
|
||||
return "${bytes[0].toInt() and 0xFF}.${bytes[1].toInt() and 0xFF}.${bytes[2].toInt() and 0xFF}.${bytes[3].toInt() and 0xFF}"
|
||||
if (family == 6 && bytes.size >= 16)
|
||||
return bytes.take(16).joinToString(":") { "%02x".format(it) }
|
||||
return "?"
|
||||
}
|
||||
|
||||
fun parseMemberDetail(data: ByteArray): MemberLinks? {
|
||||
if (data.size < 12) return null
|
||||
val buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
|
||||
buf.getLong()
|
||||
val flags = buf.get().toInt() and 0xFF
|
||||
buf.getShort()
|
||||
val linkCount = buf.get().toInt() and 0xFF
|
||||
|
||||
val links = mutableListOf<LinkStatus>()
|
||||
var offset = 12
|
||||
for (i in 0 until linkCount) {
|
||||
if (offset + MEMBER_DETAIL_LINK_SIZE > data.size) break
|
||||
val lbuf = ByteBuffer.wrap(data, offset, MEMBER_DETAIL_LINK_SIZE).order(ByteOrder.LITTLE_ENDIAN)
|
||||
val isTcp = lbuf.get().toInt() and 0xFF
|
||||
val state = lbuf.get().toInt() and 0xFF
|
||||
val status = lbuf.get().toInt() and 0xFF
|
||||
val tcpReady = lbuf.get().toInt() and 0xFF
|
||||
val isServer = lbuf.get().toInt() and 0xFF
|
||||
val lf = lbuf.get().toInt() and 0xFF; val la = ByteArray(16); lbuf.get(la)
|
||||
val lp = lbuf.getShort().toInt() and 0xFFFF
|
||||
val rf = lbuf.get().toInt() and 0xFF; val ra = ByteArray(16); lbuf.get(ra)
|
||||
val rp = lbuf.getShort().toInt() and 0xFFFF
|
||||
val snBytes = ByteArray(20); lbuf.get(snBytes)
|
||||
val sn = String(snBytes).trimEnd('\u0000').trimEnd()
|
||||
val isReality = lbuf.get().toInt() != 0
|
||||
val effState = if (isTcp != 0 && tcpReady != 0) 3 else state
|
||||
val effStatus = if (isTcp != 0 && tcpReady != 0) 1 else status
|
||||
links.add(LinkStatus(
|
||||
localFamily = lf, localAddr = addrFromBytes(lf, la), localPort = lp,
|
||||
remoteFamily = rf, remoteAddr = addrFromBytes(rf, ra), remotePort = rp,
|
||||
linkState = effState, linkStatus = effStatus, isServer = isServer != 0,
|
||||
socketName = sn, protocol = if (isReality) "TCP+REALITY" else if (isTcp != 0) "TCP" else "UDP"
|
||||
))
|
||||
offset += MEMBER_DETAIL_LINK_SIZE
|
||||
}
|
||||
|
||||
return MemberLinks(
|
||||
connected = (flags and 1) != 0, initialized = (flags and 2) != 0,
|
||||
linksUp = if ((flags and 4) != 0) 1 else 0, links = links
|
||||
)
|
||||
}
|
||||
|
||||
class ChatRepository {
|
||||
private val myNodeId: Long = NativeLib.getMyNodeId()
|
||||
|
||||
|
||||
@@ -715,15 +715,6 @@ class ChatViewModel : ViewModel() {
|
||||
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
|
||||
} else refreshSelectedMemberDetail(channel, node)
|
||||
}
|
||||
22 -> { /* MEMBER_DETAIL: бинарный снапшот links+sockets */
|
||||
if (data == null || data.size < 12) return
|
||||
val ml = parseMemberDetail(data) ?: return
|
||||
val buf = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN)
|
||||
val nodeId = buf.getLong()
|
||||
if (_selectedMember.value?.nodeId == nodeId) {
|
||||
_memberLinks.value = ml
|
||||
}
|
||||
}
|
||||
27 -> { /* CONNECTING_NODES: [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
|
||||
if (data == null || data.size < 3) return
|
||||
val cl = data[0].toInt() and 0xFF
|
||||
|
||||
@@ -611,6 +611,12 @@ target_link_libraries(test_radio_member_mute PRIVATE ${QT_CORE} utun_voice utun
|
||||
add_test(NAME test_radio_member_mute COMMAND test_radio_member_mute)
|
||||
set(RADIO_MEMBER_TEST_SOURCES src/memberlistmodel.cpp src/avatars/avatarcache.cpp src/avatars/avatarmanager.cpp
|
||||
src/avatars/avatarcrop.cpp src/avatars/avatargallery.cpp src/imageviewer_window.cpp db/db_manager.cpp)
|
||||
add_executable(test_member_detail tests/test_member_detail.cpp src/accountlist.cpp src/accountdelegate.cpp
|
||||
src/flagpainter.cpp src/memberpropsdialog.cpp src/renamedialog.cpp ${RADIO_MEMBER_TEST_SOURCES})
|
||||
target_include_directories(test_member_detail PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
target_link_libraries(test_member_detail PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun_voice utun pthread)
|
||||
add_test(NAME test_member_detail COMMAND test_member_detail)
|
||||
set_tests_properties(test_member_detail PROPERTIES TIMEOUT 20 ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
add_executable(test_radio_mute_bridge tests/test_radio_mute_bridge.cpp transport/utun_node.cpp transport/gui_bridge_impl.cpp transport/stream_bridge.cpp
|
||||
src/accountdelegate.cpp src/flagpainter.cpp ${RADIO_MEMBER_TEST_SOURCES})
|
||||
target_include_directories(test_radio_mute_bridge PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
|
||||
+141
-170
@@ -15,6 +15,8 @@
|
||||
#include <QFont>
|
||||
#include <QSet>
|
||||
#include <QMessageBox>
|
||||
#include <QShowEvent>
|
||||
#include <QHideEvent>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "../../../lib/platform_compat.h"
|
||||
@@ -23,16 +25,17 @@ extern "C" {
|
||||
#include "../lib/socket_compat.h"
|
||||
#include "../lib/debug_config.h"
|
||||
#include "chat/chat_core.h"
|
||||
#include "routing_layer/topo_node.h"
|
||||
#include "member_files/protocol.h"
|
||||
#include "../../lib/mem.h"
|
||||
}
|
||||
|
||||
static AccountList* g_detailTarget = nullptr;
|
||||
static quint64 g_nextDetailRequest = 0; // только GUI-поток, не сбрасывается при пересоздании панели
|
||||
|
||||
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);
|
||||
g_detailTarget->onMemberDetailData(data, len);
|
||||
}
|
||||
|
||||
QString AccountList::addrStr(const void* addr, int family) {
|
||||
@@ -124,11 +127,13 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
detailLayout->setSpacing(0);
|
||||
|
||||
m_detailHeader = new QLabel(QCoreApplication::translate("ChatGui", "Select a member"), m_detailPanel);
|
||||
m_detailHeader->setObjectName("memberDetailHeader");
|
||||
m_detailHeader->setWordWrap(true);
|
||||
m_detailHeader->setStyleSheet("font-weight: bold; padding: 4px 8px; background: palette(window);");
|
||||
detailLayout->addWidget(m_detailHeader);
|
||||
|
||||
m_detailText = new QTextEdit(m_detailPanel);
|
||||
m_detailText->setObjectName("memberDetailText");
|
||||
m_detailText->setReadOnly(true);
|
||||
m_detailText->setFont(QFont("monospace", 9));
|
||||
m_detailText->setStyleSheet("QTextEdit { border: none; padding: 4px; background: palette(base); }");
|
||||
@@ -150,15 +155,11 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
connect(m_listView, &QWidget::customContextMenuRequested,
|
||||
this, &AccountList::onMemberContextMenu);
|
||||
|
||||
m_detailClock.start();
|
||||
m_detailTimer = new QTimer(this);
|
||||
m_detailTimer->setInterval(5000);
|
||||
m_detailTimer->setInterval(1000);
|
||||
connect(m_detailTimer, &QTimer::timeout, this, &AccountList::onDetailTimerTick);
|
||||
|
||||
m_linkTimer = new QTimer(this);
|
||||
m_linkTimer->setInterval(1000);
|
||||
connect(m_linkTimer, &QTimer::timeout, this, [this]{
|
||||
if (m_detailNodeId != 0) requestMemberDetail();
|
||||
});
|
||||
connect(m_model, &MemberListModel::snapshotChanged, this, &AccountList::onSnapshotChanged);
|
||||
|
||||
g_detailTarget = this;
|
||||
gui_bridge_set_radio_mute_result_cb([](const uint8_t* data, int len) {
|
||||
@@ -168,25 +169,17 @@ AccountList::AccountList(DbManager* db, QWidget *parent)
|
||||
});
|
||||
gui_bridge_set_member_detail_cb(memberDetailCb);
|
||||
|
||||
static bool nodeinfoRegistered = false;
|
||||
if (!nodeinfoRegistered) {
|
||||
gui_bridge_set_nodeinfo_update_cb([](const uint8_t* data, int len) {
|
||||
if (g_detailTarget) g_detailTarget->onNodeinfoUpdate(data, len);
|
||||
});
|
||||
gui_bridge_set_bgp_info_cb([](const uint8_t* data, int len) {
|
||||
if (!g_detailTarget) return;
|
||||
g_detailTarget->m_bgpText = QString::fromUtf8((const char*)data, len);
|
||||
g_detailTarget->updateDetailDisplay();
|
||||
});
|
||||
nodeinfoRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
AccountList::~AccountList() {
|
||||
if (g_detailTarget == this) { g_detailTarget = nullptr; gui_bridge_set_radio_mute_result_cb(nullptr); }
|
||||
clearMemberDetail();
|
||||
if (g_detailTarget == this) {
|
||||
g_detailTarget = nullptr; gui_bridge_set_radio_mute_result_cb(nullptr); gui_bridge_set_member_detail_cb(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void AccountList::setChannel(const QString& channelId) {
|
||||
if (m_channelId != channelId) clearMemberDetail();
|
||||
m_channelId = channelId;
|
||||
|
||||
bool isOwner = false;
|
||||
@@ -198,11 +191,6 @@ void AccountList::setChannel(const QString& channelId) {
|
||||
}
|
||||
|
||||
m_model->setChannel(channelId, isOwner);
|
||||
m_detailTimer->stop();
|
||||
m_linkTimer->stop();
|
||||
m_detailNodeId = 0;
|
||||
m_detailHeader->setText(QCoreApplication::translate("ChatGui", "Select a member"));
|
||||
m_detailText->clear();
|
||||
|
||||
m_header->setText(isOwner ? QCoreApplication::translate("ChatGui", "Members 🔑") : QCoreApplication::translate("ChatGui", "Members"));
|
||||
m_header->setToolTip(isOwner ? QCoreApplication::translate("ChatGui", "Channel owner (has private key)") : QString());
|
||||
@@ -216,68 +204,71 @@ void AccountList::onMemberClicked(const QModelIndex& index) {
|
||||
if (nodeId) showMemberDetail(nodeId);
|
||||
}
|
||||
|
||||
void AccountList::clearMemberDetail() {
|
||||
if (m_detailNodeId || m_detailRequest) DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION,
|
||||
"member detail cleared channel=%s node=%016llx request=%llu", qPrintable(m_channelId),
|
||||
(unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest);
|
||||
m_detailTimer->stop(); m_detailNodeId = 0; m_detailRequest = 0; m_detailDirty = false;
|
||||
m_detailHops.clear(); m_snapLinks.clear(); m_snapSocks.clear(); m_detailAddrs.clear();
|
||||
m_detailLastSeen = 0; m_detailCreated = 0;
|
||||
m_detailHeader->setText(QCoreApplication::translate("ChatGui", "Select a member")); m_detailText->clear();
|
||||
}
|
||||
|
||||
void AccountList::showEvent(QShowEvent* event) {
|
||||
QWidget::showEvent(event);
|
||||
if (m_detailNodeId) { m_detailTimer->start(); requestMemberDetail(); }
|
||||
}
|
||||
|
||||
void AccountList::hideEvent(QHideEvent* event) {
|
||||
m_detailTimer->stop(); m_detailRequest = 0; m_detailHops.clear();
|
||||
QWidget::hideEvent(event);
|
||||
}
|
||||
|
||||
void AccountList::onSnapshotChanged(quint64 nodeId) {
|
||||
if (!m_detailNodeId || (nodeId && nodeId != m_detailNodeId)) return;
|
||||
if (!m_model->member(m_detailNodeId)) { clearMemberDetail(); return; }
|
||||
m_detailDirty = true; m_detailHops.clear();
|
||||
for (int row = 0; row < m_model->rowCount(); row++) if (m_model->nodeIdAt(row) == m_detailNodeId) {
|
||||
m_listView->setCurrentIndex(m_model->index(row)); break;
|
||||
}
|
||||
updateDetailDisplay();
|
||||
requestMemberDetail();
|
||||
}
|
||||
|
||||
void AccountList::showMemberDetail(quint64 nodeId) {
|
||||
if (!m_db || !m_db->m_db) return;
|
||||
|
||||
m_detailTimer->stop();
|
||||
m_linkTimer->stop();
|
||||
m_detailNodeId = nodeId;
|
||||
m_detailName.clear();
|
||||
m_detailOnline = false;
|
||||
m_detailLastSeen = 0;
|
||||
m_detailCreated = 0;
|
||||
m_detailAddrs.clear();
|
||||
m_snapConnPresent = false;
|
||||
m_snapConnUp = false;
|
||||
m_snapConnInit = false;
|
||||
m_snapOwnTcpActive = 0;
|
||||
m_snapChainRtt = 0;
|
||||
m_snapHopCount = 0;
|
||||
m_snapBgpNodes = 0;
|
||||
m_bgpText.clear();
|
||||
m_snapLinks.clear();
|
||||
m_snapSocks.clear();
|
||||
if (!m_model->member(nodeId)) return;
|
||||
clearMemberDetail(); m_detailNodeId = nodeId;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "member detail selected channel=%s node=%016llx", qPrintable(m_channelId),
|
||||
(unsigned long long)nodeId);
|
||||
readDetailMetadata(); updateDetailDisplay();
|
||||
if (isVisible()) { m_detailTimer->start(); requestMemberDetail(); }
|
||||
}
|
||||
|
||||
/* Только статические сведения БД; имя и live-состояние принадлежат модели участников. */
|
||||
void AccountList::readDetailMetadata() {
|
||||
m_detailAddrs.clear(); m_detailLastSeen = 0; m_detailCreated = 0;
|
||||
if (!m_db || !m_db->m_db || !m_detailNodeId) return;
|
||||
quint64 nodeId = m_detailNodeId;
|
||||
sqlite3* d = m_db->m_db;
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(d,
|
||||
"SELECT name, last_seen_at, created_at FROM nodes WHERE node_id=?",
|
||||
-1, &st, nullptr) == SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(d, "SELECT last_seen_at,created_at FROM nodes WHERE node_id=?", -1, &st, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
m_detailName = DbManager::colText(st, 0);
|
||||
m_detailLastSeen = sqlite3_column_int64(st, 1);
|
||||
m_detailCreated = sqlite3_column_int64(st, 2);
|
||||
}
|
||||
int rc = sqlite3_step(st);
|
||||
if (rc == SQLITE_ROW) { m_detailLastSeen = sqlite3_column_int64(st, 0); m_detailCreated = sqlite3_column_int64(st, 1); }
|
||||
else if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail dates read: %s", sqlite3_errmsg(d));
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
if (m_detailName.isEmpty())
|
||||
m_detailName = QString("0x%1").arg(nodeId, 8, 16, QChar('0')).right(4).toUpper();
|
||||
|
||||
/* prefer local nickname */
|
||||
QString tbl = m_db->peersTableName(m_channelId);
|
||||
sqlite3_stmt* nickSt = nullptr;
|
||||
QByteArray nickSql = QString(
|
||||
"SELECT local_nick FROM \"%1\" WHERE node_id=?"
|
||||
).arg(tbl).toUtf8();
|
||||
if (sqlite3_prepare_v2(d, nickSql.constData(), -1, &nickSt, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(nickSt, 1, (sqlite3_int64)nodeId);
|
||||
if (sqlite3_step(nickSt) == SQLITE_ROW) {
|
||||
QString n = DbManager::colText(nickSt, 0);
|
||||
if (!n.isEmpty()) m_detailName = n;
|
||||
}
|
||||
sqlite3_finalize(nickSt);
|
||||
}
|
||||
} else DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail dates prepare: %s", sqlite3_errmsg(d));
|
||||
|
||||
sqlite3_stmt* na = nullptr;
|
||||
sqlite3_prepare_v2(d,
|
||||
if (sqlite3_prepare_v2(d,
|
||||
"SELECT family, protocol, address, port, socket_id, addr_type, nat_mapping, nat_inbound, nat_source"
|
||||
" FROM node_addresses WHERE node_id=?",
|
||||
-1, &na, nullptr);
|
||||
" FROM node_addresses WHERE node_id=?", -1, &na, nullptr) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail addresses prepare: %s", sqlite3_errmsg(d)); return;
|
||||
}
|
||||
if (na) {
|
||||
sqlite3_bind_int64(na, 1, (sqlite3_int64)nodeId);
|
||||
while (sqlite3_step(na) == SQLITE_ROW) {
|
||||
int rc;
|
||||
while ((rc = sqlite3_step(na)) == SQLITE_ROW) {
|
||||
DetailAddress a;
|
||||
a.family = sqlite3_column_int(na, 0);
|
||||
a.protocol = sqlite3_column_int(na, 1);
|
||||
@@ -292,6 +283,7 @@ void AccountList::showMemberDetail(quint64 nodeId) {
|
||||
a.nat_source = sqlite3_column_int(na, 8);
|
||||
if (!a.addr.isEmpty()) m_detailAddrs.append(a);
|
||||
}
|
||||
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail addresses read: %s", sqlite3_errmsg(d));
|
||||
sqlite3_finalize(na);
|
||||
}
|
||||
|
||||
@@ -304,100 +296,84 @@ void AccountList::showMemberDetail(quint64 nodeId) {
|
||||
return a.port < b.port;
|
||||
});
|
||||
|
||||
m_detailTimer->start();
|
||||
m_linkTimer->start();
|
||||
updateDetailDisplay();
|
||||
requestMemberDetail();
|
||||
}
|
||||
|
||||
void AccountList::requestMemberDetail() {
|
||||
if (m_detailNodeId == 0) return;
|
||||
struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg));
|
||||
if (!arg) { DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "AccountList: req alloc failed"); return; }
|
||||
arg->inst = gui_bridge_get_inst();
|
||||
arg->node_id = m_detailNodeId;
|
||||
gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, arg);
|
||||
if (!m_detailNodeId || !isVisible() || !gui_bridge_is_uasync_ready()) return;
|
||||
const qint64 now = m_detailClock.elapsed();
|
||||
if (m_detailRequest && now - m_detailRequestedAt < 5000) return;
|
||||
if (m_detailRequest) DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "member detail timeout channel=%s node=%016llx request=%llu",
|
||||
qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest);
|
||||
auto* req = (chat_member_detail_req*)u_calloc(1, sizeof(chat_member_detail_req));
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "member detail request allocation failed"); m_detailRequest = 0; return; }
|
||||
req->inst = gui_bridge_get_inst(); req->group_id = m_channelId.toULongLong(); req->node_id = m_detailNodeId;
|
||||
if (++g_nextDetailRequest == 0) ++g_nextDetailRequest;
|
||||
req->request_id = g_nextDetailRequest;
|
||||
m_detailRequest = req->request_id; m_detailRequestedAt = now; m_detailDirty = false;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, "member detail requested channel=%s node=%016llx request=%llu", qPrintable(m_channelId),
|
||||
(unsigned long long)m_detailNodeId, (unsigned long long)m_detailRequest);
|
||||
gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, req);
|
||||
}
|
||||
|
||||
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;
|
||||
m_detailOnline = (flags & 8) != 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.isServer = p[4] != 0;
|
||||
lk.localFamily = p[5];
|
||||
lk.localAddr = QByteArray((const char*)p + 6, 16);
|
||||
memcpy(&lk.localPort, p + 22, 2);
|
||||
lk.family = p[24];
|
||||
lk.addr = QByteArray((const char*)p + 25, 16);
|
||||
memcpy(&lk.port, p + 41, 2);
|
||||
memcpy(lk.sockName, p + 43, 20); lk.sockName[19] = '\0';
|
||||
lk.isReality = p[63] != 0;
|
||||
p += MEMBER_DETAIL_LINK_SIZE;
|
||||
m_snapLinks.append(lk);
|
||||
void AccountList::onMemberDetailData(const uint8_t* data, int len) {
|
||||
chat_member_detail_view view{};
|
||||
if (len < 0 || chat_core_parse_member_detail(data, (size_t)len, &view) < 0) return;
|
||||
if (!m_detailRequest || view.request_id != m_detailRequest || view.group_id != m_channelId.toULongLong() ||
|
||||
view.node_id != m_detailNodeId) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, "member detail ignored stale request=%llu group=%016llx node=%016llx",
|
||||
(unsigned long long)view.request_id, (unsigned long long)view.group_id, (unsigned long long)view.node_id); return;
|
||||
}
|
||||
|
||||
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);
|
||||
uint16_t lu = 0, ld = 0; memcpy(&lu, p + 6, 2); memcpy(&ld, p + 8, 2);
|
||||
s.linksUp = lu; s.linksDown = ld;
|
||||
s.ifname = QString::fromUtf8((const char*)p + 10).trimmed();
|
||||
p += MEMBER_DETAIL_SOCK_SIZE;
|
||||
m_snapSocks.append(s);
|
||||
m_detailRequest = 0;
|
||||
if (view.result != CHAT_MEMBER_DETAIL_OK) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "member detail failed channel=%s node=%016llx request=%llu result=%u",
|
||||
qPrintable(m_channelId), (unsigned long long)m_detailNodeId, (unsigned long long)view.request_id, view.result);
|
||||
m_detailHops.clear(); m_snapLinks.clear(); m_snapSocks.clear(); updateDetailDisplay(); return;
|
||||
}
|
||||
|
||||
if (m_detailDirty) { requestMemberDetail(); return; }
|
||||
QVector<MemberLinkSnap> links;
|
||||
for (int i = 0; i < view.link_count; i++) {
|
||||
const uint8_t* p = view.links + i * CHAT_MEMBER_DETAIL_LINK_SIZE;
|
||||
MemberLinkSnap lk{};
|
||||
lk.isTcp = p[0] != 0; lk.linkState = p[1]; lk.linkStatus = p[2]; lk.tcpReady = p[3] != 0; lk.isServer = p[4] != 0;
|
||||
lk.localFamily = p[5]; lk.localAddr = QByteArray((const char*)p + 6, 16);
|
||||
lk.localPort = (uint16_t)p[22] | ((uint16_t)p[23] << 8);
|
||||
lk.family = p[24]; lk.addr = QByteArray((const char*)p + 25, 16);
|
||||
lk.port = (uint16_t)p[41] | ((uint16_t)p[42] << 8);
|
||||
memcpy(lk.sockName, p + 43, 20); lk.isReality = p[63] != 0; links.append(lk);
|
||||
}
|
||||
QVector<MemberSockSnap> socks;
|
||||
for (int i = 0; i < view.sock_count; i++) {
|
||||
const uint8_t* p = view.socks + i * CHAT_MEMBER_DETAIL_SOCK_SIZE;
|
||||
MemberSockSnap s{};
|
||||
uint32_t id = 0; for (int j = 0; j < 4; j++) id |= (uint32_t)p[j] << (8 * j);
|
||||
s.sockId = id; s.port = (uint16_t)p[4] | ((uint16_t)p[5] << 8);
|
||||
s.linksUp = (uint16_t)p[6] | ((uint16_t)p[7] << 8); s.linksDown = (uint16_t)p[8] | ((uint16_t)p[9] << 8);
|
||||
s.ifname = QString::fromUtf8((const char*)p + 10); socks.append(s);
|
||||
}
|
||||
QVector<quint64> hops;
|
||||
for (int i = 0; i < view.hop_count; i++) hops.append(mf_get64(view.hops + i * 8));
|
||||
m_snapLinks = links; m_snapSocks = socks; m_detailHops = hops;
|
||||
readDetailMetadata();
|
||||
/* 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.socket_id != b.socket_id) return a.socket_id < b.socket_id;
|
||||
if (a.addr_type != b.addr_type) return a.addr_type < b.addr_type;
|
||||
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() {
|
||||
bool isSelf = m_db ? (m_detailNodeId == m_db->myNodeId()) : false;
|
||||
const auto member = m_model->member(m_detailNodeId);
|
||||
if (!member) return;
|
||||
const bool isSelf = member->is_self != 0;
|
||||
QString nodeHex = QString("0x%1").arg(m_detailNodeId, 16, 16, QChar('0'));
|
||||
|
||||
/* ── Header ── */
|
||||
QString dotColor = m_detailOnline ? "#4CAF50" : "#9E9E9E";
|
||||
QString dotColor = member->online ? "#4CAF50" : "#9E9E9E";
|
||||
QStringList hdr;
|
||||
hdr.append(QString("<span style='color:%1;'>●</span> %2 %3")
|
||||
.arg(dotColor, m_detailName.toHtmlEscaped(), nodeHex));
|
||||
.arg(dotColor, QString::fromUtf8(member->name).toHtmlEscaped(), nodeHex));
|
||||
if (m_detailLastSeen > 0)
|
||||
hdr.append(QCoreApplication::translate("ChatGui", "seen %1").arg(relativeTimeStr(m_detailLastSeen)));
|
||||
if (m_detailCreated > 0)
|
||||
@@ -455,11 +431,20 @@ void AccountList::updateDetailDisplay() {
|
||||
lines.append(QString(" %1").arg(lk.sockName));
|
||||
}
|
||||
}
|
||||
/* summary */
|
||||
if (!m_bgpText.isEmpty()) {
|
||||
lines.append(m_bgpText);
|
||||
} else if (m_snapConnUp) {
|
||||
lines.append("UP");
|
||||
QString route = !member->online ? QCoreApplication::translate("ChatGui", "offline") :
|
||||
member->conn_up & NCONN_DIRECT ? "D" : member->conn_up & NCONN_INDIRECT ? "I" : "B";
|
||||
if (member->online && member->rtt != 0xffff) route += QString(" · rtt=%1ms").arg(member->rtt / 10.0, 0, 'f', 1);
|
||||
if (member->online && member->intermediaries) route += QString(" · hop:%1").arg(member->intermediaries);
|
||||
lines.prepend(route);
|
||||
if (!m_detailHops.isEmpty() && member->online) {
|
||||
QStringList hops;
|
||||
for (auto it = m_detailHops.crbegin(); it != m_detailHops.crend(); ++it) {
|
||||
QString label = QString("%1").arg(*it, 16, 16, QChar('0'));
|
||||
const auto hop = m_model->member(*it);
|
||||
if (hop) label += ' ' + QString::fromUtf8(hop->name).toHtmlEscaped();
|
||||
hops.append(label);
|
||||
}
|
||||
lines.append(QString("BGP: ") + hops.join(" → "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,20 +454,6 @@ void AccountList::updateDetailDisplay() {
|
||||
m_detailText->setHtml(QString("<pre style='margin:0;'>") + lines.join("\n") + "</pre>");
|
||||
}
|
||||
|
||||
void AccountList::onNodeinfoUpdate(const uint8_t* data, int len) {
|
||||
if (len < 16 || m_detailNodeId == 0) return;
|
||||
uint64_t nodeId; memcpy(&nodeId, data, 8);
|
||||
if (nodeId != m_detailNodeId) return;
|
||||
m_snapConnPresent = (data[8] & 1) != 0;
|
||||
m_snapConnUp = (data[9] & 1) != 0;
|
||||
memcpy(&m_snapChainRtt, data + 10, 2);
|
||||
m_snapHopCount = data[12];
|
||||
memcpy(&m_snapBgpNodes, data + 14, 2);
|
||||
/* data[13] is reserved/unused */
|
||||
if (m_snapConnPresent)
|
||||
updateDetailDisplay();
|
||||
}
|
||||
|
||||
void AccountList::onDetailTimerTick() {
|
||||
if (m_detailNodeId != 0) requestMemberDetail();
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ public:
|
||||
|
||||
void setChannel(const QString& channelId);
|
||||
void refresh();
|
||||
void onMemberDetailData(uint64_t nodeId, const uint8_t* data, int len);
|
||||
void onNodeinfoUpdate(const uint8_t* data, int len);
|
||||
void onMemberDetailData(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);
|
||||
@@ -84,7 +83,14 @@ private slots:
|
||||
void onMemberContextMenu(const QPoint& pos);
|
||||
void onDetailTimerTick();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
void hideEvent(QHideEvent* event) override;
|
||||
|
||||
private:
|
||||
void clearMemberDetail();
|
||||
void readDetailMetadata();
|
||||
void onSnapshotChanged(quint64 nodeId);
|
||||
void showMemberDetail(quint64 nodeId);
|
||||
void updateDetailDisplay();
|
||||
void requestMemberDetail();
|
||||
@@ -99,24 +105,18 @@ private:
|
||||
QLabel *m_detailHeader;
|
||||
QTextEdit *m_detailText;
|
||||
QTimer *m_detailTimer;
|
||||
QTimer *m_linkTimer;
|
||||
QString m_channelId;
|
||||
|
||||
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;
|
||||
uint16_t m_snapChainRtt = 0;
|
||||
uint8_t m_snapHopCount = 0;
|
||||
uint16_t m_snapBgpNodes = 0;
|
||||
QString m_bgpText;
|
||||
QElapsedTimer m_detailClock; // монотонный срок запроса
|
||||
quint64 m_detailRequest = 0; // ожидаемый request_id; 0 = нет запроса
|
||||
qint64 m_detailRequestedAt = 0;
|
||||
bool m_detailDirty = false; // снимок участника изменился после отправки запроса
|
||||
QVector<quint64> m_detailHops; // доп. путь; сбрасывается при следующем member-событии
|
||||
QVector<MemberLinkSnap> m_snapLinks;
|
||||
QVector<MemberSockSnap> m_snapSocks;
|
||||
};
|
||||
@@ -633,7 +633,7 @@ void MainWindow::setupBridgeCallbacks() {
|
||||
if (type == 13) {
|
||||
s_mainWindow->m_cpuLoadPending = false;
|
||||
s_mainWindow->m_channelList->setCpuLoad(0);
|
||||
s_mainWindow->m_accountList->model()->setRadioState(0, false, false);
|
||||
s_mainWindow->m_accountList->setChannel(QString());
|
||||
s_mainWindow->m_messageList->clearDmCallRoute();
|
||||
s_mainWindow->m_messageList->clearStreams();
|
||||
if (s_mainWindow->m_streamPlayer) s_mainWindow->m_streamPlayer->close();
|
||||
|
||||
@@ -17,14 +17,14 @@ extern "C" {
|
||||
|
||||
static void applyPhoto(MemberListItem& item, const QString& channel) {
|
||||
auto* manager = AvatarManager::instance(); if (!manager) return;
|
||||
const QImage image = manager->image(channel, item.nodeId); if (image.isNull()) return;
|
||||
const QImage image = manager->image(channel, item.snapshot.node_id); if (image.isNull()) return;
|
||||
const int size = MemberListModel::kAvatarSize;
|
||||
QPixmap pixmap(size, size); pixmap.fill(Qt::transparent);
|
||||
QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing);
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
QPainterPath clip; clip.addEllipse(QRectF(0, 0, size, size)); painter.setClipPath(clip);
|
||||
painter.drawImage(QRect(0, 0, size, size), image); painter.setClipping(false);
|
||||
const QColor statusColor = item.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
const QColor statusColor = item.snapshot.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
painter.setPen(QPen(statusColor, 1)); painter.setBrush(statusColor);
|
||||
painter.drawEllipse(QRect(size - 9, size - 9, 8, 8)); painter.end(); item.avatar = pixmap;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ MemberListModel::MemberListModel(QObject* parent)
|
||||
if (auto* manager = AvatarManager::instance()) connect(manager, &AvatarManager::changed, this,
|
||||
[this](const QString& channel, quint64 owner) {
|
||||
if (channel != m_channelId) return;
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) if (it->nodeId == owner) {
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) if (it->snapshot.node_id == owner) {
|
||||
applyPhoto(*it, channel); emit dataChanged(index(it.key()), index(it.key()), {Qt::DecorationRole});
|
||||
}
|
||||
});
|
||||
@@ -60,61 +60,58 @@ QVariant MemberListModel::data(const QModelIndex& index, int role) const {
|
||||
if (role == Qt::DecorationRole) return QVariant::fromValue(QIcon(it->avatar));
|
||||
if (role == Qt::UserRole + 1) {
|
||||
QString sub;
|
||||
if (it->rtt >= 0) sub += QString("rtt=%1ms").arg(it->rtt / 10);
|
||||
if (it->natType >= 0) {
|
||||
if (it->snapshot.rtt != 0xffff) sub += QString("rtt=%1ms").arg(it->snapshot.rtt / 10);
|
||||
if (it->snapshot.nat_type != 0xff) {
|
||||
static const char* nat[] = {"NET","DIR","EIM","DEP","NAT?","CONTACT","EIM?"};
|
||||
const char* nt = (it->natType <= 6) ? nat[it->natType] : "?";
|
||||
const char* nt = (it->snapshot.nat_type <= 6) ? nat[it->snapshot.nat_type] : "?";
|
||||
if (!sub.isEmpty()) sub += " ";
|
||||
sub += nt;
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
if (role == Qt::ToolTipRole)
|
||||
return (it->online ? QCoreApplication::translate("ChatGui",
|
||||
return (it->snapshot.online ? QCoreApplication::translate("ChatGui",
|
||||
"online") : QCoreApplication::translate("ChatGui",
|
||||
"offline")) + " | 0x" +
|
||||
QString("%1").arg(it->nodeId, 16, 16, QChar('0')) +
|
||||
((it->flags & CHAT_MEMBER_FLAG_RADIO) ? QCoreApplication::translate("ChatGui", " · Radio enabled") : QString());
|
||||
if (role == Qt::UserRole + 2) return it->isSelf;
|
||||
if (role == Qt::UserRole + 3) return it->flags;
|
||||
if (role == Qt::UserRole + 4) return (bool)it->storage;
|
||||
if (role == RadioSpeakingRole) return it->isSelf ? m_selfTransmitting : m_radioTalkers.contains(it->nodeId);
|
||||
if (role == OnlineRole) return it->online;
|
||||
QString("%1").arg(it->snapshot.node_id, 16, 16, QChar('0')) +
|
||||
((it->snapshot.flags & CHAT_MEMBER_FLAG_RADIO) ? QCoreApplication::translate("ChatGui", " · Radio enabled") : QString());
|
||||
if (role == Qt::UserRole + 2) return it->snapshot.is_self;
|
||||
if (role == Qt::UserRole + 3) return it->snapshot.flags;
|
||||
if (role == Qt::UserRole + 4) return (bool)it->snapshot.storage;
|
||||
if (role == RadioSpeakingRole) return it->snapshot.is_self ? m_selfTransmitting : m_radioTalkers.contains(it->snapshot.node_id);
|
||||
if (role == OnlineRole) return it->snapshot.online;
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void MemberListModel::setChannel(const QString& channelId, bool isOwner) {
|
||||
beginResetModel();
|
||||
if (m_channelId != channelId) {
|
||||
beginResetModel();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: channel %s -> %s cleared talkers=%d self_tx=%d",
|
||||
qPrintable(m_channelId), qPrintable(channelId), int(m_radioTalkers.size()), m_selfTransmitting);
|
||||
m_radioTalkers.clear(); m_radioActive = false; m_selfTransmitting = false;
|
||||
m_totalCount = 0; m_cache.clear(); endResetModel();
|
||||
emit snapshotChanged(0);
|
||||
}
|
||||
m_channelId = channelId;
|
||||
m_isOwner = isOwner;
|
||||
m_totalCount = 0;
|
||||
m_cache.clear();
|
||||
endResetModel();
|
||||
m_channelId = channelId; m_isOwner = isOwner;
|
||||
refresh();
|
||||
}
|
||||
|
||||
if (!channelId.isEmpty()) {
|
||||
void MemberListModel::refresh() {
|
||||
if (!m_channelId.isEmpty() && gui_bridge_is_uasync_ready()) {
|
||||
struct chat_member_list_req* req = (struct chat_member_list_req*)u_calloc(1, sizeof(*req));
|
||||
if (!req) return;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list request allocation failed"); return; }
|
||||
req->inst = gui_bridge_get_inst();
|
||||
strncpy(req->ch_id, channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
|
||||
strncpy(req->ch_id, m_channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, req);
|
||||
}
|
||||
}
|
||||
|
||||
void MemberListModel::refresh() {
|
||||
if (!m_channelId.isEmpty()) setChannel(m_channelId, m_isOwner);
|
||||
}
|
||||
|
||||
void MemberListModel::refreshRtt() {
|
||||
if (m_channelId.isEmpty()) return;
|
||||
if (m_channelId.isEmpty() || !gui_bridge_is_uasync_ready()) return;
|
||||
struct chat_member_list_req* req = (struct chat_member_list_req*)u_calloc(1, sizeof(*req));
|
||||
if (!req) return;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member RTT request allocation failed"); return; }
|
||||
req->inst = gui_bridge_get_inst();
|
||||
strncpy(req->ch_id, m_channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, req);
|
||||
@@ -122,24 +119,25 @@ void MemberListModel::refreshRtt() {
|
||||
|
||||
quint64 MemberListModel::nodeIdAt(int row) const {
|
||||
auto it = m_cache.find(row);
|
||||
return it != m_cache.end() ? it->nodeId : 0;
|
||||
return it != m_cache.end() ? it->snapshot.node_id : 0;
|
||||
}
|
||||
|
||||
bool MemberListModel::isOnlineAt(int row) const {
|
||||
auto it = m_cache.find(row);
|
||||
return it != m_cache.end() ? it->online : false;
|
||||
return it != m_cache.end() ? it->snapshot.online : false;
|
||||
}
|
||||
|
||||
std::optional<chat_member_display> MemberListModel::member(quint64 nodeId) const {
|
||||
for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it)
|
||||
if (it->snapshot.node_id == nodeId) return it->snapshot;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static void deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
struct chat_member_display member;
|
||||
chat_core_deserialize_member(src, &member);
|
||||
item.nodeId = member.node_id; item.isSelf = member.is_self != 0;
|
||||
item.flags = member.flags; item.rtt = member.rtt == 0xffff ? -1 : member.rtt;
|
||||
item.natType = member.nat_type == 0xff ? -1 : member.nat_type;
|
||||
item.storage = member.storage; item.online = member.online != 0;
|
||||
item.name = QString::fromUtf8(member.name);
|
||||
chat_core_deserialize_member(src, &item.snapshot);
|
||||
item.name = QString::fromUtf8(item.snapshot.name);
|
||||
|
||||
QColor c = item.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
QColor c = item.snapshot.online ? QColor("#4CAF50") : QColor("#9E9E9E");
|
||||
const int size = MemberListModel::kAvatarSize;
|
||||
QPixmap pm(size, size); pm.fill(Qt::transparent);
|
||||
QPainter p(&pm);
|
||||
@@ -157,7 +155,10 @@ static void deserialize_member(const uint8_t* src, MemberListItem& item) {
|
||||
|
||||
void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen) {
|
||||
if (chId != m_channelId) return;
|
||||
if (count < 0 || count > dataLen / CHAT_MEMBER_DISPLAY_SIZE) return;
|
||||
if (count < 0 || dataLen < 0 || count != dataLen / CHAT_MEMBER_DISPLAY_SIZE ||
|
||||
dataLen % CHAT_MEMBER_DISPLAY_SIZE || (count && !data)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member list: invalid count=%d bytes=%d", count, dataLen); return;
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
m_totalCount = 0;
|
||||
@@ -168,11 +169,12 @@ void MemberListModel::onMemberList(const QString& chId, int count, const uint8_t
|
||||
MemberListItem item;
|
||||
deserialize_member(data + i * CHAT_MEMBER_DISPLAY_SIZE, item);
|
||||
applyPhoto(item, chId);
|
||||
if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) continue;
|
||||
if ((item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) continue;
|
||||
m_cache[row++] = item;
|
||||
}
|
||||
m_totalCount = row;
|
||||
endResetModel();
|
||||
emit snapshotChanged(0);
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data) {
|
||||
@@ -185,12 +187,12 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
|
||||
/* 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; }
|
||||
if (it->snapshot.node_id == item.snapshot.node_id) { row = it.key(); break; }
|
||||
}
|
||||
|
||||
if ((item.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) {
|
||||
if ((item.snapshot.flags & CHAT_MEMBER_FLAG_DELETED) && !m_isOwner) {
|
||||
/* deleted member — remove from non-owner's list */
|
||||
onMemberRemoved(chId, item.nodeId);
|
||||
onMemberRemoved(chId, item.snapshot.node_id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,6 +207,7 @@ void MemberListModel::onMemberUpdated(const QString& chId, const uint8_t* data)
|
||||
m_totalCount++;
|
||||
endInsertRows();
|
||||
}
|
||||
emit snapshotChanged(item.snapshot.node_id);
|
||||
}
|
||||
|
||||
void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) {
|
||||
@@ -214,13 +217,14 @@ void MemberListModel::onMemberRemoved(const QString& chId, uint64_t nodeId) {
|
||||
qPrintable(chId), (unsigned long long)nodeId);
|
||||
|
||||
for (auto it = m_cache.begin(); it != m_cache.end(); ++it) {
|
||||
if (it->nodeId == nodeId) {
|
||||
if (it->snapshot.node_id == nodeId) {
|
||||
int row = it.key();
|
||||
beginRemoveRows(QModelIndex(), row, row);
|
||||
m_cache.remove(row);
|
||||
for (int next = row + 1; next < m_totalCount; ++next) m_cache[next - 1] = m_cache.take(next);
|
||||
m_totalCount--;
|
||||
endRemoveRows();
|
||||
emit snapshotChanged(nodeId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -255,7 +259,7 @@ void MemberListModel::onRadioTalk(quint64 groupId, quint64 nodeId, bool on) {
|
||||
else m_radioTalkers.remove(nodeId);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "radio members: talk ch=%s node=%016llx on=%d talkers=%d",
|
||||
qPrintable(m_channelId), (unsigned long long)nodeId, on, int(m_radioTalkers.size()));
|
||||
for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it) if (it->nodeId == nodeId) {
|
||||
for (auto it = m_cache.cbegin(); it != m_cache.cend(); ++it) if (it->snapshot.node_id == nodeId) {
|
||||
emit dataChanged(index(it.key()), index(it.key()), {RadioSpeakingRole});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* Qt-модель участников канала: ленивый кэш строк, обновления CHAT_EVT_MEMBER_* и RTT.
|
||||
* События bridge применяются в GUI-потоке; данные строки доступны через Qt roles. */
|
||||
/* Qt-модель участников выбранного канала: полные снимки CHAT_EVT_MEMBER_* в GUI-потоке.
|
||||
* Список читает Qt roles, карточка — копию той же записи через member(nodeId). */
|
||||
#ifndef MEMBERLISTMODEL_H
|
||||
#define MEMBERLISTMODEL_H
|
||||
|
||||
@@ -12,17 +12,13 @@
|
||||
#include <QPixmap>
|
||||
#include <QTimer>
|
||||
#include <stdint.h>
|
||||
#include <optional>
|
||||
#include "../../../src/chat/chat_member.h"
|
||||
|
||||
struct MemberListItem {
|
||||
quint64 nodeId = 0;
|
||||
bool online = false;
|
||||
bool isSelf = false;
|
||||
QPixmap avatar;
|
||||
QString name;
|
||||
int rtt = -1;
|
||||
int natType = -1;
|
||||
uint8_t flags = 0;
|
||||
uint8_t storage = 0;
|
||||
chat_member_display snapshot{}; // полная запись выбранного канала; источник всех live-полей
|
||||
QPixmap avatar; // производное изображение строки
|
||||
QString name; // UTF-8 имя из snapshot для Qt
|
||||
};
|
||||
|
||||
class MemberListModel : public QAbstractListModel {
|
||||
@@ -44,6 +40,8 @@ public:
|
||||
void refreshRtt();
|
||||
quint64 nodeIdAt(int row) const;
|
||||
bool isOnlineAt(int row) const;
|
||||
/* Копия записи: указатели в row-cache нельзя сохранять через reset/обновления. */
|
||||
std::optional<chat_member_display> member(quint64 nodeId) const;
|
||||
|
||||
/* Called from bridge callbacks (GUI thread) */
|
||||
void onMemberList(const QString& chId, int count, const uint8_t* data, int dataLen);
|
||||
@@ -54,9 +52,11 @@ public:
|
||||
void setRadioState(quint64 groupId, bool active, bool transmitting);
|
||||
void onRadioTalk(quint64 groupId, quint64 nodeId, bool on);
|
||||
|
||||
signals:
|
||||
/* nodeId=0: полный снимок/смена канала; иначе изменился или удалён конкретный участник. */
|
||||
void snapshotChanged(quint64 nodeId);
|
||||
|
||||
private:
|
||||
void loadBatch(int from, int count);
|
||||
QPixmap makeAvatar(QColor color, QChar letter) const;
|
||||
|
||||
QString m_channelId;
|
||||
bool m_isOwner = false;
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/* Настоящие Qt-карточка и модель; очередь ядра задерживает ответы для проверки смены выбора. */
|
||||
#include "../src/accountlist.h"
|
||||
#include "../src/memberlistmodel.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include <QtTest>
|
||||
#include <QListView>
|
||||
#include <vector>
|
||||
extern "C" {
|
||||
#include "utun_instance.h"
|
||||
#include "chat/chat_member.h"
|
||||
#include "member_files/protocol.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
}
|
||||
|
||||
static UTUN_INSTANCE instance{};
|
||||
static std::vector<chat_member_detail_req> requests;
|
||||
static gui_member_detail_fn detailCallback;
|
||||
static bool ready = true;
|
||||
|
||||
extern "C" UTUN_INSTANCE* gui_bridge_get_inst() { return &instance; }
|
||||
extern "C" int gui_bridge_is_uasync_ready() { return ready; }
|
||||
extern "C" void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb) { detailCallback = cb; }
|
||||
extern "C" void gui_bridge_set_radio_mute_result_cb(gui_dm_messages_fn) {}
|
||||
extern "C" int gui_bridge_toggle_member_radio_mute(const char*, uint64_t) { return 0; }
|
||||
extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) {
|
||||
if (fn == chat_core_collect_member_detail_trampoline) requests.push_back(*static_cast<chat_member_detail_req*>(arg));
|
||||
u_free(arg);
|
||||
}
|
||||
|
||||
static QByteArray row(quint64 node, bool online = true, int up = 5, int rtt = 30) {
|
||||
chat_member_display member{};
|
||||
member.node_id = node; member.online = online; member.conn_presence = 5; member.conn_up = up;
|
||||
member.rtt = rtt; member.nat_type = 0xff; member.flags = CHAT_MEMBER_FLAG_PHONE_KNOWN;
|
||||
snprintf(member.name, sizeof(member.name), "peer%llu", (unsigned long long)node);
|
||||
QByteArray bytes(CHAT_MEMBER_DISPLAY_SIZE, 0);
|
||||
chat_core_serialize_member(&member, reinterpret_cast<uint8_t*>(bytes.data()));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static QByteArray reply(const chat_member_detail_req& req, const QList<quint64>& hops = {}, int result = 0) {
|
||||
QByteArray bytes(CHAT_MEMBER_DETAIL_HEADER_SIZE + hops.size() * 8, 0);
|
||||
auto* data = reinterpret_cast<uint8_t*>(bytes.data());
|
||||
mf_put64(data, req.request_id); mf_put64(data + 8, req.group_id); mf_put64(data + 16, req.node_id);
|
||||
data[24] = result; data[25] = result ? 0 : 7; data[28] = hops.size();
|
||||
for (int i = 0; i < hops.size(); i++) mf_put64(data + CHAT_MEMBER_DETAIL_HEADER_SIZE + i * 8, hops[i]);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static void deliver(const QByteArray& bytes) {
|
||||
if (detailCallback) detailCallback(reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
}
|
||||
|
||||
static void fill(AccountList& panel, const QString& channel, const QByteArray& bytes) {
|
||||
panel.setChannel(channel);
|
||||
panel.onMemberList(channel, bytes.size() / CHAT_MEMBER_DISPLAY_SIZE,
|
||||
reinterpret_cast<const uint8_t*>(bytes.constData()), bytes.size());
|
||||
}
|
||||
|
||||
class MemberDetailTest : public QObject {
|
||||
Q_OBJECT
|
||||
static QString body(AccountList& p) { return p.findChild<QTextEdit*>("memberDetailText")->toPlainText(); }
|
||||
static QString header(AccountList& p) { return p.findChild<QLabel*>("memberDetailHeader")->text(); }
|
||||
static bool select(AccountList& p, int rowNumber) {
|
||||
return QMetaObject::invokeMethod(&p, "onMemberClicked", Qt::DirectConnection, Q_ARG(QModelIndex, p.model()->index(rowNumber)));
|
||||
}
|
||||
private slots:
|
||||
void init() { requests.clear(); ready = true; debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); }
|
||||
|
||||
void modelOwnsPresenceAndRttAndRefreshPreservesSelection() {
|
||||
AccountList panel; panel.resize(600, 800); panel.show();
|
||||
fill(panel, "123", row(7, false, 0, 0xffff) + row(8));
|
||||
QVERIFY(select(panel, 0)); QCOMPARE(requests.size(), size_t(1));
|
||||
deliver(reply(requests.back())); // Физический транспорт UP не делает участника онлайн.
|
||||
QVERIFY(header(panel).contains("#9E9E9E")); QVERIFY(body(panel).contains("offline"));
|
||||
const auto live = row(7, true, 4, 93);
|
||||
panel.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(live.constData()));
|
||||
QVERIFY(header(panel).contains("#4CAF50")); QVERIFY(body(panel).contains("rtt=9.3ms"));
|
||||
deliver(reply(requests.back(), {7, 9})); QVERIFY(body(panel).contains("0000000000000009"));
|
||||
const auto other = row(7, false, 0, 0xffff);
|
||||
panel.onMemberUpdated("456", reinterpret_cast<const uint8_t*>(other.constData()));
|
||||
QVERIFY(header(panel).contains("#4CAF50"));
|
||||
panel.refresh(); QCOMPARE(panel.model()->rowCount(), 2); QVERIFY(body(panel).contains("rtt=9.3ms"));
|
||||
const auto reordered = row(8) + live;
|
||||
panel.onMemberList("123", 2, reinterpret_cast<const uint8_t*>(reordered.constData()), reordered.size());
|
||||
QVERIFY(header(panel).contains("peer7"));
|
||||
QCOMPARE(panel.findChild<QListView*>()->currentIndex().row(), 1);
|
||||
}
|
||||
|
||||
void aToBToADiscardsOldResponsesWithoutClearingNewRequest() {
|
||||
AccountList panel; panel.show(); fill(panel, "123", row(7) + row(8));
|
||||
QVERIFY(select(panel, 0)); const auto first = requests.back();
|
||||
QVERIFY(select(panel, 1)); const auto second = requests.back();
|
||||
QVERIFY(select(panel, 0)); const auto third = requests.back();
|
||||
QVERIFY(first.request_id < second.request_id && second.request_id < third.request_id);
|
||||
deliver(reply(first, {111})); deliver(reply(second, {222}));
|
||||
QVERIFY(!body(panel).contains("BGP:"));
|
||||
auto wrongGroup = third; wrongGroup.group_id = 456;
|
||||
deliver(reply(wrongGroup, {333})); QVERIFY(!body(panel).contains("BGP:"));
|
||||
deliver(reply(third, {7, 444})); QVERIFY(body(panel).contains("00000000000001bc"));
|
||||
const auto pending = requests.size();
|
||||
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); QCOMPARE(requests.size(), pending + 1);
|
||||
const auto oldChannel = requests.back();
|
||||
fill(panel, "456", row(7)); QVERIFY(select(panel, 0)); const auto current = requests.back();
|
||||
deliver(reply(oldChannel, {555})); QVERIFY(!body(panel).contains("BGP:"));
|
||||
deliver(reply(current, {7, 666})); QVERIFY(body(panel).contains("000000000000029a"));
|
||||
}
|
||||
|
||||
void newMemberEventInvalidatesPendingPathEvenWhenFieldsAreEqual() {
|
||||
AccountList panel; panel.show(); const auto initial = row(7);
|
||||
fill(panel, "123", initial); QVERIFY(select(panel, 0)); const auto first = requests.back();
|
||||
panel.onMemberUpdated("123", reinterpret_cast<const uint8_t*>(initial.constData()));
|
||||
QCOMPARE(requests.size(), size_t(1));
|
||||
deliver(reply(first, {111})); QCOMPARE(requests.size(), size_t(2)); QVERIFY(!body(panel).contains("BGP:"));
|
||||
const auto correct = reply(requests.back(), {7, 8});
|
||||
deliver(correct.left(correct.size() - 1)); QVERIFY(!body(panel).contains("BGP:"));
|
||||
deliver(correct); QVERIFY(body(panel).contains("0000000000000008"));
|
||||
panel.onMemberRemoved("123", 7); QVERIFY(!header(panel).contains("peer7"));
|
||||
deliver(correct); QVERIFY(!body(panel).contains("BGP:"));
|
||||
}
|
||||
|
||||
void errorTimeoutAndHiddenPanelReleaseOnlyTheirOwnRequests() {
|
||||
AccountList panel; panel.show(); fill(panel, "123", row(7)); QVERIFY(select(panel, 0));
|
||||
deliver(reply(requests.back(), {}, CHAT_MEMBER_DETAIL_NOT_MEMBER));
|
||||
QVERIFY(header(panel).contains("#4CAF50")); // Диагностическая ошибка не меняет live-снимок.
|
||||
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); const auto expired = requests.back();
|
||||
QTRY_VERIFY_WITH_TIMEOUT(requests.back().request_id != expired.request_id, 6500);
|
||||
const auto current = requests.back(); deliver(reply(expired, {111}));
|
||||
deliver(reply(current, {7, 8})); QVERIFY(body(panel).contains("BGP:"));
|
||||
QVERIFY(QMetaObject::invokeMethod(&panel, "onDetailTimerTick")); const auto hidden = requests.back();
|
||||
panel.hide(); deliver(reply(hidden, {222})); panel.show();
|
||||
QVERIFY(requests.back().request_id > hidden.request_id);
|
||||
deliver(reply(hidden, {222})); QVERIFY(!body(panel).contains("00000000000000de"));
|
||||
ready = false; panel.setChannel(QString()); deliver(reply(requests.back(), {333}));
|
||||
QCOMPARE(panel.model()->rowCount(), 0); QVERIFY(!body(panel).contains("BGP:"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(MemberDetailTest)
|
||||
#include "test_member_detail.moc"
|
||||
@@ -23,6 +23,16 @@ private slots:
|
||||
void initTestCase() { gui_bridge_init(); }
|
||||
void cleanup() { debug_disable_file_output(); }
|
||||
|
||||
void routeChangesStillNotifyPmPresence() {
|
||||
static int notifications;
|
||||
notifications = 0;
|
||||
gui_bridge_set_network_changed_cb([](int type) { if (type == GUI_EVT_NODEINFO_UPDATE) notifications++; });
|
||||
TOPO_GROUP_NODE peer{}; peer.node_id = 7;
|
||||
gui_nodeinfo_cb_impl(nullptr, &peer, nullptr);
|
||||
QTRY_COMPARE(notifications, 1);
|
||||
gui_bridge_set_network_changed_cb(nullptr);
|
||||
}
|
||||
|
||||
void services_data() {
|
||||
QTest::addColumn<QByteArray>("settings");
|
||||
QTest::addColumn<bool>("withTun");
|
||||
|
||||
@@ -11,6 +11,7 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "../../../src/chat/chat_member.h"
|
||||
|
||||
struct UASYNC;
|
||||
struct TOPO_GROUP;
|
||||
@@ -32,14 +33,13 @@ struct UTUN_INSTANCE;
|
||||
#define GUI_EVT_NODE_CHANGED 11 /* data: [node_id:8] */
|
||||
#define GUI_EVT_ATTACHMENT_DOWNLOADED 14 /* data: [ch_id_len:1][ch_id:var][msg_id:8] */
|
||||
#define GUI_EVT_DOWNLOAD_PROGRESS 15 /* data: [ch_id_len:1][ch_id:var][msg_id:8][blocks_done:4][num_blocks:4] */
|
||||
#define GUI_EVT_NODEINFO_UPDATE 16 /* data: node_id:8 conn_presence:1 conn_up:1 best_rtt:2(0.1ms,0xFFFF=nodata) */
|
||||
#define GUI_EVT_NODEINFO_UPDATE 16 /* Сигнал изменения маршрутов для PM presence; без live-полей карточки. */
|
||||
#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_DETAIL 22 /* Адресованный ответ диагностики; codec в chat_member.h. */
|
||||
#define GUI_EVT_MEMBER_LIST 23 /* data: [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */
|
||||
#define GUI_EVT_MEMBER_UPDATED 24 /* data: [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */
|
||||
#define GUI_EVT_MEMBER_REMOVED 25 /* data: [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
|
||||
#define GUI_EVT_BGP_INFO 26 /* data: text — BGP hop summary for member detail */
|
||||
#define GUI_EVT_ADMIN_KEY_RECEIVED 28 /* data: [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ */
|
||||
#define GUI_EVT_INVITE_LINK_READY 29 /* data: [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
|
||||
#define GUI_EVT_INVITE_CANDIDATES 30 /* data: [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
|
||||
@@ -57,12 +57,6 @@ struct UTUN_INSTANCE;
|
||||
#define GUI_EVT_RADIO_TALK 43 /* data: [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */
|
||||
#define GUI_EVT_CALL_CONNECTION 44 /* data: [call_id:8][CONN_TYPE:1][next_hop_node_id:8] */
|
||||
|
||||
/* Размеры элементов пакета GUI_EVT_MEMBER_DETAIL (должны совпадать с chat_status.c) */
|
||||
#define MEMBER_DETAIL_LINK_SIZE 64
|
||||
#define MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define MEMBER_DETAIL_SOCK_SIZE 48
|
||||
|
||||
/* ── API ── */
|
||||
|
||||
/* Инициализация моста (вызывается из GUI при старте) */
|
||||
@@ -134,10 +128,6 @@ void gui_bridge_set_attachment_downloaded_cb(gui_attachment_downloaded_fn cb);
|
||||
typedef void (*gui_download_progress_fn)(const char* ch_id, int ch_id_len, int64_t msg_id, int blocks_done, int num_blocks);
|
||||
void gui_bridge_set_download_progress_cb(gui_download_progress_fn cb);
|
||||
|
||||
/* Callback для обновления статуса узла (nodeinfo) — data: 12B packed */
|
||||
typedef void (*gui_nodeinfo_update_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_nodeinfo_update_cb(gui_nodeinfo_update_fn cb);
|
||||
|
||||
/* Callback для входящих приглашений (вызывается из GUI-потока)
|
||||
data: [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */
|
||||
typedef void (*gui_invite_received_fn)(const uint8_t* data, int len);
|
||||
@@ -152,7 +142,7 @@ 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 */
|
||||
data: см. CHAT_MEMBER_DETAIL_* в chat_member.h; проверка целого пакета до применения. */
|
||||
typedef void (*gui_member_detail_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_member_detail_cb(gui_member_detail_fn cb);
|
||||
|
||||
@@ -171,11 +161,6 @@ void gui_bridge_set_member_updated_cb(gui_member_updated_fn cb);
|
||||
typedef void (*gui_member_removed_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb);
|
||||
|
||||
/* Callback для BGP hop info текста (вызывается из GUI-потока)
|
||||
data: текстовая строка формата "rtt:12ms hop:3 via bgp: [ABCD] name1 [EF12] name2" */
|
||||
typedef void (*gui_bgp_info_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb);
|
||||
|
||||
/* Callback: получен канальный приватный ключ (стал совладельцем) — data: [ch_id_len:1][ch_id][from_node_id:8] */
|
||||
typedef void (*gui_admin_key_received_fn)(const uint8_t* data, int len);
|
||||
void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb);
|
||||
|
||||
@@ -41,7 +41,6 @@ static gui_status_refresh_fn g_status_refresh_cb = nullptr;
|
||||
static gui_cpu_load_fn g_cpu_load_cb = nullptr;
|
||||
static gui_attachment_downloaded_fn g_attachment_downloaded_cb = nullptr;
|
||||
static gui_download_progress_fn g_download_progress_cb = nullptr;
|
||||
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;
|
||||
@@ -49,7 +48,6 @@ 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 gui_bgp_info_fn g_bgp_info_cb = nullptr;
|
||||
static gui_admin_key_received_fn g_admin_key_received_cb = nullptr;
|
||||
static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr;
|
||||
static gui_invite_candidates_fn g_invite_candidates_cb = nullptr;
|
||||
@@ -272,9 +270,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case GUI_EVT_NODEINFO_UPDATE:
|
||||
if (g_nodeinfo_update_cb) g_nodeinfo_update_cb(d, dlen);
|
||||
break;
|
||||
case GUI_EVT_NODEINFO_UPDATE: break; /* network_changed уже уведомлён выше. */
|
||||
case 18: /* CHAT_EVT_INVITE_RECEIVED */
|
||||
if (g_invite_received_cb) g_invite_received_cb(d, dlen);
|
||||
break;
|
||||
@@ -296,9 +292,7 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
|
||||
case GUI_EVT_MEMBER_REMOVED:
|
||||
if (g_member_removed_cb) g_member_removed_cb(d, dlen);
|
||||
break;
|
||||
case GUI_EVT_BGP_INFO:
|
||||
if (g_bgp_info_cb) g_bgp_info_cb(d, dlen);
|
||||
break;
|
||||
|
||||
case GUI_EVT_ADMIN_KEY_RECEIVED:
|
||||
if (dlen >= 2) {
|
||||
uint8_t chLen = d[0];
|
||||
@@ -552,9 +546,6 @@ void gui_bridge_set_download_progress_cb(gui_download_progress_fn cb) {
|
||||
g_download_progress_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_nodeinfo_update_cb(gui_nodeinfo_update_fn cb) {
|
||||
g_nodeinfo_update_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_invite_received_cb(gui_invite_received_fn cb) {
|
||||
g_invite_received_cb = cb;
|
||||
@@ -584,9 +575,6 @@ void gui_bridge_set_member_removed_cb(gui_member_removed_fn cb) {
|
||||
g_member_removed_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_bgp_info_cb(gui_bgp_info_fn cb) {
|
||||
g_bgp_info_cb = cb;
|
||||
}
|
||||
|
||||
void gui_bridge_set_admin_key_received_cb(gui_admin_key_received_fn cb) {
|
||||
g_admin_key_received_cb = cb;
|
||||
@@ -696,21 +684,8 @@ void gui_bridge_set_attachment_preparation_cb(gui_dm_messages_fn cb) {
|
||||
/* ── uTun nodeinfo callback (called from uasync thread) ── */
|
||||
|
||||
extern "C" void gui_nodeinfo_cb_impl(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq, void* arg) {
|
||||
(void)arg;
|
||||
if (!nq) return;
|
||||
uint16_t best_rtt = topo_get_chain_rtt(nq);
|
||||
uint8_t hop_count = 0;
|
||||
topo_node_best_hop_list(nq, &hop_count, NULL);
|
||||
uint8_t data[16];
|
||||
memcpy(data, &nq->node_id, 8);
|
||||
data[8] = nq->conn_presence;
|
||||
data[9] = nq->conn_up;
|
||||
memcpy(data + 10, &best_rtt, 2);
|
||||
data[12] = hop_count;
|
||||
data[13] = 0;
|
||||
uint16_t bgp_nodes = group ? (uint16_t)queue_entry_count(group->nodes) : 0;
|
||||
memcpy(data + 14, &bgp_nodes, 2);
|
||||
gui_bridge_post(GUI_EVT_NODEINFO_UPDATE, data, sizeof(data));
|
||||
(void)group; (void)arg;
|
||||
if (nq) gui_bridge_post(GUI_EVT_NODEINFO_UPDATE, nullptr, 0);
|
||||
}
|
||||
|
||||
#include "gui_bridge_impl.moc"
|
||||
|
||||
Reference in new issue
Block a user