chat: invite-ссылки с выбором достижимого узла + tcp_io фикс half-close FIN
- Общий модуль 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 (был недостижим)
This commit is contained in:
1 parent
a17a85a383
commit
1345f44e10
23 files changed
+872
-539
No files matched your search
@@ -83,6 +83,7 @@ utun_CORE_SOURCES = \
|
||||
chat/member_sync.c \
|
||||
chat/merkle_sync.c \
|
||||
chat/invite_link.c \
|
||||
chat/invite_build.c \
|
||||
chat/chat_headless_control.c \
|
||||
broadcast.c
|
||||
|
||||
@@ -166,6 +167,7 @@ libutun_a_SOURCES = \
|
||||
chat/member_sync.c \
|
||||
chat/merkle_sync.c \
|
||||
chat/invite_link.c \
|
||||
chat/invite_build.c \
|
||||
chat/chat_headless_control.c \
|
||||
broadcast.c
|
||||
libutun_a_CFLAGS = $(utun_CFLAGS)
|
||||
|
||||
@@ -47,6 +47,8 @@ extern "C" {
|
||||
#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][link_len:2][link:var] — пустой link = ошибка */
|
||||
#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
|
||||
|
||||
typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
#include "chat_headless_control.h"
|
||||
#include "invite_link.h"
|
||||
#include "invite_build.h"
|
||||
#include "chat_core.h"
|
||||
#include "chat_sync.h"
|
||||
#include "chat_event.h"
|
||||
@@ -301,76 +302,63 @@ static void hc_handle_send(struct headless_client* cli, int id, const char* json
|
||||
}
|
||||
|
||||
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
|
||||
char ch[64];
|
||||
char ch[64], node_id_str[32];
|
||||
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
|
||||
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
|
||||
|
||||
uint64_t my_node_id = g_hc.inst->node_id;
|
||||
uint8_t my_pubkey[32]; memcpy(my_pubkey, g_hc.inst->my_keys.public_key, 32);
|
||||
uint64_t channel_id = strtoull(ch, NULL, 10);
|
||||
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
|
||||
|
||||
sqlite3* db = chat_core_get_db();
|
||||
if (!db) { send_response(cli, id, NULL, "no database"); return; }
|
||||
|
||||
struct InviteData inv; memset(&inv, 0, sizeof(inv));
|
||||
inv.channelId = strtoull(ch, NULL, 10);
|
||||
if (inv.channelId == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
|
||||
memcpy(inv.pubkey, my_pubkey, 32);
|
||||
inv.nodeId = my_node_id;
|
||||
|
||||
sqlite3_stmt* as = NULL;
|
||||
sqlite3_prepare_v2(db, "SELECT family, protocol, address, port, rtt, socket_id FROM node_addresses WHERE node_id=? AND protocol IN (1,2) ORDER BY family, protocol",
|
||||
-1, &as, NULL);
|
||||
if (as) {
|
||||
sqlite3_bind_int64(as, 1, (sqlite3_int64)my_node_id);
|
||||
while (sqlite3_step(as) == SQLITE_ROW && inv.addrCount < INVITE_ADDR_MAX) {
|
||||
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
|
||||
int fam = sqlite3_column_int(as, 0);
|
||||
a->proto = (uint8_t)sqlite3_column_int(as, 1);
|
||||
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(as, 2);
|
||||
int addr_len = sqlite3_column_bytes(as, 2);
|
||||
a->port = (uint16_t)sqlite3_column_int(as, 3);
|
||||
a->socketId = (uint8_t)sqlite3_column_int(as, 5);
|
||||
a->family = fam;
|
||||
memset(a->address, 0, sizeof(a->address));
|
||||
if (addr && addr_len > 0) memcpy(a->address, addr, addr_len < 16 ? (size_t)addr_len : 16);
|
||||
}
|
||||
sqlite3_finalize(as);
|
||||
}
|
||||
|
||||
if (inv.addrCount == 0) {
|
||||
/* fallback: collect from sockets directly */
|
||||
struct ETCP_SOCKET* s = g_hc.inst->etcp_sockets;
|
||||
while (s && inv.addrCount < INVITE_ADDR_MAX) {
|
||||
if (s->is_tcp) { s = s->next; continue; }
|
||||
if (s->local_addr.ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)&s->local_addr;
|
||||
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
|
||||
a->family = 4; a->proto = 1; a->socketId = s->sock_id;
|
||||
a->port = ntohs(sin->sin_port); memcpy(a->address, &sin->sin_addr, 4);
|
||||
}
|
||||
s = s->next;
|
||||
}
|
||||
struct ETCP_SOCKET* ts = g_hc.inst->etcp_sockets;
|
||||
while (ts && inv.addrCount < INVITE_ADDR_MAX) {
|
||||
if (!ts->is_tcp) { ts = ts->next; continue; }
|
||||
if (ts->interface_addr.ss_family == AF_INET) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)&ts->interface_addr;
|
||||
struct InviteAddr* a = &inv.addrs[inv.addrCount++];
|
||||
a->family = 4; a->proto = 2; a->socketId = ts->sock_id;
|
||||
a->port = ntohs(sin->sin_port); memcpy(a->address, &sin->sin_addr, 4);
|
||||
}
|
||||
ts = ts->next;
|
||||
}
|
||||
}
|
||||
|
||||
if (inv.addrCount == 0) { send_response(cli, id, NULL, "no addresses available to build invite link"); return; }
|
||||
/* опциональный выбор узла: "0" или отсутствует → auto (лучший узел) */
|
||||
uint64_t target_node_id = 0;
|
||||
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0)
|
||||
target_node_id = strtoull(node_id_str, NULL, 16);
|
||||
|
||||
char link[1024];
|
||||
int r = invite_link_encode(&inv, NULL, link, sizeof(link));
|
||||
if (r < 0) { send_response(cli, id, NULL, "failed to encode invite link"); return; }
|
||||
if (chat_invite_build_link(channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
|
||||
send_response(cli, id, NULL, "failed to build invite link (see log)");
|
||||
return;
|
||||
}
|
||||
|
||||
char resp[1280]; snprintf(resp, sizeof(resp), "{\"link\":\"%s\",\"channel_id\":%llu,\"addrs\":%d}", link,
|
||||
(unsigned long long)inv.channelId, (int)inv.addrCount);
|
||||
struct InviteData d; char err[128];
|
||||
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
|
||||
send_response(cli, id, NULL, "failed to decode built link");
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite ch=%s target=0x%016llx node=0x%016llx addrs=%d",
|
||||
ch, (unsigned long long)target_node_id, (unsigned long long)d.nodeId, (int)d.addrCount);
|
||||
|
||||
char resp[1280];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
|
||||
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
|
||||
send_response(cli, id, resp, NULL);
|
||||
}
|
||||
|
||||
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
|
||||
char ch[64];
|
||||
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
|
||||
uint64_t channel_id = strtoull(ch, NULL, 10);
|
||||
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
|
||||
|
||||
uint64_t ids[INVITE_ADDR_MAX];
|
||||
int cnt = chat_invite_candidate_nodes(channel_id, ids, INVITE_ADDR_MAX);
|
||||
if (cnt < 0) { send_response(cli, id, NULL, "no candidates (see log)"); return; }
|
||||
|
||||
uint64_t auto_id = 0;
|
||||
chat_invite_best_node(channel_id, &auto_id);
|
||||
|
||||
char resp[4096]; int off = 0;
|
||||
off += snprintf(resp + off, sizeof(resp) - off, "{\"ch\":\"%s\",\"auto_node_id\":\"0x%016llx\",\"nodes\":[",
|
||||
ch, (unsigned long long)auto_id);
|
||||
for (int i = 0; i < cnt && off < (int)sizeof(resp) - 256; i++) {
|
||||
char name[64] = "";
|
||||
chat_core_get_node_name(ids[i], name, sizeof(name));
|
||||
off += snprintf(resp + off, sizeof(resp) - off, "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\"}",
|
||||
i ? "," : "", (unsigned long long)ids[i], name);
|
||||
}
|
||||
off += snprintf(resp + off, sizeof(resp) - off, "]}");
|
||||
send_response(cli, id, resp, NULL);
|
||||
}
|
||||
|
||||
@@ -476,8 +464,8 @@ static void hc_handle_quit(struct headless_client* cli, int id, const char* json
|
||||
|
||||
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json);
|
||||
|
||||
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "invite", "connect", "create_channel", "invite_to", "subscribe", "quit", NULL };
|
||||
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_invite, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_quit };
|
||||
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "quit", NULL };
|
||||
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_quit };
|
||||
|
||||
void hc_handle_command(struct headless_client* cli, const char* json) {
|
||||
if (!cli || !json) return;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
|
||||
* Event: {"event":"<type>",...}
|
||||
*
|
||||
* Commands: ping, status, channels, members, messages, send, invite, connect,
|
||||
* create_channel, subscribe, quit
|
||||
* Commands: ping, status, channels, members, messages, send, invite,
|
||||
* invite_nodes, connect, create_channel, subscribe, quit
|
||||
*/
|
||||
#ifndef CHAT_HEADLESS_CONTROL_H
|
||||
#define CHAT_HEADLESS_CONTROL_H
|
||||
|
||||
+49
-33
@@ -94,8 +94,7 @@ void chat_core_submit_trampoline(void* arg) {
|
||||
struct media_submit_ctx {
|
||||
char channel_id[64];
|
||||
char content_type[32];
|
||||
char media_dest[512];
|
||||
char media_base[512];
|
||||
char media_dest[1024];
|
||||
uint8_t msg_data[4096];
|
||||
uint32_t msg_data_len;
|
||||
uint64_t timestamp;
|
||||
@@ -177,7 +176,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
|
||||
CC_ID, mctx->channel_id, (unsigned long long)ts);
|
||||
const char* base = strrchr(mctx->media_dest, '/');
|
||||
const char* filename = base ? base + 1 : mctx->media_dest;
|
||||
char attrs[320]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
|
||||
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
|
||||
chat_core_update_local_attrs(mctx->channel_id, ts, json_sig, attrs);
|
||||
|
||||
/* whisper транскрипция для своих голосовых сообщений */
|
||||
@@ -222,7 +221,6 @@ static void chat_core_submit_media_message(struct chat_msg_submit* req) {
|
||||
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
|
||||
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
|
||||
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
|
||||
snprintf(mctx->media_base, sizeof(mctx->media_base), "%s", media_base);
|
||||
mctx->timestamp = req->timestamp;
|
||||
if (req->data && req->data_len > 0) {
|
||||
mctx->msg_data_len = req->data_len < sizeof(mctx->msg_data) ? req->data_len : sizeof(mctx->msg_data) - 1;
|
||||
@@ -557,6 +555,22 @@ static void sanitize_filename(char* buf, size_t size) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: filename sanitized [%s] -> [%s]", CC_ID, orig, buf);
|
||||
}
|
||||
|
||||
/* Извлекает имя файла из тела "d"-поля медиасообщения: берёт сегмент до '|',
|
||||
декодирует base64url-префикс '*', если он есть, и санитизирует. */
|
||||
static void extract_base_filename(const char* body, char* out, size_t out_sz) {
|
||||
size_t bfi = 0;
|
||||
while (body[bfi] && body[bfi] != '|' && bfi < out_sz - 1)
|
||||
out[bfi++] = body[bfi];
|
||||
out[bfi] = '\0';
|
||||
if (out[0] == '*' && bfi > 1) {
|
||||
uint8_t dec[256];
|
||||
int dlen = (int)b64_decode(out + 1, bfi - 1, dec, sizeof(dec));
|
||||
if (dlen > 0 && dlen < (int)out_sz) { memcpy(out, dec, (size_t)dlen); out[dlen] = '\0'; }
|
||||
}
|
||||
if (bfi == 0) snprintf(out, out_sz, "file");
|
||||
sanitize_filename(out, out_sz);
|
||||
}
|
||||
|
||||
static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
const char* data_str, size_t data_len,
|
||||
const char* ch_id, const char* base_filename,
|
||||
@@ -607,6 +621,16 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
p += 128; if (n < nb - 1 && *p == ',') p++;
|
||||
}
|
||||
|
||||
/* для голосовых имя генерируем из media_id: msg_data — это wf-строка, а не имя файла */
|
||||
char final_name[256];
|
||||
if (content_type && strcmp(content_type, "audio/opus") == 0) {
|
||||
char hex[33];
|
||||
for (int i = 0; i < 16; i++) snprintf(hex + i * 2, 3, "%02x", result.media_id[i]);
|
||||
snprintf(final_name, sizeof(final_name), "voice_%s.opus", hex);
|
||||
} else {
|
||||
snprintf(final_name, sizeof(final_name), "%s", base_filename);
|
||||
}
|
||||
|
||||
char dest[1024], media_base[512];
|
||||
{
|
||||
const char* last_slash = strrchr(g_cc.db_path, '/');
|
||||
@@ -616,23 +640,21 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
|
||||
}
|
||||
}
|
||||
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, base_filename);
|
||||
|
||||
const char* fp_name = strrchr(dest, '/'); fp_name = fp_name ? fp_name + 1 : dest;
|
||||
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
|
||||
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { media_index_result_free(&result); return -1; }
|
||||
snprintf(ctx->channel_id, sizeof(ctx->channel_id), "%s", ch_id);
|
||||
ctx->ts = ts; ctx->msg_id = msg_id;
|
||||
snprintf(ctx->dest_relpath, sizeof(ctx->dest_relpath), "%s", fp_name);
|
||||
snprintf(ctx->dest_relpath, sizeof(ctx->dest_relpath), "%s", final_name);
|
||||
snprintf(ctx->dest_abs_path, sizeof(ctx->dest_abs_path), "%s", dest);
|
||||
ctx->author_node_id = author_node_id;
|
||||
if (content_type) snprintf(ctx->content_type, sizeof(ctx->content_type), "%s", content_type);
|
||||
else ctx->content_type[0] = '\0';
|
||||
if (author_sig) memcpy(ctx->author_sig, author_sig, 64);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s file=%s blocks=%d size=%lld dest=%s",
|
||||
CC_ID, ch_id, base_filename, nb, (long long)fsize, dest);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s ct=%s file=%s blocks=%d size=%lld dest=%s",
|
||||
CC_ID, ch_id, content_type ? content_type : "?", final_name, nb, (long long)fsize, dest);
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
media_download_start(inst, gid, &result, dest, media_base, author_node_id, md_download_done_cb, ctx, md_download_progress_cb, ctx);
|
||||
media_index_result_free(&result);
|
||||
@@ -690,17 +712,8 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
|
||||
while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++;
|
||||
body[bi] = '\0';
|
||||
|
||||
char base_filename[256]; size_t bfi = 0;
|
||||
while (body[bfi] && body[bfi] != '|' && bfi < sizeof(base_filename) - 1)
|
||||
base_filename[bfi++] = body[bfi];
|
||||
base_filename[bfi] = '\0';
|
||||
/* base64-decode display prefix if marked with '*' (new format) */
|
||||
if (base_filename[0] == '*' && bfi > 1) {
|
||||
uint8_t dec[256]; int dlen = (int)b64_decode(base_filename + 1, bfi - 1, dec, sizeof(dec));
|
||||
if (dlen > 0 && dlen < (int)sizeof(base_filename)) { memcpy(base_filename, dec, (size_t)dlen); base_filename[dlen] = '\0'; }
|
||||
}
|
||||
if (bfi == 0) snprintf(base_filename, sizeof(base_filename), "file");
|
||||
sanitize_filename(base_filename, sizeof(base_filename));
|
||||
char base_filename[256];
|
||||
extract_base_filename(body, base_filename, sizeof(base_filename));
|
||||
|
||||
const uint8_t* sig_field = (const uint8_t*)strstr(buf, "\"sig\":\"");
|
||||
uint8_t author_sig[64] = {0};
|
||||
@@ -777,23 +790,26 @@ void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
|
||||
while (*d_start && *d_start != '\"' && bi < sizeof(body) - 1) body[bi++] = *d_start++;
|
||||
body[bi] = '\0';
|
||||
|
||||
char base_filename[256]; size_t bfi = 0;
|
||||
while (body[bfi] && body[bfi] != '|' && bfi < sizeof(base_filename) - 1)
|
||||
base_filename[bfi++] = body[bfi];
|
||||
base_filename[bfi] = '\0';
|
||||
if (base_filename[0] == '*' && bfi > 1) {
|
||||
uint8_t dec[256]; int dlen = (int)b64_decode(base_filename + 1, bfi - 1, dec, sizeof(dec));
|
||||
if (dlen > 0 && dlen < (int)sizeof(base_filename)) { memcpy(base_filename, dec, (size_t)dlen); base_filename[dlen] = '\0'; }
|
||||
char content_type[32] = {0};
|
||||
{
|
||||
const char* ct = strstr((const char*)jdata, "\"ct\":\"");
|
||||
if (ct) {
|
||||
const char* cv = ct + 6;
|
||||
int i = 0;
|
||||
while (cv[i] && cv[i] != '\"' && i < (int)sizeof(content_type) - 1) content_type[i++] = cv[i];
|
||||
content_type[i] = '\0';
|
||||
}
|
||||
}
|
||||
if (bfi == 0) snprintf(base_filename, sizeof(base_filename), "file");
|
||||
sanitize_filename(base_filename, sizeof(base_filename));
|
||||
|
||||
char base_filename[256];
|
||||
extract_base_filename(body, base_filename, sizeof(base_filename));
|
||||
|
||||
uint8_t author_sig[64]; memcpy(author_sig, sig_blob, 64);
|
||||
sqlite3_finalize(st);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld file=%s",
|
||||
CC_ID, channel_id, (long long)msg_id, base_filename);
|
||||
md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, NULL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld ct=%s file=%s",
|
||||
CC_ID, channel_id, (long long)msg_id, content_type, base_filename);
|
||||
md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
|
||||
}
|
||||
|
||||
static void chat_core_attachment_download_trampoline_impl(void* arg) {
|
||||
|
||||
@@ -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 */
|
||||
Reference in new issue
Block a user