503 lines
22 KiB
C
503 lines
22 KiB
C
/*
|
|
* invite_build.c — сборка invite-ссылок utun:// (выбор узла + адреса)
|
|
*
|
|
* Общий модуль для всех GUI (desktop chatgui, Android, headless CLI).
|
|
* Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge.
|
|
* Работает в uasync-потоке, использует per-instance контекст.
|
|
*/
|
|
#include "invite_build.h"
|
|
#include "invite_link.h"
|
|
#include "chat_core_priv.h"
|
|
#include "chat_event.h"
|
|
#include "chat_join.h"
|
|
|
|
#include "../routing_layer/topo_group.h"
|
|
#include "../routing_layer/topo_node.h"
|
|
#include "../utun_instance.h"
|
|
#include "../transport_layer/etcp_connections.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/platform_compat.h"
|
|
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <openssl/rand.h>
|
|
|
|
/* ── живость узла в канале (BGP/прямое/через посредника) ── */
|
|
|
|
static struct TOPO_GROUP* resolve_chat_group(struct UTUN_INSTANCE* inst, uint64_t channel_id) {
|
|
if (!inst || !inst->topo_groups) return NULL;
|
|
struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, channel_id);
|
|
if (!grp || grp->group_type != TOPO_GROUP_TYPE_CHAT) return NULL;
|
|
return grp;
|
|
}
|
|
|
|
static int node_is_alive(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* grp, uint64_t node_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (cc && node_id == cc->my_node_id) return 1; /* self всегда доступен */
|
|
if (!grp) return 0;
|
|
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, node_id);
|
|
if (!nq) return 0;
|
|
return (nq->conn_presence & (NCONN_DIRECT | NCONN_INDIRECT | NCONN_BGP)) != 0;
|
|
}
|
|
|
|
/* есть ли у узла публичные адреса v4 и v6 (addr_type=DIRECT) */
|
|
static int node_has_both_public(struct UTUN_INSTANCE* inst, uint64_t node_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
int has_v4 = 0, has_v6 = 0;
|
|
sqlite3_stmt* st = NULL;
|
|
char sql[256];
|
|
snprintf(sql, sizeof(sql),
|
|
"SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT);
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
|
int fam = sqlite3_column_int(st, 0);
|
|
if (fam == 4) has_v4 = 1;
|
|
else if (fam == 6) has_v6 = 1;
|
|
}
|
|
sqlite3_finalize(st);
|
|
return (has_v4 && has_v6) ? 1 : 0;
|
|
}
|
|
|
|
/* ── Фаза 1-2: лучший достижимый узел в канале ── */
|
|
|
|
static uint64_t pick_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both_public, int* out_priority) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
uint64_t best_nid = 0;
|
|
int best_priority = 4; /* 1=super, 2=regular, 3=self, 4=invalid */
|
|
int best_has_both = 0;
|
|
|
|
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
|
|
if (!grp) {
|
|
*out_has_both_public = 0;
|
|
*out_priority = best_priority;
|
|
return 0;
|
|
}
|
|
|
|
char ch_str[32];
|
|
snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)channel_id);
|
|
char peers_tbl[80];
|
|
peers_table_name(ch_str, peers_tbl, sizeof(peers_tbl));
|
|
|
|
char sql[512];
|
|
snprintf(sql, sizeof(sql), "SELECT node_id, node_type FROM \"%s\" ORDER BY node_id", peers_tbl);
|
|
sqlite3_stmt* pst = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &pst, NULL) != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: pick_best — query failed ch=%llu err=%s",
|
|
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
|
|
*out_has_both_public = 0;
|
|
*out_priority = best_priority;
|
|
return 0;
|
|
}
|
|
|
|
while (sqlite3_step(pst) == SQLITE_ROW) {
|
|
uint64_t nid = (uint64_t)sqlite3_column_int64(pst, 0);
|
|
int ntype = sqlite3_column_int(pst, 1);
|
|
if (!node_is_alive(inst, grp, nid)) continue;
|
|
|
|
int has_both = node_has_both_public(inst, nid);
|
|
|
|
int priority;
|
|
if (ntype == 4) priority = 1; /* supernode, включая self */
|
|
else if (nid == cc->my_node_id) priority = 3;
|
|
else priority = 2;
|
|
|
|
if (priority <= best_priority) {
|
|
best_nid = nid;
|
|
best_priority = priority;
|
|
best_has_both = has_both;
|
|
}
|
|
}
|
|
sqlite3_finalize(pst);
|
|
|
|
*out_has_both_public = best_has_both;
|
|
*out_priority = best_priority;
|
|
return best_nid;
|
|
}
|
|
|
|
/* авто-узел: pick_best_node + фолбэк на self */
|
|
static uint64_t resolve_auto_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both, int* out_priority) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
int has_both = 0, prio = 4;
|
|
uint64_t best = pick_best_node(inst, channel_id, &has_both, &prio);
|
|
if (best == 0) best = cc->my_node_id;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto selection ch=%llu node=0x%016llx role=%s self=%d",
|
|
(unsigned long long)channel_id, (unsigned long long)best,
|
|
prio == 1 ? "supernode" : prio == 2 ? "regular" : "self", best == cc->my_node_id);
|
|
if (out_has_both) *out_has_both = has_both;
|
|
if (out_priority) *out_priority = prio;
|
|
return best;
|
|
}
|
|
|
|
/* ── Фаза 3: pubkey узла ── */
|
|
|
|
static int get_node_pubkey(struct UTUN_INSTANCE* inst, uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (is_self) {
|
|
if (!inst) return -1;
|
|
memcpy(pubkey_out, inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
|
|
return 0;
|
|
}
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?",
|
|
-1, &st, NULL) != SQLITE_OK) return -1;
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
|
int ok = 0;
|
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
|
const void* pk = sqlite3_column_blob(st, 0);
|
|
int pklen = sqlite3_column_bytes(st, 0);
|
|
if (pk && pklen >= INVITE_PUBKEY_SIZE) { memcpy(pubkey_out, pk, INVITE_PUBKEY_SIZE); ok = 1; }
|
|
}
|
|
sqlite3_finalize(st);
|
|
return ok ? 0 : -1;
|
|
}
|
|
|
|
/* ── Фаза 4a: свои адреса из сокетов ── */
|
|
|
|
static int collect_self_addrs(struct UTUN_INSTANCE* inst, struct InviteAddr* out, int max_cnt) {
|
|
int cnt = 0;
|
|
if (!inst) return 0;
|
|
struct ETCP_SOCKET* s;
|
|
|
|
/* UDP */
|
|
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
|
|
if (s->is_tcp) continue;
|
|
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
|
|
if (sa->ss_family == AF_INET) {
|
|
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
|
out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP;
|
|
memcpy(out[cnt].address, &sin->sin_addr, 4);
|
|
out[cnt].port = ntohs(sin->sin_port); cnt++;
|
|
} else if (sa->ss_family == AF_INET6) {
|
|
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
|
out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = INVITE_PROTO_UDP;
|
|
memcpy(out[cnt].address, &sin6->sin6_addr, 16);
|
|
out[cnt].port = ntohs(sin6->sin6_port); cnt++;
|
|
}
|
|
}
|
|
/* TCP */
|
|
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
|
|
if (!s->is_tcp) continue;
|
|
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
|
|
uint8_t proto = INVITE_PROTO_TCP | (s->reality_enabled ? INVITE_PROTO_REALITY : 0);
|
|
if (sa->ss_family == AF_INET) {
|
|
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
|
|
out[cnt].family = 4; out[cnt].socketId = s->sock_id; out[cnt].proto = proto;
|
|
memcpy(out[cnt].address, &sin->sin_addr, 4);
|
|
out[cnt].port = ntohs(sin->sin_port); cnt++;
|
|
} else if (sa->ss_family == AF_INET6) {
|
|
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
|
|
out[cnt].family = 6; out[cnt].socketId = s->sock_id; out[cnt].proto = proto;
|
|
memcpy(out[cnt].address, &sin6->sin6_addr, 16);
|
|
out[cnt].port = ntohs(sin6->sin6_port); cnt++;
|
|
}
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
/* reality-параметры self-узла из конфига (node-global, как в topo_node_update_my_addresses). */
|
|
static void collect_self_reality(struct UTUN_INSTANCE* inst, struct InviteReality* out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!inst || !inst->config) return;
|
|
const struct reality_config* rc = &inst->config->global.reality;
|
|
if (!rc->enabled || !rc->has_private_key) return;
|
|
int any = 0;
|
|
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next)
|
|
if (s->is_tcp && s->reality_enabled) { any = 1; break; }
|
|
if (!any) return;
|
|
uint8_t pub[32];
|
|
if (reality_pubkey_from_priv(rc->private_key, pub) != REALITY_OK) return;
|
|
out->has = 1;
|
|
if (rc->short_id_count > 0) memcpy(out->short_id, rc->short_ids[0], 8);
|
|
else memcpy(out->short_id, rc->short_id, 8);
|
|
memcpy(out->server_pubkey, pub, 32);
|
|
memcpy(out->version, rc->version, 3);
|
|
strncpy(out->server_name, rc->server_name, INVITE_SNAME_MAX - 1);
|
|
out->server_name[INVITE_SNAME_MAX - 1] = '\0';
|
|
}
|
|
|
|
/* ── Фаза 4b: адреса узла из БД ── */
|
|
|
|
static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
|
|
struct InviteReality* reality) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
char sql[256];
|
|
if (dir_only)
|
|
snprintf(sql, sizeof(sql),
|
|
"SELECT family,socket_id,protocol,address,port,options FROM node_addresses"
|
|
" WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT);
|
|
else
|
|
snprintf(sql, sizeof(sql),
|
|
"SELECT family,socket_id,protocol,address,port,options FROM node_addresses WHERE node_id=?");
|
|
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
|
|
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
|
|
|
int cnt = 0;
|
|
while (sqlite3_step(st) == SQLITE_ROW && cnt < max_cnt) {
|
|
int family = sqlite3_column_int(st, 0);
|
|
out[cnt].family = family;
|
|
out[cnt].socketId = (uint8_t)sqlite3_column_int(st, 1);
|
|
out[cnt].proto = (uint8_t)sqlite3_column_int(st, 2);
|
|
const uint8_t* addr = sqlite3_column_blob(st, 3);
|
|
int addr_len = sqlite3_column_bytes(st, 3);
|
|
int ip_len = (family == 6) ? 16 : 4;
|
|
memset(out[cnt].address, 0, sizeof(out[cnt].address));
|
|
if (addr && addr_len >= ip_len) memcpy(out[cnt].address, addr, (size_t)ip_len);
|
|
out[cnt].port = (uint16_t)sqlite3_column_int(st, 4);
|
|
if (reality && !reality->has && (out[cnt].proto & INVITE_PROTO_REALITY)
|
|
&& sqlite3_column_type(st, 5) == SQLITE_BLOB
|
|
&& sqlite3_column_bytes(st, 5) == TOPO_ADDR_REALITY_OPTS_SIZE) {
|
|
const struct TOPO_ADDR_REALITY_OPTS* o = (const struct TOPO_ADDR_REALITY_OPTS*)sqlite3_column_blob(st, 5);
|
|
reality->has = 1;
|
|
memcpy(reality->short_id, o->short_id, 8);
|
|
memcpy(reality->server_pubkey, o->server_pubkey, 32);
|
|
memcpy(reality->version, o->version, 3);
|
|
memcpy(reality->server_name, o->server_name, INVITE_SNAME_MAX);
|
|
reality->server_name[INVITE_SNAME_MAX - 1] = '\0';
|
|
}
|
|
cnt++;
|
|
}
|
|
sqlite3_finalize(st);
|
|
return cnt;
|
|
}
|
|
|
|
/* ── публичный API ── */
|
|
|
|
static int prepare_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
|
|
const char* password, char* out, size_t out_size, uint64_t* target, uint64_t* key) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!out || out_size == 0) return -1;
|
|
out[0] = '\0';
|
|
if (!cc || !cc->initialized || !inst || !cc->db) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: not initialized (inst=%p db=%p)",
|
|
(void*)inst, (void*)(cc ? cc->db : NULL));
|
|
return -1;
|
|
}
|
|
if (channel_id == 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: invalid channel_id");
|
|
return -1;
|
|
}
|
|
|
|
/* ── выбор узла ── */
|
|
uint64_t target_nid = target_node_id;
|
|
int use_self = 0, has_both_public = 0;
|
|
|
|
if (target_nid == 0) {
|
|
int prio = 4;
|
|
target_nid = resolve_auto_node(inst, channel_id, &has_both_public, &prio);
|
|
use_self = (target_nid == cc->my_node_id) ? 1 : 0;
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto ch=%llu best=0x%016llx prio=%d self=%d both=%d",
|
|
(unsigned long long)channel_id, (unsigned long long)target_nid,
|
|
prio, use_self, has_both_public);
|
|
} else {
|
|
if (target_nid == cc->my_node_id) {
|
|
use_self = 1;
|
|
} else {
|
|
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
|
|
if (!node_is_alive(inst, grp, target_nid)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC,
|
|
"invite_build: node 0x%016llx not reachable in ch=%llu",
|
|
(unsigned long long)target_nid, (unsigned long long)channel_id);
|
|
return -1;
|
|
}
|
|
has_both_public = node_has_both_public(inst, target_nid);
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: manual ch=%llu target=0x%016llx self=%d both=%d",
|
|
(unsigned long long)channel_id, (unsigned long long)target_nid, use_self, has_both_public);
|
|
}
|
|
|
|
/* ── pubkey ── */
|
|
struct InviteData data;
|
|
memset(&data, 0, sizeof(data));
|
|
data.channelId = channel_id;
|
|
data.nodeId = target_nid;
|
|
/* join-ключ: генерируется инвайтером, кладётся в ссылку и регистрируется на connection-узле */
|
|
if (RAND_bytes((uint8_t*)&data.join_key, sizeof(data.join_key)) != 1 || data.join_key == 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: join_key generation failed");
|
|
return -1;
|
|
}
|
|
if (get_node_pubkey(inst, target_nid, use_self, data.pubkey) != 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no pubkey for node=0x%016llx",
|
|
(unsigned long long)target_nid);
|
|
return -1;
|
|
}
|
|
|
|
/* ── адреса ── */
|
|
if (use_self) {
|
|
data.addrCount = (uint8_t)collect_self_addrs(inst, data.addrs, INVITE_ADDR_MAX);
|
|
collect_self_reality(inst, &data.reality);
|
|
} else {
|
|
int dir_only = has_both_public ? 1 : 0;
|
|
data.addrCount = (uint8_t)collect_node_addrs_from_db(inst, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
|
|
}
|
|
if (data.addrCount == 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no addrs for node=0x%016llx",
|
|
(unsigned long long)target_nid);
|
|
return -1;
|
|
}
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: node=0x%016llx addrs=%d dir_only=%d",
|
|
(unsigned long long)target_nid, data.addrCount, (!use_self && has_both_public) ? 1 : 0);
|
|
|
|
/* ── encode ── */
|
|
if (invite_link_encode(&data, password, out, out_size) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: encode failed ch=%llu",
|
|
(unsigned long long)channel_id);
|
|
return -1;
|
|
}
|
|
|
|
/* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */
|
|
*target = target_nid;
|
|
*key = data.join_key;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!out_node_id) return -1;
|
|
if (!cc || !cc->initialized || !inst || !cc->db) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: best_node — not initialized");
|
|
return -1;
|
|
}
|
|
*out_node_id = resolve_auto_node(inst, channel_id, NULL, NULL);
|
|
return 0;
|
|
}
|
|
|
|
struct chat_invite_build {
|
|
struct chat_join_registration* registration;
|
|
chat_invite_ready_fn callback;
|
|
void* arg;
|
|
char link[1024];
|
|
};
|
|
|
|
static void invite_registered(void* arg, int result) {
|
|
struct chat_invite_build* req = arg;
|
|
req->registration = NULL;
|
|
req->callback(req->arg, result, result == CHAT_JOIN_OK ? req->link : NULL);
|
|
u_free(req);
|
|
}
|
|
|
|
struct chat_invite_build* chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
|
|
const char* password, chat_invite_ready_fn callback, void* arg) {
|
|
if (!callback) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: missing callback"); return NULL; }
|
|
struct chat_invite_build* req = u_calloc(1, sizeof(*req));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: allocation failed"); return NULL; }
|
|
uint64_t target, key;
|
|
if (prepare_link(inst, channel_id, target_node_id, password, req->link, sizeof(req->link), &target, &key) != 0) {
|
|
u_free(req); return NULL;
|
|
}
|
|
req->callback = callback; req->arg = arg;
|
|
req->registration = chat_join_register_key(inst, channel_id, target, key, invite_registered, req);
|
|
if (!req->registration) { u_free(req); return NULL; }
|
|
return req;
|
|
}
|
|
|
|
void chat_invite_build_cancel(struct chat_invite_build* req) {
|
|
if (req && req->registration) chat_join_cancel_registration(req->registration);
|
|
}
|
|
|
|
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) {
|
|
struct chat_core_ctx* cc = CC(inst);
|
|
if (!out_ids || max_ids <= 0) return -1;
|
|
if (!cc || !cc->initialized || !inst || !cc->db) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized");
|
|
return -1;
|
|
}
|
|
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
|
|
if (!grp) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — group not found ch=%llu",
|
|
(unsigned long long)channel_id);
|
|
return -1;
|
|
}
|
|
|
|
char ch_str[32];
|
|
snprintf(ch_str, sizeof(ch_str), "%llu", (unsigned long long)channel_id);
|
|
char peers_tbl[80];
|
|
peers_table_name(ch_str, peers_tbl, sizeof(peers_tbl));
|
|
|
|
char sql[512];
|
|
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" ORDER BY node_id", peers_tbl);
|
|
sqlite3_stmt* st = NULL;
|
|
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — query failed ch=%llu err=%s",
|
|
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
|
|
return -1;
|
|
}
|
|
|
|
int cnt = 0;
|
|
while (sqlite3_step(st) == SQLITE_ROW && cnt < max_ids) {
|
|
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
|
|
if (node_is_alive(inst, grp, nid)) out_ids[cnt++] = nid;
|
|
}
|
|
sqlite3_finalize(st);
|
|
return cnt;
|
|
}
|
|
|
|
/* ── трамплины для gui_bridge ── */
|
|
|
|
void chat_invite_candidates_trampoline(void* arg) {
|
|
struct chat_invite_cand_req* req = (struct chat_invite_cand_req*)arg;
|
|
if (!req) 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);
|
|
if (!CC(inst) || !CC(inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
|
|
|
|
uint64_t channel_id = strtoull(ch_id, NULL, 10);
|
|
uint64_t auto_id = 0;
|
|
chat_invite_best_node(inst, channel_id, &auto_id);
|
|
uint64_t ids[INVITE_ADDR_MAX];
|
|
int cnt = chat_invite_candidate_nodes(inst, channel_id, ids, INVITE_ADDR_MAX);
|
|
if (cnt < 0) cnt = 0;
|
|
|
|
size_t ch_len = strlen(ch_id);
|
|
size_t evt_sz = 1 + ch_len + 8 + 2 + (size_t)cnt * 8;
|
|
uint8_t* evt = u_malloc(evt_sz);
|
|
if (!evt) return;
|
|
evt[0] = (uint8_t)ch_len;
|
|
memcpy(evt + 1, ch_id, ch_len);
|
|
memcpy(evt + 1 + ch_len, &auto_id, 8);
|
|
uint16_t u16 = (uint16_t)cnt;
|
|
memcpy(evt + 1 + ch_len + 8, &u16, 2);
|
|
for (int i = 0; i < cnt; i++) memcpy(evt + 1 + ch_len + 8 + 2 + i * 8, &ids[i], 8);
|
|
chat_event_post(inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
|
|
u_free(evt);
|
|
}
|
|
|
|
static void gui_invite_ready(void* arg, int result, const char* link) {
|
|
struct chat_invite_build_req* req = arg;
|
|
req->inst->invite_gui_request = NULL;
|
|
if (result == CHAT_JOIN_CANCELLED) { u_free(req); return; }
|
|
size_t ch_len = strlen(req->ch_id);
|
|
size_t link_len = link ? strlen(link) : 0;
|
|
size_t evt_sz = 1 + ch_len + 8 + 2 + link_len;
|
|
uint8_t* evt = u_malloc(evt_sz);
|
|
if (!evt) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: event allocation failed"); u_free(req); return; }
|
|
evt[0] = (uint8_t)ch_len;
|
|
memcpy(evt + 1, req->ch_id, ch_len);
|
|
uint16_t u16 = (uint16_t)link_len;
|
|
memcpy(evt + 1 + ch_len, &req->request_id, 8);
|
|
memcpy(evt + 1 + ch_len + 8, &u16, 2);
|
|
if (link_len) memcpy(evt + 1 + ch_len + 10, link, link_len);
|
|
chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
|
|
u_free(evt);
|
|
u_free(req);
|
|
}
|
|
|
|
void chat_invite_build_link_trampoline(void* arg) {
|
|
struct chat_invite_build_req* req = arg;
|
|
if (!req) return;
|
|
if (!req->inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: GUI request without instance"); u_free(req); return; }
|
|
if (req->inst->invite_gui_request) chat_invite_build_cancel(req->inst->invite_gui_request->pending);
|
|
req->inst->invite_gui_request = req;
|
|
req->pending = chat_invite_build_link(req->inst, strtoull(req->ch_id, NULL, 10), req->target_node_id,
|
|
req->password[0] ? req->password : NULL, gui_invite_ready, req);
|
|
if (!req->pending) gui_invite_ready(req, CHAT_JOIN_ERROR, NULL);
|
|
}
|