Browse Source
- Общий модуль invite_build: сборка utun://-ссылок, авто-выбор лучшего достижимого узла, список кандидатов (вынесено из android jni_bridge) - chatgui: диалог invite с выбором узла (комбобокс), события INVITE_CANDIDATES / INVITE_LINK_READY через gui_bridge - headless/chatcli: invite <ch_id> [node_id], команда invite_nodes - tcp_io: error_cb отличает грациозный EPOLLHUP (SO_ERROR==0) от реальной ошибки — при приостановленном чтении FIN не роняет недослейные данные (фикс flaky half-close) - run_test.sh: half-close echo на выделенном порту 19190 + отдельный DNAT (был недостижим)v2
23 changed files with 870 additions and 537 deletions
@ -0,0 +1,410 @@
|
||||
/*
|
||||
* invite_build.c — сборка invite-ссылок utun:// (выбор узла + адреса)
|
||||
* |
||||
* Общий модуль для всех GUI (desktop chatgui, Android, headless CLI). |
||||
* Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge. |
||||
* Работает в uasync-потоке, использует общий контекст g_cc. |
||||
*/ |
||||
#include "invite_build.h" |
||||
#include "invite_link.h" |
||||
#include "chat_core_priv.h" |
||||
#include "chat_event.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> |
||||
|
||||
/* ── живость узла в канале (BGP/прямое/через посредника) ── */ |
||||
|
||||
static struct TOPO_GROUP* resolve_chat_group(uint64_t channel_id) { |
||||
if (!g_cc.inst || !g_cc.inst->topo_groups) return NULL; |
||||
struct TOPO_GROUP* grp = topo_groups_find(g_cc.inst->topo_groups, channel_id); |
||||
if (!grp || grp->group_type != TOPO_GROUP_TYPE_CHAT) return NULL; |
||||
return grp; |
||||
} |
||||
|
||||
static int node_is_alive(struct TOPO_GROUP* grp, uint64_t node_id) { |
||||
if (node_id == g_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(uint64_t node_id) { |
||||
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(g_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(uint64_t channel_id, int* out_has_both_public, int* out_priority) { |
||||
uint64_t best_nid = 0; |
||||
int best_priority = 4; /* 1=regular, 2=self, 3=super, 4=invalid */ |
||||
int best_has_both = 0; |
||||
|
||||
struct TOPO_GROUP* grp = resolve_chat_group(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(g_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(g_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(grp, nid)) continue; |
||||
|
||||
int has_both = node_has_both_public(nid); |
||||
|
||||
int priority; |
||||
if (nid == g_cc.my_node_id) priority = 2; /* self */ |
||||
else if (ntype == 4) priority = 3; /* supernode */ |
||||
else priority = 1; /* regular */ |
||||
|
||||
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(uint64_t channel_id, int* out_has_both, int* out_priority) { |
||||
int has_both = 0, prio = 4; |
||||
uint64_t best = pick_best_node(channel_id, &has_both, &prio); |
||||
if (best == 0) best = g_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(uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) { |
||||
if (is_self) { |
||||
if (!g_cc.inst) return -1; |
||||
memcpy(pubkey_out, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE); |
||||
return 0; |
||||
} |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(g_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 InviteAddr* out, int max_cnt) { |
||||
int cnt = 0; |
||||
if (!g_cc.inst) return 0; |
||||
struct ETCP_SOCKET* s; |
||||
|
||||
/* UDP */ |
||||
for (s = g_cc.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 = g_cc.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_TCP; |
||||
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_TCP; |
||||
memcpy(out[cnt].address, &sin6->sin6_addr, 16); |
||||
out[cnt].port = ntohs(sin6->sin6_port); cnt++; |
||||
} |
||||
} |
||||
return cnt; |
||||
} |
||||
|
||||
/* ── Фаза 4b: адреса узла из БД ── */ |
||||
|
||||
static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only) { |
||||
char sql[256]; |
||||
if (dir_only) |
||||
snprintf(sql, sizeof(sql), |
||||
"SELECT family,socket_id,protocol,address,port 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 FROM node_addresses WHERE node_id=?"); |
||||
|
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(g_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); |
||||
cnt++; |
||||
} |
||||
sqlite3_finalize(st); |
||||
return cnt; |
||||
} |
||||
|
||||
/* ── публичный API ── */ |
||||
|
||||
int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id, |
||||
const char* password, char* out, size_t out_size) { |
||||
if (!out || out_size == 0) return -1; |
||||
out[0] = '\0'; |
||||
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: not initialized (inst=%p db=%p)", |
||||
(void*)g_cc.inst, (void*)g_cc.db); |
||||
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(channel_id, &has_both_public, &prio); |
||||
use_self = (target_nid == g_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 == g_cc.my_node_id) { |
||||
use_self = 1; |
||||
} else { |
||||
struct TOPO_GROUP* grp = resolve_chat_group(channel_id); |
||||
if (!node_is_alive(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(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; |
||||
if (get_node_pubkey(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(data.addrs, INVITE_ADDR_MAX); |
||||
} else { |
||||
int dir_only = has_both_public ? 1 : 0; |
||||
data.addrCount = (uint8_t)collect_node_addrs_from_db(target_nid, data.addrs, INVITE_ADDR_MAX, dir_only); |
||||
} |
||||
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; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int chat_invite_best_node(uint64_t channel_id, uint64_t* out_node_id) { |
||||
if (!out_node_id) return -1; |
||||
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: best_node — not initialized"); |
||||
return -1; |
||||
} |
||||
*out_node_id = resolve_auto_node(channel_id, NULL, NULL); |
||||
return 0; |
||||
} |
||||
|
||||
int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_ids) { |
||||
if (!out_ids || max_ids <= 0) return -1; |
||||
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); |
||||
return -1; |
||||
} |
||||
struct TOPO_GROUP* grp = resolve_chat_group(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(g_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(g_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(grp, nid)) out_ids[cnt++] = nid; |
||||
} |
||||
sqlite3_finalize(st); |
||||
return cnt; |
||||
} |
||||
|
||||
/* ── трамплины для gui_bridge ── */ |
||||
|
||||
void chat_invite_candidates_trampoline(void* arg) { |
||||
if (!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); |
||||
if (!g_cc.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(channel_id, &auto_id); |
||||
uint64_t ids[INVITE_ADDR_MAX]; |
||||
int cnt = chat_invite_candidate_nodes(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(CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz); |
||||
u_free(evt); |
||||
} |
||||
|
||||
void chat_invite_build_link_trampoline(void* arg) { |
||||
struct chat_invite_build_req* req = (struct chat_invite_build_req*)arg; |
||||
if (!req) return; |
||||
|
||||
uint64_t channel_id = strtoull(req->ch_id, NULL, 10); |
||||
uint64_t target = req->target_node_id; |
||||
char password[INVITE_PASS_MAX]; |
||||
password[0] = '\0'; |
||||
const char* pass = NULL; |
||||
if (req->password[0]) { |
||||
strncpy(password, req->password, sizeof(password) - 1); |
||||
password[sizeof(password) - 1] = '\0'; |
||||
pass = password; |
||||
} |
||||
|
||||
char link[1024]; |
||||
int ok = chat_invite_build_link(channel_id, target, pass, link, sizeof(link)); |
||||
|
||||
size_t ch_len = strlen(req->ch_id); |
||||
size_t link_len = (ok == 0) ? strlen(link) : 0; |
||||
size_t evt_sz = 1 + ch_len + 2 + link_len; |
||||
uint8_t* evt = u_malloc(evt_sz); |
||||
if (!evt) { 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, &u16, 2); |
||||
if (link_len) memcpy(evt + 1 + ch_len + 2, link, link_len); |
||||
chat_event_post(CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz); |
||||
u_free(evt); |
||||
u_free(req); |
||||
} |
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* invite_build.h — сборка invite-ссылок utun:// (выбор узла + адреса)
|
||||
* |
||||
* Общий модуль для всех GUI (desktop chatgui, Android, headless CLI). |
||||
* Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge. |
||||
* Работает в uasync-потоке, использует общий контекст g_cc. |
||||
*/ |
||||
#ifndef INVITE_BUILD_H |
||||
#define INVITE_BUILD_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* Собрать invite-ссылку для канала.
|
||||
* target_node_id = 0 → auto (лучший достижимый мембер, иначе self). |
||||
* target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала). |
||||
* password = NULL → без пароля. |
||||
* Возвращает 0 и заполняет out, либо -1 (детали в логе). */ |
||||
int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id, |
||||
const char* password, char* out, size_t out_size); |
||||
|
||||
/* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto.
|
||||
* Возвращает количество (0..max_ids), либо -1. */ |
||||
int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_ids); |
||||
|
||||
/* Авто-узел: лучший достижимый мембер, иначе self. Возвращает 0 и заполняет out_node_id, либо -1. */ |
||||
int chat_invite_best_node(uint64_t channel_id, uint64_t* out_node_id); |
||||
|
||||
/* Трамплин для gui_bridge (arg = u_malloc(struct chat_invite_build_req)).
|
||||
* Результат: CHAT_EVT_INVITE_LINK_READY. */ |
||||
struct chat_invite_build_req { |
||||
char ch_id[64]; |
||||
uint64_t target_node_id; /* 0 = auto */ |
||||
char password[128]; |
||||
}; |
||||
void chat_invite_build_link_trampoline(void* arg); |
||||
|
||||
/* Трамплин (arg = u_strdup(ch_id)). Результат: CHAT_EVT_INVITE_CANDIDATES
|
||||
* (формат: [ch_id_len:1][ch_id][auto_node_id:8][count:2][node_id:8]*). */ |
||||
void chat_invite_candidates_trampoline(void* arg); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* INVITE_BUILD_H */ |
||||
Loading…
Reference in new issue