Files
utun2/src/chat/chat_member.c
T

417 lines
20 KiB
C

/*
* chat_member.c — единый код запроса мемберов для GUI
*
* Используется: chatgui (desktop), headless CLI, Android.
* Live-состояние участников читается только в uasync-потоке.
*/
#include "chat_core_priv.h"
#include "chat_member.h"
#include "chat_event.h"
#include "member_sync.h"
#include "../member_files/protocol.h"
#include "../utun_instance.h"
#include "../routing_layer/topo_group.h"
#include "../radio/radio.h"
#include "../call/call.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../../lib/json_flat.h"
#include "../../lib/platform_compat.h"
#include <stdlib.h>
/* ─── live-онлайн: тот же живой путь, которым пользуется маршрутизатор ─── */
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->inst || !ch_id || !ch_id[0]) return 0;
if (node_id == cc->my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
if (!cc->inst->topo_groups) return 0;
uint64_t group_id = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(cc->inst->topo_groups, group_id);
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
return topo_node_best_path(topo_node_find_by_id(g, node_id)) != NULL;
}
/* Рация — живая подписка на конкретную CHAT-группу; у self нет записи в group->nodes. */
static uint8_t member_radio_flag(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
if (!inst || !inst->topo_groups || !ch_id || !ch_id[0]) return 0;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, strtoull(ch_id, NULL, 10));
if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
uint8_t status = 0, flags = 0;
if (node_id == inst->node_id) {
status = call_phone_status(inst);
flags = group->radio_active ? CHAT_MEMBER_FLAG_RADIO : 0;
}
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(group, node_id);
if (node_id != inst->node_id && node) {
status = node->phone_status;
flags = node->radio ? CHAT_MEMBER_FLAG_RADIO : 0;
}
if (status & TOPO_FLAG_PHONE_KNOWN) flags |= CHAT_MEMBER_FLAG_PHONE_KNOWN;
if (status & TOPO_FLAG_PHONE_BUSY) flags |= CHAT_MEMBER_FLAG_PHONE_BUSY;
return flags;
}
/* ─── парсинг 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) {
mf_put64(dst, src->node_id);
dst[8] = src->online;
dst[9] = src->is_self;
dst[10] = src->flags;
dst[11] = (uint8_t)src->rtt; dst[12] = (uint8_t)(src->rtt >> 8);
dst[13] = src->nat_type;
dst[14] = src->storage;
/* Поле на проводе всегда завершается NUL и не содержит хвост памяти исходной строки. */
strncpy((char*)dst + 15, src->name, sizeof(src->name) - 1);
dst[78] = 0;
mf_put64(dst + 79, src->avatar_server);
memcpy(dst + 87, src->avatar_file, 70);
dst[157] = src->storage_role;
dst[158] = src->verified; dst[159] = src->node_type;
dst[160] = src->conn_presence; dst[161] = src->conn_up;
dst[162] = (uint8_t)src->intermediaries; dst[163] = (uint8_t)(src->intermediaries >> 8);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: serialized node=%016llx name_bytes=%zu zero_padded=1",
(unsigned long long)src->node_id, strlen((const char*)dst + 15));
}
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst) {
if (!src || !dst) return -1;
dst->node_id = mf_get64(src);
dst->online = src[8];
dst->is_self = src[9];
dst->flags = src[10];
dst->rtt = (uint16_t)src[11] | ((uint16_t)src[12] << 8);
dst->nat_type = src[13];
dst->storage = src[14];
memcpy(dst->name, src + 15, 64);
dst->name[63] = '\0';
dst->avatar_server = mf_get64(src + 79);
memcpy(dst->avatar_file, src + 87, 70); dst->avatar_file[69] = 0;
dst->storage_role = src[157];
dst->verified = src[158]; dst->node_type = src[159];
dst->conn_presence = src[160]; dst->conn_up = src[161];
dst->intermediaries = (uint16_t)src[162] | ((uint16_t)src[163] << 8);
return 0;
}
static void member_avatar(struct chat_member_display* m, const char* userinfo) {
m->avatar_server = 0; memset(m->avatar_file, 0, sizeof(m->avatar_file));
char server[MS_USERINFO_MAX + 1], file[MS_USERINFO_MAX + 1];
if (!userinfo || json_flat_get(userinfo, "avatar_server", server, sizeof(server)) < 0 ||
json_flat_get(userinfo, "avatar_file", file, sizeof(file)) < 0) return;
if (strlen(server) != 16 || !mf_avatar_manifest_valid(file)) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "avatar: invalid profile reference node=%016llx", (unsigned long long)m->node_id); return;
}
for (int i = 0; i < 16; i++) if (!((server[i] >= '0' && server[i] <= '9') || (server[i] >= 'a' && server[i] <= 'f'))) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "avatar: invalid server ID node=%016llx", (unsigned long long)m->node_id); return;
}
m->avatar_server = strtoull(server, NULL, 16);
if (m->avatar_server) memcpy(m->avatar_file, file, 70);
}
static int member_storage_role(const char* tags, sqlite3_stmt* st, int source, int signature, int deleted) {
char role[16];
return !deleted && sqlite3_column_int(st, source) == 0 && sqlite3_column_bytes(st, signature) == 64 &&
tags && json_flat_get(tags, "storage", role, sizeof(role)) == 0 && !strcmp(role, "yes");
}
/* Один SELECT и одна функция заполнения для списка и одиночного обновления. */
static void member_from_row(struct UTUN_INSTANCE* inst, const char* ch_id, sqlite3_stmt* st, struct chat_member_display* m) {
memset(m, 0, sizeof(*m));
m->node_id = (uint64_t)sqlite3_column_int64(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* tags = (const char*)sqlite3_column_text(st, 3);
m->storage = (uint8_t)sqlite3_column_int(st, 4);
m->storage_role = member_storage_role(tags, st, 7, 8, sqlite3_column_int(st, 5));
m->is_self = m->node_id == inst->node_id;
m->flags = parse_adm_tags_flags(tags) | member_radio_flag(inst, ch_id, m->node_id);
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
member_avatar(m, (const char*)sqlite3_column_text(st, 6));
char verified[16];
m->verified = tags && json_flat_get(tags, "verified", verified, sizeof(verified)) == 0 && !strcmp(verified, "yes");
m->node_type = (uint8_t)sqlite3_column_int(st, 10);
m->nat_type = (uint8_t)sqlite3_column_int(st, 11);
m->rtt = UINT16_MAX;
struct TOPO_GROUP* group = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ch_id, NULL, 10)) : NULL;
struct TOPO_GROUP_NODE* node = group ? topo_node_find_by_id(group, m->node_id) : NULL;
if (node) {
m->conn_presence = node->conn_presence; m->conn_up = node->conn_up;
struct TOPO_NODEPATH* path = topo_node_best_path(node);
if (path) {
m->rtt = topo_get_chain_rtt(node);
m->intermediaries = path->hop_count > 0 ? path->hop_count - 1 : 0;
m->online = 1;
}
}
if (m->is_self) m->online = 1;
if (m->flags & CHAT_MEMBER_FLAG_DELETED) m->online = 0;
if (!m->online) m->flags &= ~(CHAT_MEMBER_FLAG_PHONE_KNOWN | CHAT_MEMBER_FLAG_PHONE_BUSY);
const char* name = local_nick && local_nick[0] ? local_nick : node_name;
if (name && name[0]) snprintf(m->name, sizeof(m->name), "%s", name);
else snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
}
static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int single, uint64_t node_id,
uint8_t** out, int* count) {
struct chat_core_ctx* cc = CC(inst);
*out = NULL; *count = 0;
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: invalid instance/channel"); return -1;
}
char table[80]; peers_table_name(ch_id, table, sizeof(table));
char sql[1536];
snprintf(sql, sizeof(sql),
"SELECT p.node_id,COALESCE(n.name,''),COALESCE(p.local_nick,''),COALESCE(p.adm_tags,''),"
"p.storage,p.deleted,p.userinfo,p.source,p.adm_tags_sig,p.radio_muted,p.node_type,"
"(SELECT CASE WHEN COUNT(*)=0 THEN 255 WHEN MAX(addr_type=1) THEN 1 WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2 WHEN MAX(addr_type=6) THEN 6 WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3 ELSE 0 END FROM node_addresses WHERE node_id=p.node_id)"
" FROM \"%s\" p LEFT JOIN nodes n ON n.node_id=p.node_id %s ORDER BY p.node_id ASC",
table, single ? "WHERE p.node_id=?" : "");
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK && single) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: query ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
sqlite3_finalize(st); return -1;
}
struct chat_member_display* members = NULL;
int cap = 0, cnt = 0;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
if (cnt == cap) {
cap = cap ? cap * 2 : (single ? 1 : 32);
struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members));
if (!tmp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation ch=%s count=%d", ch_id, cnt);
u_free(members); sqlite3_finalize(st); return -1;
}
members = tmp;
}
member_from_row(inst, ch_id, st, &members[cnt++]);
}
sqlite3_finalize(st);
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: read ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
u_free(members); return -1;
}
for (int i = 1; i < cnt; i++) {
struct chat_member_display key = members[i];
int j = i - 1;
while (j >= 0 && (members[j].online < key.online ||
(members[j].online == key.online && members[j].node_id > key.node_id))) {
members[j + 1] = members[j]; j--;
}
members[j + 1] = key;
}
if (cnt) {
*out = u_malloc((size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE);
if (!*out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: wire allocation ch=%s count=%d", ch_id, cnt);
u_free(members); return -1;
}
for (int i = 0; i < cnt; i++) chat_core_serialize_member(&members[i], *out + i * CHAT_MEMBER_DISPLAY_SIZE);
}
u_free(members); *count = cnt;
return 0;
}
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
if (!out || !count) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null output"); return -1; }
return member_query(inst, ch_id, 0, 0, out, count);
}
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out) {
if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null single output"); return -1; }
uint8_t* wire = NULL; int count = 0;
if (member_query(inst, ch_id, 1, node_id, &wire, &count) < 0) return -1;
if (count != 1) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: absent ch=%s node=%016llx", ch_id, (unsigned long long)node_id);
u_free(wire); return -1;
}
memcpy(out, wire, CHAT_MEMBER_DISPLAY_SIZE); u_free(wire);
return 0;
}
int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
uint8_t member[CHAT_MEMBER_DISPLAY_SIZE];
if (!ch_id || !ch_id[0] || strlen(ch_id) >= 64 || chat_core_get_single_member(inst, ch_id, node_id, member) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "radio: mute toggle member not found node=%016llx", (unsigned long long)node_id); return -1;
}
int muted = !(member[10] & CHAT_MEMBER_FLAG_RADIO_MUTED);
if (radio_set_member_muted(inst, strtoull(ch_id, NULL, 10), node_id, muted) < 0) return -1;
if (chat_core_get_single_member(inst, ch_id, node_id, member) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: cannot publish local mute ch=%s node=%016llx",
ch_id, (unsigned long long)node_id); return -1;
}
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;
memcpy(evt + 2 + cl, member, sizeof(member));
chat_event_post(inst, CHAT_EVT_MEMBER_UPDATED, evt, (int)(cl + 2 + sizeof(member)));
return 0;
}
void chat_core_toggle_member_radio_mute_trampoline(void* arg) {
struct chat_member_radio_mute_req* req = arg;
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: null member mute request"); return; }
int rc = chat_core_toggle_member_radio_mute(req->inst, req->ch_id, req->node_id);
uint8_t result[17];
mf_put64(result, strtoull(req->ch_id, NULL, 10)); mf_put64(result + 8, req->node_id); result[16] = rc == 0;
chat_event_post(req->inst, CHAT_EVT_RADIO_MUTE_RESULT, result, sizeof(result));
u_free(req);
}
/* ─── трамплины для gui_bridge ─── */
void chat_core_request_member_list_trampoline(void* arg) {
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
struct UTUN_INSTANCE* inst = req->inst;
char ch_id[64]; strncpy(ch_id, req->ch_id, 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(inst, ch_id, &members, &count) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build list ch=%s", ch_id); 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) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: list event allocation failed ch=%s count=%d", ch_id, count);
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);
if (count) memcpy(evt + 1 + ch_len + 2, members, (size_t)count * CHAT_MEMBER_DISPLAY_SIZE);
int radios = 0;
for (int i = 0; i < count; i++) radios += !!(members[i * CHAT_MEMBER_DISPLAY_SIZE + 10] & CHAT_MEMBER_FLAG_RADIO);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: list ch=%s count=%d radios=%d", ch_id, count, radios);
chat_event_post(inst, CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz);
u_free(evt);
u_free(members);
}
void chat_core_request_member_rtt_trampoline(void* arg) {
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
struct UTUN_INSTANCE* inst = req->inst;
char ch_id[64]; strncpy(ch_id, req->ch_id, 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(inst, 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 мог измениться у каждого) */
for (int i = 0; i < count; i++) {
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(inst, CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz);
}
u_free(evt);
u_free(members);
}
/* ─── node_props_changed callback (BGP-события узла → MEMBER_UPDATED в GUI) ─── */
static void on_member_props_changed(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)adm_tags;
if (!cc || !cc->initialized || !cc->inst || !cc->db || !channel_id || !channel_id[0]) return;
char peers_tbl[80]; peers_table_name(channel_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(channel_id);
uint8_t evt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, channel_id, cl);
evt[1 + cl] = 1;
if (chat_core_get_single_member(inst, channel_id, node_id, evt + 1 + cl + 1) == 0) {
const uint8_t* member = evt + 1 + cl + 1;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: update ch=%s node=%016llx online=%u self=%u radio=%u",
channel_id, (unsigned long long)node_id, member[8], member[9], !!(member[10] & CHAT_MEMBER_FLAG_RADIO));
chat_event_post(inst, CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: cannot build update ch=%s node=%016llx",
channel_id, (unsigned long long)node_id);
}
return;
}
sqlite3_finalize(st);
}
}
static void on_member_route_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, void* arg) {
if (!group || !node || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0]) return;
on_member_props_changed(node->node_id, NULL, group->channel_id, arg);
}
void chat_member_init(struct UTUN_INSTANCE* inst) {
member_sync_add_props_cbk(inst, on_member_props_changed, inst);
utun_add_nodeinfo_cbk(inst, on_member_route_changed, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (group properties/routes -> complete MEMBER_UPDATED)");
}
void chat_member_destroy(struct UTUN_INSTANCE* inst) {
utun_remove_nodeinfo_cbk(inst, on_member_route_changed, inst);
member_sync_remove_props_cbk(inst, on_member_props_changed, inst);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: properties/routes subscriptions removed");
}