38 changed files with 1276 additions and 541 deletions
@ -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)"); |
||||
} |
||||
@ -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 */ |
||||
Loading…
Reference in new issue